Skip to content

Commit fe459fc

Browse files
committed
refactor(perf): return sync parity for metric controls
1 parent ee884f9 commit fe459fc

31 files changed

Lines changed: 444 additions & 317 deletions

File tree

.github/scripts/compare-types/configs/perf-config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ const config: PackageConfig = {
6262
{
6363
name: 'PerformanceTrace',
6464
reason:
65-
'React Native uses async `start`/`stop` (`Promise<null>`) because work crosses the native bridge. ' +
66-
'The web SDK uses synchronous `start`/`stop` and exposes `record()`. RN Firebase exposes ' +
67-
'`getMetrics` and `removeMetric` for native-backed custom metrics instead of the web shape.',
65+
'React Native uses synchronous `start`/`stop` (in-memory native calls via TurboModules), matching ' +
66+
'the firebase-js-sdk web `start`/`stop`. The web SDK additionally exposes `record()`, while RN Firebase ' +
67+
'exposes `getMetrics` and `removeMetric` for native-backed custom metrics instead.',
6868
},
6969
],
7070
};

docs/migrating-to-v26.mdx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ When migrating from firebase-js-sdk web examples, watch for these high-impact ru
9090
| **Cloud Storage** | `uploadBytes` / `uploadString` from JS blobs | `putFile` and `writeToFile` are **native-only** file-path APIs with no firebase-js-sdk equivalent. Retry-time setters are modular functions on RN, not instance properties. |
9191
| **Cloud Messaging** | Web push / service-worker surface | FCM token lifecycle, permissions, background handlers, and **APNs token APIs are iOS only**. Event delivery still uses the legacy native event proxy during the TurboModule migration; behavior matches pre-v26 releases but will change when Codegen events land in a future release. |
9292
| **Remote Config** | `reset()` clears server and local state | `reset()` is **Android only** — iOS does not clear activated, fetched, or default Remote Config values. `setDefaultsFromResource` loads from native resource files (`.plist` / XML). |
93-
| **Performance** | `initializePerformance` returns synchronously; trace `start`/`stop` are sync | `initializePerformance` is **synchronous** on RN (applies settings to the native instance). Trace `start`/`stop` are **async** through the native bridge. |
93+
| **Performance** | `initializePerformance` returns synchronously; trace `start`/`stop` are sync | `initializePerformance` is **synchronous** on RN (applies settings to the native instance). Trace, HTTP metric, and screen trace `start`/`stop` are **synchronous** through TurboModules, matching firebase-js-sdk. |
9494
| **Firestore** | IndexedDB persistence, `memoryLocalCache`, `persistentLocalCache` | Local cache factories and IndexedDB APIs are **web only**. Persistence is controlled by the native Firestore SDK. `initializeFirestore` is **async** on RN. |
9595
| **App Check** | reCAPTCHA version 3 / Enterprise web providers | Use `ReactNativeFirebaseAppCheckProvider` for Device Check, App Attest, Play Integrity, etc. Web reCAPTCHA provider classes have **no RN equivalent**. |
9696
| **RN-native-only modules** | No web SDK | `crashlytics`, `in-app-messaging`, `app-distribution`, and `ml` are **React Native only** — no firebase-js-sdk modular surface. |
@@ -402,6 +402,11 @@ The namespaced API for `@react-native-firebase/perf` has been **removed**. This
402402
| `perf().newTrace()` | `trace(performance, name)` |
403403
| `perf().newHttpMetric()` | `httpMetric(performance, url, httpMethod)` |
404404

405+
## Signature changes
406+
407+
- `Trace`, `HttpMetric`, and `ScreenTrace` `start()` / `stop()` now return `void` synchronously (previously `Promise<null>`), matching the firebase-js-sdk web `PerformanceTrace`. The underlying native calls are in-memory, so no `await` is required — remove any `await` on these calls.
408+
- `startScreenTrace(performance, screenName)` and the internal `startTrace(name)` helper now return the trace instance synchronously instead of a `Promise`.
409+
405410
# App Check
406411

407412
The namespaced API for `@react-native-firebase/app-check` has been **removed**. This package is **modular-only** — use `getAppCheck()` / `initializeAppCheck()` and root-level helper functions.

docs/perf/axios-integration.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ axios.interceptors.request.use(async function (config) {
3232
// add any extra metric attributes, if required
3333
// httpMetricInstance.putAttribute('userId', '12345678');
3434

35-
await httpMetricInstance.start();
35+
httpMetricInstance.start();
3636
} finally {
3737
return config;
3838
}
@@ -62,7 +62,7 @@ axios.interceptors.response.use(
6262

6363
httpMetric.setHttpResponseCode(response.status);
6464
httpMetric.setResponseContentType(response.headers['content-type']);
65-
await httpMetric.stop();
65+
httpMetric.stop();
6666
} finally {
6767
return response;
6868
}
@@ -78,7 +78,7 @@ axios.interceptors.response.use(
7878

7979
httpMetric.setHttpResponseCode(error.response.status);
8080
httpMetric.setResponseContentType(error.response.headers['content-type']);
81-
await httpMetric.stop();
81+
httpMetric.stop();
8282
} finally {
8383
// Ensure failed requests throw after interception
8484
return Promise.reject(error);

docs/perf/ky-integration.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const api = ky.create({
3838
// options.metric.putAttribute('userId', '12345678');
3939
// options.metric.setRequestPayloadSize(1234)
4040

41-
await options.metric.start();
41+
options.metric.start();
4242
},
4343
],
4444
},
@@ -73,7 +73,7 @@ const api = ky.create({
7373
metric.setHttpResponseCode(status);
7474
metric.setResponseContentType(getHeader('Content-Type', headers));
7575
metric.setResponsePayloadSize(getContentLength(headers));
76-
await metric.stop();
76+
metric.stop();
7777
},
7878
],
7979
},
@@ -113,7 +113,7 @@ const api = ky.create({
113113
// options.metric.putAttribute('userId', '12345678');
114114
// options.metric.setRequestPayloadSize(1234)
115115

116-
await options.metric.start();
116+
options.metric.start();
117117
},
118118
],
119119

@@ -124,7 +124,7 @@ const api = ky.create({
124124
metric.setHttpResponseCode(status);
125125
metric.setResponseContentType(getHeader('Content-Type', headers));
126126
metric.setResponsePayloadSize(getContentLength(headers));
127-
await metric.stop();
127+
metric.stop();
128128
},
129129
],
130130
},

docs/perf/usage/index.mdx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ apply plugin: 'com.google.firebase.firebase-perf'
5555
| **Platforms** | Android, iOS (native Firebase SDK) |
5656
| **New Architecture** | **Required** from v26. See [Migrating to v26](/migrating-to-v26#new-architecture-requirement). |
5757

58-
**Platform notes:** `initializePerformance` returns synchronously on RN. Custom trace `start`/`stop` are async through the native bridge (unlike the web SDK).
58+
**Platform notes:** `initializePerformance` returns synchronously on RN. Trace, HTTP metric, and screen trace `start`/`stop` are synchronous through TurboModules, matching firebase-js-sdk.
5959

6060
# What does it do
6161

@@ -81,14 +81,14 @@ Below is how you would measure the amount of time it would take to complete a sp
8181
```jsx
8282
import { getPerformance, trace } from '@react-native-firebase/perf';
8383

84-
async function customTrace() {
84+
function customTrace() {
8585
const perf = getPerformance();
8686
const t = trace(perf, 'custom_trace');
8787

88-
await t.start();
88+
t.start();
8989
t.putAttribute('user', 'abcd');
9090
t.putMetric('credits', 30);
91-
await t.stop();
91+
t.stop();
9292
}
9393
```
9494

@@ -99,10 +99,10 @@ Record a custom screen rendering trace (slow frames / frozen frames)
9999
```jsx
100100
import { getPerformance, startScreenTrace } from '@react-native-firebase/perf';
101101

102-
async function screenTrace() {
102+
function screenTrace() {
103103
try {
104-
const screenTrace = await startScreenTrace(getPerformance(), 'FooScreen');
105-
await screenTrace.stop();
104+
const screenTrace = startScreenTrace(getPerformance(), 'FooScreen');
105+
screenTrace.stop();
106106
} catch (e) {
107107
// rejects if iOS or (Android == 8 || Android == 8.1)
108108
// or if hardware acceleration is off
@@ -122,14 +122,14 @@ async function getRequest(url) {
122122

123123
metric.putAttribute('user', 'abcd');
124124

125-
await metric.start();
125+
metric.start();
126126

127127
const response = await fetch(url);
128128
metric.setHttpResponseCode(response.status);
129129
metric.setResponseContentType(response.headers.get('Content-Type'));
130130
metric.setResponsePayloadSize(response.headers.get('Content-Length'));
131131

132-
await metric.stop();
132+
metric.stop();
133133

134134
return response.json();
135135
}

jest.setup.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -435,12 +435,12 @@ jest.doMock('react-native', () => {
435435
isInstrumentationEnabled: true,
436436
instrumentationEnabled: jest.fn(() => Promise.resolve()),
437437
setPerformanceCollectionEnabled: jest.fn(() => Promise.resolve()),
438-
startScreenTrace: jest.fn(() => Promise.resolve()),
439-
stopScreenTrace: jest.fn(() => Promise.resolve()),
440-
startTrace: jest.fn(() => Promise.resolve()),
441-
stopTrace: jest.fn(() => Promise.resolve()),
442-
startHttpMetric: jest.fn(() => Promise.resolve()),
443-
stopHttpMetric: jest.fn(() => Promise.resolve()),
438+
startScreenTrace: jest.fn(),
439+
stopScreenTrace: jest.fn(),
440+
startTrace: jest.fn(),
441+
stopTrace: jest.fn(),
442+
startHttpMetric: jest.fn(),
443+
stopHttpMetric: jest.fn(),
444444
},
445445
NativeRNFBTurboPnv: {
446446
enableTestSession: jest.fn(() => Promise.resolve()),

okf-bundle/new-architecture/migration-work-queue.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -332,9 +332,9 @@ Skip steps 1–2 when spec shape is known (most Tier D packages).
332332

333333
**Label:** `phase-s-sync-conversion` (2026-07-03)
334334

335-
**Next item:** Phase **S** perf metric controls sync conversion (`PS-perf-metrics`)
335+
**Next item:** Phase **S** database sync controls (`PS-database-online` — after perf commit)
336336

337-
**Current gates:** PS-auth-parsers all gates **closed** — committing `refactor(auth): return sync parity for auth parsers`.
337+
**Current gates:** PS-perf-metrics all gates **closed** — committing `refactor(perf): return sync parity for metric controls`.
338338

339339
**Host rule:** one `:test-cover` at a time — never parallel subagents with e2e.
340340

@@ -373,7 +373,8 @@ Skip steps 1–2 when spec shape is known (most Tier D packages).
373373
| Phase R — remove `NativeModules` fallback | PR-fallback | **closed** | **closed** | **closed** | done | `full` + RNFBDebug | `refactor: cleanup legacy arch native module fallback` | Proof 683/823/849 (`/tmp/rnfb-e2e-phaseR-proof-*.log`). Review 2026-07-03: no critical/serious. Android cold boot committed separately. |
374374
| Phase S gap-analysis | PS-gap | n/a | n/a | n/a | done | `none` | none | Completed 2026-07-03. First implementation slice: `app/registerVersion`; follow-ups: auth parser/TOTP URL, then perf metric controls. |
375375
| Phase S `app/registerVersion` sync parity | PS-app-registerVersion | **closed** | **closed** | **closed** | done | `area-focused` | `refactor(app): return sync parity for registerVersion` | Implemented 2026-07-03: `registerVersion(): void`, sync throw Jest assertion, removed stale `configs/app.ts` async-vs-sync entry. Green: `lerna:prepare`, `tsc:compile`, `tsc:compile:consumer`, focused app Jest 10/10, `reference:api`, `compare:types`, `lint:js`. Independent review: no findings; no e2e needed for type-only scope. |
376-
| Phase S auth parser/TOTP sync parity | PS-auth-parsers | **closed** | **closed** | **closed** | done | `area-focused` | `refactor(auth): return sync parity for auth parsers` | Sync `isSignInWithEmailLink` + `TotpSecret.generateQrCodeUrl`; native sync error shape; jest.setup; tests + e2e sync calls; compare-types + v26 doc. Validation: Jest 91/91, compare:types, lint, reference:api, codegen:verify exit 0. Android area e2e 159/15/0 (`/tmp/rnfb-e2e-android-auth-phaseS-final.log`). |
376+
| Phase S auth parser/TOTP sync parity | PS-auth-parsers | **closed** | **closed** | **closed** | done | `area-focused` | `refactor(auth): return sync parity for auth parsers` | Committed 2026-07-03. Sync `isSignInWithEmailLink` + `TotpSecret.generateQrCodeUrl`; native sync error shape; jest.setup; tests + e2e sync calls; compare-types + v26 doc. Validation: Jest 91/91, compare:types, lint, reference:api, codegen:verify exit 0. Android area e2e 159/15/0 (`/tmp/rnfb-e2e-android-auth-phaseS-final.log`). |
377+
| Phase S perf metric controls sync parity | PS-perf-metrics | **closed** | **closed** | **closed** | done | `area-focused` | `refactor(perf): return sync parity for metric controls` | Sync trace/http/screen start/stop; Android *Sync helpers; iOS sync shells. Jest 10/10; compare:types, lint, reference:api green. Review e2e iOS/Android 60/2/0 (`/tmp/rnfb-e2e-*-perf-review.log`). macOS N/A (module skip). Remediation: perf usage docs await removed. |
377378

378379
---
379380

packages/perf/__tests__/nativeModuleContract.test.ts

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,74 @@ const SPEC_METHODS = [
1212
'stopHttpMetric',
1313
] as const;
1414

15+
const SYNC_METHODS = [
16+
'startTrace',
17+
'stopTrace',
18+
'startScreenTrace',
19+
'stopScreenTrace',
20+
'startHttpMetric',
21+
'stopHttpMetric',
22+
] as const;
23+
1524
describe('TurboModule wrapper contract (NewArch-AD-17.1)', function () {
16-
it('exposes every spec method callable through the real wrapper', function () {
17-
assertTurboContract({
18-
namespace: 'perf',
19-
nativeModuleName: 'NativeRNFBTurboPerf',
20-
nativeEvents: false,
21-
hasMultiAppSupport: false,
22-
hasCustomUrlOrRegionSupport: false,
23-
turboModule: true,
24-
specMethods: SPEC_METHODS,
25-
createMock: () => jest.fn(() => Promise.resolve(null)),
26-
constants: {
27-
isPerformanceCollectionEnabled: true,
28-
isInstrumentationEnabled: true,
25+
it('exposes every spec method callable through the real wrapper, with sync start/stop', function () {
26+
assertTurboContract(
27+
{
28+
namespace: 'perf',
29+
nativeModuleName: 'NativeRNFBTurboPerf',
30+
nativeEvents: false,
31+
hasMultiAppSupport: false,
32+
hasCustomUrlOrRegionSupport: false,
33+
turboModule: true,
34+
specMethods: SPEC_METHODS,
35+
createMock: method =>
36+
jest.fn(() => {
37+
if (SYNC_METHODS.includes(method as (typeof SYNC_METHODS)[number])) {
38+
return undefined;
39+
}
40+
return Promise.resolve(null);
41+
}),
42+
constants: {
43+
isPerformanceCollectionEnabled: true,
44+
isInstrumentationEnabled: true,
45+
},
46+
assertExtra: wrapped => {
47+
expect(wrapped.isPerformanceCollectionEnabled).toBe(true);
48+
expect(wrapped.isInstrumentationEnabled).toBe(true);
49+
},
2950
},
30-
assertExtra: wrapped => {
31-
expect(wrapped.isPerformanceCollectionEnabled).toBe(true);
32-
expect(wrapped.isInstrumentationEnabled).toBe(true);
51+
{
52+
startTrace: wrapped => {
53+
const result = wrapped.startTrace(0, 'trace');
54+
expect(result).toBeUndefined();
55+
expect(result).not.toBeInstanceOf(Promise);
56+
},
57+
stopTrace: wrapped => {
58+
const result = wrapped.stopTrace(0, { metrics: {}, attributes: {} });
59+
expect(result).toBeUndefined();
60+
expect(result).not.toBeInstanceOf(Promise);
61+
},
62+
startScreenTrace: wrapped => {
63+
const result = wrapped.startScreenTrace(0, 'screen');
64+
expect(result).toBeUndefined();
65+
expect(result).not.toBeInstanceOf(Promise);
66+
},
67+
stopScreenTrace: wrapped => {
68+
const result = wrapped.stopScreenTrace(0);
69+
expect(result).toBeUndefined();
70+
expect(result).not.toBeInstanceOf(Promise);
71+
},
72+
startHttpMetric: wrapped => {
73+
const result = wrapped.startHttpMetric(0, 'https://example.com', 'GET');
74+
expect(result).toBeUndefined();
75+
expect(result).not.toBeInstanceOf(Promise);
76+
},
77+
stopHttpMetric: wrapped => {
78+
const result = wrapped.stopHttpMetric(0, { attributes: {} });
79+
expect(result).toBeUndefined();
80+
expect(result).not.toBeInstanceOf(Promise);
81+
},
3382
},
34-
});
83+
);
3584
});
3685
});

packages/perf/__tests__/perf.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,47 @@ describe('Performance Monitoring', function () {
3535
expect(startScreenTrace).toBeDefined();
3636
});
3737
});
38+
39+
describe('synchronous start/stop parity', function () {
40+
it('`Trace.start()` and `Trace.stop()` return synchronously (not Promises)', function () {
41+
const perf = getPerformance();
42+
const traceInstance = trace(perf, 'sync-trace');
43+
44+
const startResult = traceInstance.start();
45+
expect(startResult).toBeUndefined();
46+
expect(startResult).not.toBeInstanceOf(Promise);
47+
// @ts-expect-error internal flag for test assertion only
48+
expect(traceInstance._started).toBe(true);
49+
50+
const stopResult = traceInstance.stop();
51+
expect(stopResult).toBeUndefined();
52+
expect(stopResult).not.toBeInstanceOf(Promise);
53+
// @ts-expect-error internal flag for test assertion only
54+
expect(traceInstance._stopped).toBe(true);
55+
});
56+
57+
it('`HttpMetric.start()` and `HttpMetric.stop()` return synchronously (not Promises)', function () {
58+
const perf = getPerformance();
59+
const metric = httpMetric(perf, 'https://example.com', 'GET');
60+
metric.setHttpResponseCode(200);
61+
62+
const startResult = metric.start();
63+
expect(startResult).toBeUndefined();
64+
expect(startResult).not.toBeInstanceOf(Promise);
65+
66+
const stopResult = metric.stop();
67+
expect(stopResult).toBeUndefined();
68+
expect(stopResult).not.toBeInstanceOf(Promise);
69+
});
70+
71+
it('`startTrace()` returns a started Trace synchronously (not a Promise)', function () {
72+
const perf = getPerformance();
73+
const traceInstance = (perf as unknown as { startTrace(name: string): unknown }).startTrace(
74+
'sync-start-trace',
75+
);
76+
77+
expect(traceInstance).not.toBeInstanceOf(Promise);
78+
expect((traceInstance as { _started: boolean })._started).toBe(true);
79+
});
80+
});
3881
});

0 commit comments

Comments
 (0)