Skip to content

Commit

Permalink
Reference implementation (mock) for NativePerformanceObserver (#36116)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
rshest authored and facebook-github-bot committed Feb 15, 2023
1 parent 96fb708 commit f76d4de
Show file tree
Hide file tree
Showing 8 changed files with 261 additions and 71 deletions.
6 changes: 6 additions & 0 deletions Libraries/WebPerformance/NativePerformanceObserver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,10 @@ void NativePerformanceObserver::setOnPerformanceEntryCallback(
PerformanceEntryReporter::getInstance().setReportingCallback(callback);
}

void NativePerformanceObserver::logRawEntry(
jsi::Runtime &rt,
RawPerformanceEntry entry) {
PerformanceEntryReporter::getInstance().logEntry(entry);
}

} // namespace facebook::react
2 changes: 2 additions & 0 deletions Libraries/WebPerformance/NativePerformanceObserver.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ class NativePerformanceObserver
jsi::Runtime &rt,
std::optional<AsyncCallback<>> callback);

void logRawEntry(jsi::Runtime &rt, RawPerformanceEntry entry);

private:
};

Expand Down
7 changes: 0 additions & 7 deletions Libraries/WebPerformance/NativePerformanceObserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,6 @@ import type {TurboModule} from '../TurboModule/RCTExport';

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

export const RawPerformanceEntryTypeValues = {
UNDEFINED: 0,
MARK: 1,
MEASURE: 2,
EVENT: 3,
};

export type RawPerformanceEntryType = number;

export type RawPerformanceEntry = {|
Expand Down
69 changes: 5 additions & 64 deletions Libraries/WebPerformance/PerformanceObserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,74 +8,15 @@
* @flow strict
*/

import type {
RawPerformanceEntry,
RawPerformanceEntryType,
} from './NativePerformanceObserver';
import type {PerformanceEntryType} from './PerformanceEntry';

import warnOnce from '../Utilities/warnOnce';
import NativePerformanceObserver, {
RawPerformanceEntryTypeValues,
} from './NativePerformanceObserver';
import NativePerformanceObserver from './NativePerformanceObserver';
import {PerformanceEntry} from './PerformanceEntry';
import {PerformanceEventTiming} from './PerformanceEventTiming';

function rawToPerformanceEntryType(
type: RawPerformanceEntryType,
): PerformanceEntryType {
switch (type) {
case RawPerformanceEntryTypeValues.MARK:
return 'mark';
case RawPerformanceEntryTypeValues.MEASURE:
return 'measure';
case RawPerformanceEntryTypeValues.EVENT:
return 'event';
default:
throw new TypeError(
`rawToPerformanceEntryType: unexpected performance entry type received: ${type}`,
);
}
}

function performanceEntryTypeToRaw(
type: PerformanceEntryType,
): RawPerformanceEntryType {
switch (type) {
case 'mark':
return RawPerformanceEntryTypeValues.MARK;
case 'measure':
return RawPerformanceEntryTypeValues.MEASURE;
case 'event':
return RawPerformanceEntryTypeValues.EVENT;
default:
// Verify exhaustive check with Flow
(type: empty);
throw new TypeError(
`performanceEntryTypeToRaw: unexpected performance entry type received: ${type}`,
);
}
}

function rawToPerformanceEntry(entry: RawPerformanceEntry): PerformanceEntry {
if (entry.entryType === RawPerformanceEntryTypeValues.EVENT) {
return new PerformanceEventTiming({
name: entry.name,
startTime: entry.startTime,
duration: entry.duration,
processingStart: entry.processingStart,
processingEnd: entry.processingEnd,
interactionId: entry.interactionId,
});
} else {
return new PerformanceEntry({
name: entry.name,
entryType: rawToPerformanceEntryType(entry.entryType),
startTime: entry.startTime,
duration: entry.duration,
});
}
}
import {
performanceEntryTypeToRaw,
rawToPerformanceEntry,
} from './RawPerformanceEntry';

export type PerformanceEntryList = $ReadOnlyArray<PerformanceEntry>;

Expand Down
87 changes: 87 additions & 0 deletions Libraries/WebPerformance/RawPerformanceEntry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict
*/

import type {
RawPerformanceEntry,
RawPerformanceEntryType,
} from './NativePerformanceObserver';
import type {PerformanceEntryType} from './PerformanceEntry';

import {PerformanceEntry} from './PerformanceEntry';
import {PerformanceEventTiming} from './PerformanceEventTiming';

export const RawPerformanceEntryTypeValues = {
UNDEFINED: 0,
MARK: 1,
MEASURE: 2,
EVENT: 3,
};

export function rawToPerformanceEntry(
entry: RawPerformanceEntry,
): PerformanceEntry {
if (entry.entryType === RawPerformanceEntryTypeValues.EVENT) {
return new PerformanceEventTiming({
name: entry.name,
startTime: entry.startTime,
duration: entry.duration,
processingStart: entry.processingStart,
processingEnd: entry.processingEnd,
interactionId: entry.interactionId,
});
} else {
return new PerformanceEntry({
name: entry.name,
entryType: rawToPerformanceEntryType(entry.entryType),
startTime: entry.startTime,
duration: entry.duration,
});
}
}

export function rawToPerformanceEntryType(
type: RawPerformanceEntryType,
): PerformanceEntryType {
switch (type) {
case RawPerformanceEntryTypeValues.MARK:
return 'mark';
case RawPerformanceEntryTypeValues.MEASURE:
return 'measure';
case RawPerformanceEntryTypeValues.EVENT:
return 'event';
case RawPerformanceEntryTypeValues.UNDEFINED:
throw new TypeError(
"rawToPerformanceEntryType: UNDEFINED can't be cast to PerformanceEntryType",
);
default:
throw new TypeError(
`rawToPerformanceEntryType: unexpected performance entry type received: ${type}`,
);
}
}

export function performanceEntryTypeToRaw(
type: PerformanceEntryType,
): RawPerformanceEntryType {
switch (type) {
case 'mark':
return RawPerformanceEntryTypeValues.MARK;
case 'measure':
return RawPerformanceEntryTypeValues.MEASURE;
case 'event':
return RawPerformanceEntryTypeValues.EVENT;
default:
// Verify exhaustive check with Flow
(type: empty);
throw new TypeError(
`performanceEntryTypeToRaw: unexpected performance entry type received: ${type}`,
);
}
}
56 changes: 56 additions & 0 deletions Libraries/WebPerformance/__mocks__/NativePerformanceObserver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/

import type {
GetPendingEntriesResult,
RawPerformanceEntry,
RawPerformanceEntryType,
Spec as NativePerformanceObserver,
} from '../NativePerformanceObserver';

const reportingType: Set<RawPerformanceEntryType> = new Set();
let entries: Array<RawPerformanceEntry> = [];
let onPerformanceEntryCallback: ?() => void;

const NativePerformanceObserverMock: NativePerformanceObserver = {
startReporting: (entryType: RawPerformanceEntryType) => {
reportingType.add(entryType);
},

stopReporting: (entryType: RawPerformanceEntryType) => {
reportingType.delete(entryType);
},

popPendingEntries: (): GetPendingEntriesResult => {
const res = entries;
entries = [];
return {
droppedEntriesCount: 0,
entries: res,
};
},

setOnPerformanceEntryCallback: (callback?: () => void) => {
onPerformanceEntryCallback = callback;
},

logRawEntry: (entry: RawPerformanceEntry) => {
if (reportingType.has(entry.entryType)) {
entries.push(entry);
// $FlowFixMe[incompatible-call]
global.queueMicrotask(() => {
// We want to emulate the way it's done in native (i.e. async/batched)
onPerformanceEntryCallback?.();
});
}
},
};

export default NativePerformanceObserverMock;
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @oncall react_native
*/

import NativePerformanceObserverMock from '../__mocks__/NativePerformanceObserver';
import {RawPerformanceEntryTypeValues} from '../RawPerformanceEntry';

describe('NativePerformanceObserver', () => {
it('correctly starts and stops listening to entries in a nominal scenario', async () => {
NativePerformanceObserverMock.startReporting(
RawPerformanceEntryTypeValues.MARK,
);

NativePerformanceObserverMock.logRawEntry({
name: 'mark1',
entryType: RawPerformanceEntryTypeValues.MARK,
startTime: 0,
duration: 10,
});

NativePerformanceObserverMock.logRawEntry({
name: 'mark2',
entryType: RawPerformanceEntryTypeValues.MARK,
startTime: 0,
duration: 20,
});

NativePerformanceObserverMock.logRawEntry({
name: 'event1',
entryType: RawPerformanceEntryTypeValues.EVENT,
startTime: 0,
duration: 20,
});

const entriesResult = NativePerformanceObserverMock.popPendingEntries();
expect(entriesResult).not.toBe(undefined);
const entries = entriesResult.entries;

expect(entries.length).toBe(2);
expect(entries[0].name).toBe('mark1');
expect(entries[1].name).toBe('mark2');

const entriesResult1 = NativePerformanceObserverMock.popPendingEntries();
expect(entriesResult1).not.toBe(undefined);
const entries1 = entriesResult1.entries;
expect(entries1.length).toBe(0);

NativePerformanceObserverMock.stopReporting('mark');
});
});
49 changes: 49 additions & 0 deletions Libraries/WebPerformance/__tests__/PerformanceObserver-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @oncall react_native
*/

import {RawPerformanceEntryTypeValues} from '../RawPerformanceEntry';

// NOTE: Jest mocks of transitive dependencies don't appear to work with
// ES6 module imports, therefore forced to use commonjs style imports here.
const NativePerformanceObserver = require('../NativePerformanceObserver');
const PerformanceObserver = require('../PerformanceObserver').default;

jest.mock(
'../NativePerformanceObserver',
() => require('../__mocks__/NativePerformanceObserver').default,
);

describe('PerformanceObserver', () => {
it('can be mocked by a reference NativePerformanceObserver implementation', async () => {
expect(NativePerformanceObserver).not.toBe(undefined);

let totalEntries = 0;
const observer = new PerformanceObserver((list, _observer) => {
const entries = list.getEntries();
expect(entries).toHaveLength(1);
const entry = entries[0];
expect(entry.name).toBe('mark1');
expect(entry.entryType).toBe('mark');
totalEntries += entries.length;
});
expect(() => observer.observe({entryTypes: ['mark']})).not.toThrow();

NativePerformanceObserver.logRawEntry({
name: 'mark1',
entryType: RawPerformanceEntryTypeValues.MARK,
startTime: 0,
duration: 0,
});

await jest.runAllTicks();
expect(totalEntries).toBe(1);
observer.disconnect();
});
});

0 comments on commit f76d4de

Please sign in to comment.