Skip to content

Commit f76d4de

Browse files
rshestfacebook-github-bot
authored andcommitted
Reference implementation (mock) for NativePerformanceObserver (#36116)
Summary: Pull Request resolved: #36116 [Changelog][Internal] Add a minimal/reference JavaScript implementation for NativePerformanceObserver - the purpose is both unit testing (JS and native sides separately) and potentially shimming the part of functionality that is not dependent on native side. This is both a setup for adding general unit tests for the Performance* APIs, but also to be able to do non-trivial changes on JS side for WebPerformance (such as in (D43154319). Reviewed By: rubennorte Differential Revision: D43167392 fbshipit-source-id: 213d9534d810dece1dd464f910e92e08dbf39508
1 parent 96fb708 commit f76d4de

File tree

8 files changed

+261
-71
lines changed

8 files changed

+261
-71
lines changed

Libraries/WebPerformance/NativePerformanceObserver.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,10 @@ void NativePerformanceObserver::setOnPerformanceEntryCallback(
4545
PerformanceEntryReporter::getInstance().setReportingCallback(callback);
4646
}
4747

48+
void NativePerformanceObserver::logRawEntry(
49+
jsi::Runtime &rt,
50+
RawPerformanceEntry entry) {
51+
PerformanceEntryReporter::getInstance().logEntry(entry);
52+
}
53+
4854
} // namespace facebook::react

Libraries/WebPerformance/NativePerformanceObserver.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ class NativePerformanceObserver
6969
jsi::Runtime &rt,
7070
std::optional<AsyncCallback<>> callback);
7171

72+
void logRawEntry(jsi::Runtime &rt, RawPerformanceEntry entry);
73+
7274
private:
7375
};
7476

Libraries/WebPerformance/NativePerformanceObserver.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,6 @@ import type {TurboModule} from '../TurboModule/RCTExport';
1212

1313
import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry';
1414

15-
export const RawPerformanceEntryTypeValues = {
16-
UNDEFINED: 0,
17-
MARK: 1,
18-
MEASURE: 2,
19-
EVENT: 3,
20-
};
21-
2215
export type RawPerformanceEntryType = number;
2316

2417
export type RawPerformanceEntry = {|

Libraries/WebPerformance/PerformanceObserver.js

Lines changed: 5 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -8,74 +8,15 @@
88
* @flow strict
99
*/
1010

11-
import type {
12-
RawPerformanceEntry,
13-
RawPerformanceEntryType,
14-
} from './NativePerformanceObserver';
1511
import type {PerformanceEntryType} from './PerformanceEntry';
1612

1713
import warnOnce from '../Utilities/warnOnce';
18-
import NativePerformanceObserver, {
19-
RawPerformanceEntryTypeValues,
20-
} from './NativePerformanceObserver';
14+
import NativePerformanceObserver from './NativePerformanceObserver';
2115
import {PerformanceEntry} from './PerformanceEntry';
22-
import {PerformanceEventTiming} from './PerformanceEventTiming';
23-
24-
function rawToPerformanceEntryType(
25-
type: RawPerformanceEntryType,
26-
): PerformanceEntryType {
27-
switch (type) {
28-
case RawPerformanceEntryTypeValues.MARK:
29-
return 'mark';
30-
case RawPerformanceEntryTypeValues.MEASURE:
31-
return 'measure';
32-
case RawPerformanceEntryTypeValues.EVENT:
33-
return 'event';
34-
default:
35-
throw new TypeError(
36-
`rawToPerformanceEntryType: unexpected performance entry type received: ${type}`,
37-
);
38-
}
39-
}
40-
41-
function performanceEntryTypeToRaw(
42-
type: PerformanceEntryType,
43-
): RawPerformanceEntryType {
44-
switch (type) {
45-
case 'mark':
46-
return RawPerformanceEntryTypeValues.MARK;
47-
case 'measure':
48-
return RawPerformanceEntryTypeValues.MEASURE;
49-
case 'event':
50-
return RawPerformanceEntryTypeValues.EVENT;
51-
default:
52-
// Verify exhaustive check with Flow
53-
(type: empty);
54-
throw new TypeError(
55-
`performanceEntryTypeToRaw: unexpected performance entry type received: ${type}`,
56-
);
57-
}
58-
}
59-
60-
function rawToPerformanceEntry(entry: RawPerformanceEntry): PerformanceEntry {
61-
if (entry.entryType === RawPerformanceEntryTypeValues.EVENT) {
62-
return new PerformanceEventTiming({
63-
name: entry.name,
64-
startTime: entry.startTime,
65-
duration: entry.duration,
66-
processingStart: entry.processingStart,
67-
processingEnd: entry.processingEnd,
68-
interactionId: entry.interactionId,
69-
});
70-
} else {
71-
return new PerformanceEntry({
72-
name: entry.name,
73-
entryType: rawToPerformanceEntryType(entry.entryType),
74-
startTime: entry.startTime,
75-
duration: entry.duration,
76-
});
77-
}
78-
}
16+
import {
17+
performanceEntryTypeToRaw,
18+
rawToPerformanceEntry,
19+
} from './RawPerformanceEntry';
7920

8021
export type PerformanceEntryList = $ReadOnlyArray<PerformanceEntry>;
8122

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
* @flow strict
9+
*/
10+
11+
import type {
12+
RawPerformanceEntry,
13+
RawPerformanceEntryType,
14+
} from './NativePerformanceObserver';
15+
import type {PerformanceEntryType} from './PerformanceEntry';
16+
17+
import {PerformanceEntry} from './PerformanceEntry';
18+
import {PerformanceEventTiming} from './PerformanceEventTiming';
19+
20+
export const RawPerformanceEntryTypeValues = {
21+
UNDEFINED: 0,
22+
MARK: 1,
23+
MEASURE: 2,
24+
EVENT: 3,
25+
};
26+
27+
export function rawToPerformanceEntry(
28+
entry: RawPerformanceEntry,
29+
): PerformanceEntry {
30+
if (entry.entryType === RawPerformanceEntryTypeValues.EVENT) {
31+
return new PerformanceEventTiming({
32+
name: entry.name,
33+
startTime: entry.startTime,
34+
duration: entry.duration,
35+
processingStart: entry.processingStart,
36+
processingEnd: entry.processingEnd,
37+
interactionId: entry.interactionId,
38+
});
39+
} else {
40+
return new PerformanceEntry({
41+
name: entry.name,
42+
entryType: rawToPerformanceEntryType(entry.entryType),
43+
startTime: entry.startTime,
44+
duration: entry.duration,
45+
});
46+
}
47+
}
48+
49+
export function rawToPerformanceEntryType(
50+
type: RawPerformanceEntryType,
51+
): PerformanceEntryType {
52+
switch (type) {
53+
case RawPerformanceEntryTypeValues.MARK:
54+
return 'mark';
55+
case RawPerformanceEntryTypeValues.MEASURE:
56+
return 'measure';
57+
case RawPerformanceEntryTypeValues.EVENT:
58+
return 'event';
59+
case RawPerformanceEntryTypeValues.UNDEFINED:
60+
throw new TypeError(
61+
"rawToPerformanceEntryType: UNDEFINED can't be cast to PerformanceEntryType",
62+
);
63+
default:
64+
throw new TypeError(
65+
`rawToPerformanceEntryType: unexpected performance entry type received: ${type}`,
66+
);
67+
}
68+
}
69+
70+
export function performanceEntryTypeToRaw(
71+
type: PerformanceEntryType,
72+
): RawPerformanceEntryType {
73+
switch (type) {
74+
case 'mark':
75+
return RawPerformanceEntryTypeValues.MARK;
76+
case 'measure':
77+
return RawPerformanceEntryTypeValues.MEASURE;
78+
case 'event':
79+
return RawPerformanceEntryTypeValues.EVENT;
80+
default:
81+
// Verify exhaustive check with Flow
82+
(type: empty);
83+
throw new TypeError(
84+
`performanceEntryTypeToRaw: unexpected performance entry type received: ${type}`,
85+
);
86+
}
87+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict
8+
* @format
9+
*/
10+
11+
import type {
12+
GetPendingEntriesResult,
13+
RawPerformanceEntry,
14+
RawPerformanceEntryType,
15+
Spec as NativePerformanceObserver,
16+
} from '../NativePerformanceObserver';
17+
18+
const reportingType: Set<RawPerformanceEntryType> = new Set();
19+
let entries: Array<RawPerformanceEntry> = [];
20+
let onPerformanceEntryCallback: ?() => void;
21+
22+
const NativePerformanceObserverMock: NativePerformanceObserver = {
23+
startReporting: (entryType: RawPerformanceEntryType) => {
24+
reportingType.add(entryType);
25+
},
26+
27+
stopReporting: (entryType: RawPerformanceEntryType) => {
28+
reportingType.delete(entryType);
29+
},
30+
31+
popPendingEntries: (): GetPendingEntriesResult => {
32+
const res = entries;
33+
entries = [];
34+
return {
35+
droppedEntriesCount: 0,
36+
entries: res,
37+
};
38+
},
39+
40+
setOnPerformanceEntryCallback: (callback?: () => void) => {
41+
onPerformanceEntryCallback = callback;
42+
},
43+
44+
logRawEntry: (entry: RawPerformanceEntry) => {
45+
if (reportingType.has(entry.entryType)) {
46+
entries.push(entry);
47+
// $FlowFixMe[incompatible-call]
48+
global.queueMicrotask(() => {
49+
// We want to emulate the way it's done in native (i.e. async/batched)
50+
onPerformanceEntryCallback?.();
51+
});
52+
}
53+
},
54+
};
55+
56+
export default NativePerformanceObserverMock;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
* @oncall react_native
9+
*/
10+
11+
import NativePerformanceObserverMock from '../__mocks__/NativePerformanceObserver';
12+
import {RawPerformanceEntryTypeValues} from '../RawPerformanceEntry';
13+
14+
describe('NativePerformanceObserver', () => {
15+
it('correctly starts and stops listening to entries in a nominal scenario', async () => {
16+
NativePerformanceObserverMock.startReporting(
17+
RawPerformanceEntryTypeValues.MARK,
18+
);
19+
20+
NativePerformanceObserverMock.logRawEntry({
21+
name: 'mark1',
22+
entryType: RawPerformanceEntryTypeValues.MARK,
23+
startTime: 0,
24+
duration: 10,
25+
});
26+
27+
NativePerformanceObserverMock.logRawEntry({
28+
name: 'mark2',
29+
entryType: RawPerformanceEntryTypeValues.MARK,
30+
startTime: 0,
31+
duration: 20,
32+
});
33+
34+
NativePerformanceObserverMock.logRawEntry({
35+
name: 'event1',
36+
entryType: RawPerformanceEntryTypeValues.EVENT,
37+
startTime: 0,
38+
duration: 20,
39+
});
40+
41+
const entriesResult = NativePerformanceObserverMock.popPendingEntries();
42+
expect(entriesResult).not.toBe(undefined);
43+
const entries = entriesResult.entries;
44+
45+
expect(entries.length).toBe(2);
46+
expect(entries[0].name).toBe('mark1');
47+
expect(entries[1].name).toBe('mark2');
48+
49+
const entriesResult1 = NativePerformanceObserverMock.popPendingEntries();
50+
expect(entriesResult1).not.toBe(undefined);
51+
const entries1 = entriesResult1.entries;
52+
expect(entries1.length).toBe(0);
53+
54+
NativePerformanceObserverMock.stopReporting('mark');
55+
});
56+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
* @oncall react_native
9+
*/
10+
11+
import {RawPerformanceEntryTypeValues} from '../RawPerformanceEntry';
12+
13+
// NOTE: Jest mocks of transitive dependencies don't appear to work with
14+
// ES6 module imports, therefore forced to use commonjs style imports here.
15+
const NativePerformanceObserver = require('../NativePerformanceObserver');
16+
const PerformanceObserver = require('../PerformanceObserver').default;
17+
18+
jest.mock(
19+
'../NativePerformanceObserver',
20+
() => require('../__mocks__/NativePerformanceObserver').default,
21+
);
22+
23+
describe('PerformanceObserver', () => {
24+
it('can be mocked by a reference NativePerformanceObserver implementation', async () => {
25+
expect(NativePerformanceObserver).not.toBe(undefined);
26+
27+
let totalEntries = 0;
28+
const observer = new PerformanceObserver((list, _observer) => {
29+
const entries = list.getEntries();
30+
expect(entries).toHaveLength(1);
31+
const entry = entries[0];
32+
expect(entry.name).toBe('mark1');
33+
expect(entry.entryType).toBe('mark');
34+
totalEntries += entries.length;
35+
});
36+
expect(() => observer.observe({entryTypes: ['mark']})).not.toThrow();
37+
38+
NativePerformanceObserver.logRawEntry({
39+
name: 'mark1',
40+
entryType: RawPerformanceEntryTypeValues.MARK,
41+
startTime: 0,
42+
duration: 0,
43+
});
44+
45+
await jest.runAllTicks();
46+
expect(totalEntries).toBe(1);
47+
observer.disconnect();
48+
});
49+
});

0 commit comments

Comments
 (0)