diff --git a/.changeset/dirty-buses-taste.md b/.changeset/dirty-buses-taste.md new file mode 100644 index 00000000..e2caadc1 --- /dev/null +++ b/.changeset/dirty-buses-taste.md @@ -0,0 +1,5 @@ +--- +'@callstack/reassure-measure': minor +--- + +improve `measureRenders` precision on React Native diff --git a/packages/measure/src/measure-renders.tsx b/packages/measure/src/measure-renders.tsx index 9a3a0ad3..2acb69ca 100644 --- a/packages/measure/src/measure-renders.tsx +++ b/packages/measure/src/measure-renders.tsx @@ -3,9 +3,10 @@ import * as logger from '@callstack/reassure-logger'; import { config } from './config'; import { RunResult, processRunResults } from './measure-helpers'; import { showFlagsOutputIfNeeded, writeTestStats } from './output'; +import { applyRenderPolyfills, revertRenderPolyfills } from './polyfills'; +import { ElementJsonTree, detectRedundantUpdates } from './redundant-renders'; import { resolveTestingLibrary, getTestingLibrary } from './testing-library'; import type { MeasureRendersResults } from './types'; -import { ElementJsonTree, detectRedundantUpdates } from './redundant-renders'; logger.configure({ verbose: process.env.REASSURE_VERBOSE === 'true' || process.env.REASSURE_VERBOSE === '1', @@ -59,6 +60,7 @@ async function measureRendersInternal( const testingLibrary = getTestingLibrary(); showFlagsOutputIfNeeded(); + applyRenderPolyfills(); const runResults: RunResult[] = []; let hasTooLateRender = false; @@ -66,7 +68,7 @@ async function measureRendersInternal( const renderJsonTrees: ElementJsonTree[] = []; let initialRenderCount = 0; - for (let i = 0; i < runs + warmupRuns; i += 1) { + for (let iteration = 0; iteration < runs + warmupRuns; iteration += 1) { let duration = 0; let count = 0; let isFinished = false; @@ -75,7 +77,7 @@ async function measureRendersInternal( const captureRenderDetails = () => { // We capture render details only on the first run - if (i !== 0) { + if (iteration !== 0) { return; } @@ -124,6 +126,8 @@ async function measureRendersInternal( ); } + revertRenderPolyfills(); + return { ...processRunResults(runResults, warmupRuns), issues: { diff --git a/packages/measure/src/polyfills.ts b/packages/measure/src/polyfills.ts new file mode 100644 index 00000000..35b8ea07 --- /dev/null +++ b/packages/measure/src/polyfills.ts @@ -0,0 +1,34 @@ +import { performance as perf } from 'perf_hooks'; +import { getTestingLibrary } from './testing-library'; + +export function applyRenderPolyfills() { + const testingLibrary = getTestingLibrary(); + if (testingLibrary === 'react-native') { + polyfillPerformanceNow(); + } +} + +export function revertRenderPolyfills() { + const testingLibrary = getTestingLibrary(); + if (testingLibrary === 'react-native') { + restorePerformanceNow(); + } +} + +/** + * React Native Jest preset mocks the global.performance object, with `now()` method being `Date.now()`. + * Ref: https://github.com/facebook/react-native/blob/3dfe22bd27429a43b4648c597b71f7965f31ca65/packages/react-native/jest/setup.js#L41 + * + * Then React uses `performance.now()` in `Scheduler` to measure component render time. + * https://github.com/facebook/react/blob/45804af18d589fd2c181f3b020f07661c46b73ea/packages/scheduler/src/forks/Scheduler.js#L59 + */ +let originalPerformanceNow: () => number; + +function polyfillPerformanceNow() { + originalPerformanceNow = global.performance?.now; + global.performance.now = () => perf.now(); +} + +function restorePerformanceNow() { + globalThis.performance.now = originalPerformanceNow; +}