diff --git a/packages/react-reconciler/src/SchedulingProfiler.js b/packages/react-reconciler/src/SchedulingProfiler.js index 4c2a18b780e6..1c69edebcbb7 100644 --- a/packages/react-reconciler/src/SchedulingProfiler.js +++ b/packages/react-reconciler/src/SchedulingProfiler.js @@ -20,31 +20,63 @@ import getComponentName from 'shared/getComponentName'; * require. */ const supportsUserTiming = - typeof performance !== 'undefined' && typeof performance.mark === 'function'; + typeof performance !== 'undefined' && + typeof performance.mark === 'function' && + typeof performance.clearMarks === 'function'; + +let supportsUserTimingV3 = false; +if (enableSchedulingProfiler) { + if (supportsUserTiming) { + const CHECK_V3_MARK = '__v3'; + const markOptions = {}; + // $FlowFixMe: Ignore Flow complaining about needing a value + Object.defineProperty(markOptions, 'startTime', { + get: function() { + supportsUserTimingV3 = true; + return 0; + }, + set: function() {}, + }); + + try { + // $FlowFixMe: Flow expects the User Timing level 2 API. + performance.mark(CHECK_V3_MARK, markOptions); + } catch (error) { + // Ignore + } finally { + performance.clearMarks(CHECK_V3_MARK); + } + } +} function formatLanes(laneOrLanes: Lane | Lanes): string { return ((laneOrLanes: any): number).toString(); } +function markAndClear(name) { + performance.mark(name); + performance.clearMarks(name); +} + // Create a mark on React initialization if (enableSchedulingProfiler) { - if (supportsUserTiming) { - performance.mark(`--react-init-${ReactVersion}`); + if (supportsUserTimingV3) { + markAndClear(`--react-init-${ReactVersion}`); } } export function markCommitStarted(lanes: Lanes): void { if (enableSchedulingProfiler) { - if (supportsUserTiming) { - performance.mark(`--commit-start-${formatLanes(lanes)}`); + if (supportsUserTimingV3) { + markAndClear(`--commit-start-${formatLanes(lanes)}`); } } } export function markCommitStopped(): void { if (enableSchedulingProfiler) { - if (supportsUserTiming) { - performance.mark('--commit-stop'); + if (supportsUserTimingV3) { + markAndClear('--commit-stop'); } } } @@ -63,14 +95,14 @@ function getWakeableID(wakeable: Wakeable): number { export function markComponentSuspended(fiber: Fiber, wakeable: Wakeable): void { if (enableSchedulingProfiler) { - if (supportsUserTiming) { + if (supportsUserTimingV3) { const id = getWakeableID(wakeable); const componentName = getComponentName(fiber.type) || 'Unknown'; // TODO Add component stack id - performance.mark(`--suspense-suspend-${id}-${componentName}`); + markAndClear(`--suspense-suspend-${id}-${componentName}`); wakeable.then( - () => performance.mark(`--suspense-resolved-${id}-${componentName}`), - () => performance.mark(`--suspense-rejected-${id}-${componentName}`), + () => markAndClear(`--suspense-resolved-${id}-${componentName}`), + () => markAndClear(`--suspense-rejected-${id}-${componentName}`), ); } } @@ -78,74 +110,74 @@ export function markComponentSuspended(fiber: Fiber, wakeable: Wakeable): void { export function markLayoutEffectsStarted(lanes: Lanes): void { if (enableSchedulingProfiler) { - if (supportsUserTiming) { - performance.mark(`--layout-effects-start-${formatLanes(lanes)}`); + if (supportsUserTimingV3) { + markAndClear(`--layout-effects-start-${formatLanes(lanes)}`); } } } export function markLayoutEffectsStopped(): void { if (enableSchedulingProfiler) { - if (supportsUserTiming) { - performance.mark('--layout-effects-stop'); + if (supportsUserTimingV3) { + markAndClear('--layout-effects-stop'); } } } export function markPassiveEffectsStarted(lanes: Lanes): void { if (enableSchedulingProfiler) { - if (supportsUserTiming) { - performance.mark(`--passive-effects-start-${formatLanes(lanes)}`); + if (supportsUserTimingV3) { + markAndClear(`--passive-effects-start-${formatLanes(lanes)}`); } } } export function markPassiveEffectsStopped(): void { if (enableSchedulingProfiler) { - if (supportsUserTiming) { - performance.mark('--passive-effects-stop'); + if (supportsUserTimingV3) { + markAndClear('--passive-effects-stop'); } } } export function markRenderStarted(lanes: Lanes): void { if (enableSchedulingProfiler) { - if (supportsUserTiming) { - performance.mark(`--render-start-${formatLanes(lanes)}`); + if (supportsUserTimingV3) { + markAndClear(`--render-start-${formatLanes(lanes)}`); } } } export function markRenderYielded(): void { if (enableSchedulingProfiler) { - if (supportsUserTiming) { - performance.mark('--render-yield'); + if (supportsUserTimingV3) { + markAndClear('--render-yield'); } } } export function markRenderStopped(): void { if (enableSchedulingProfiler) { - if (supportsUserTiming) { - performance.mark('--render-stop'); + if (supportsUserTimingV3) { + markAndClear('--render-stop'); } } } export function markRenderScheduled(lane: Lane): void { if (enableSchedulingProfiler) { - if (supportsUserTiming) { - performance.mark(`--schedule-render-${formatLanes(lane)}`); + if (supportsUserTimingV3) { + markAndClear(`--schedule-render-${formatLanes(lane)}`); } } } export function markForceUpdateScheduled(fiber: Fiber, lane: Lane): void { if (enableSchedulingProfiler) { - if (supportsUserTiming) { + if (supportsUserTimingV3) { const componentName = getComponentName(fiber.type) || 'Unknown'; // TODO Add component stack id - performance.mark( + markAndClear( `--schedule-forced-update-${formatLanes(lane)}-${componentName}`, ); } @@ -154,10 +186,10 @@ export function markForceUpdateScheduled(fiber: Fiber, lane: Lane): void { export function markStateUpdateScheduled(fiber: Fiber, lane: Lane): void { if (enableSchedulingProfiler) { - if (supportsUserTiming) { + if (supportsUserTimingV3) { const componentName = getComponentName(fiber.type) || 'Unknown'; // TODO Add component stack id - performance.mark( + markAndClear( `--schedule-state-update-${formatLanes(lane)}-${componentName}`, ); } diff --git a/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js b/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js index 55f8d3617c0d..35c854e4b2e7 100644 --- a/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js +++ b/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js @@ -18,22 +18,56 @@ describe('SchedulingProfiler', () => { let ReactNoop; let Scheduler; + let clearedMarks; + let featureDetectionMarkName = null; let marks; function createUserTimingPolyfill() { + featureDetectionMarkName = null; + + clearedMarks = []; + marks = []; + // This is not a true polyfill, but it gives us enough to capture marks. // Reference: https://developer.mozilla.org/en-US/docs/Web/API/User_Timing_API return { - mark(markName) { + clearMarks(markName) { + clearedMarks.push(markName); + marks = marks.filter(mark => mark !== markName); + }, + mark(markName, markOptions) { + if (featureDetectionMarkName === null) { + featureDetectionMarkName = markName; + } marks.push(markName); + if (markOptions != null) { + // This is triggers the feature detection. + markOptions.startTime++; + } }, }; } + function clearPendingMarks() { + clearedMarks.splice(0); + } + + function expectMarksToContain(expectedMarks) { + expect(clearedMarks).toContain(expectedMarks); + } + + function expectMarksToEqual(expectedMarks) { + expect( + clearedMarks[0] === featureDetectionMarkName + ? clearedMarks.slice(1) + : clearedMarks, + ).toEqual(expectedMarks); + } + beforeEach(() => { jest.resetModules(); + global.performance = createUserTimingPolyfill(); - marks = []; React = require('react'); @@ -45,25 +79,28 @@ describe('SchedulingProfiler', () => { }); afterEach(() => { + // Verify all logged marks also get cleared. + expect(marks).toHaveLength(0); + delete global.performance; }); // @gate !enableSchedulingProfiler it('should not mark if enableSchedulingProfiler is false', () => { ReactTestRenderer.create(
); - expect(marks).toEqual([]); + expectMarksToEqual([]); }); // @gate enableSchedulingProfiler it('should log React version on initialization', () => { - expect(marks).toEqual([`--react-init-${ReactVersion}`]); + expectMarksToEqual([`--react-init-${ReactVersion}`]); }); // @gate enableSchedulingProfiler it('should mark sync render without suspends or state updates', () => { ReactTestRenderer.create(
); - expect(marks).toEqual([ + expectMarksToEqual([ `--react-init-${ReactVersion}`, '--schedule-render-1', '--render-start-1', @@ -79,16 +116,16 @@ describe('SchedulingProfiler', () => { it('should mark concurrent render without suspends or state updates', () => { ReactTestRenderer.create(
, {unstable_isConcurrent: true}); - expect(marks).toEqual([ + expectMarksToEqual([ `--react-init-${ReactVersion}`, '--schedule-render-512', ]); - marks.splice(0); + clearPendingMarks(); expect(Scheduler).toFlushUntilNextPaint([]); - expect(marks).toEqual([ + expectMarksToEqual([ '--render-start-512', '--render-stop', '--commit-start-512', @@ -114,7 +151,7 @@ describe('SchedulingProfiler', () => { // Do one step of work. expect(ReactNoop.flushNextYield()).toEqual(['Foo']); - expect(marks).toEqual([ + expectMarksToEqual([ `--react-init-${ReactVersion}`, '--schedule-render-512', '--render-start-512', @@ -135,7 +172,7 @@ describe('SchedulingProfiler', () => { , ); - expect(marks).toEqual([ + expectMarksToEqual([ `--react-init-${ReactVersion}`, '--schedule-render-1', '--render-start-1', @@ -147,10 +184,10 @@ describe('SchedulingProfiler', () => { '--commit-stop', ]); - marks.splice(0); + clearPendingMarks(); await fakeSuspensePromise; - expect(marks).toEqual(['--suspense-resolved-0-Example']); + expectMarksToEqual(['--suspense-resolved-0-Example']); }); // @gate enableSchedulingProfiler @@ -166,7 +203,7 @@ describe('SchedulingProfiler', () => { , ); - expect(marks).toEqual([ + expectMarksToEqual([ `--react-init-${ReactVersion}`, '--schedule-render-1', '--render-start-1', @@ -178,10 +215,10 @@ describe('SchedulingProfiler', () => { '--commit-stop', ]); - marks.splice(0); + clearPendingMarks(); await expect(fakeSuspensePromise).rejects.toThrow(); - expect(marks).toEqual(['--suspense-rejected-0-Example']); + expectMarksToEqual(['--suspense-rejected-0-Example']); }); // @gate enableSchedulingProfiler @@ -198,16 +235,16 @@ describe('SchedulingProfiler', () => { {unstable_isConcurrent: true}, ); - expect(marks).toEqual([ + expectMarksToEqual([ `--react-init-${ReactVersion}`, '--schedule-render-512', ]); - marks.splice(0); + clearPendingMarks(); expect(Scheduler).toFlushUntilNextPaint([]); - expect(marks).toEqual([ + expectMarksToEqual([ '--render-start-512', '--suspense-suspend-0-Example', '--render-stop', @@ -217,10 +254,10 @@ describe('SchedulingProfiler', () => { '--commit-stop', ]); - marks.splice(0); + clearPendingMarks(); await fakeSuspensePromise; - expect(marks).toEqual(['--suspense-resolved-0-Example']); + expectMarksToEqual(['--suspense-resolved-0-Example']); }); // @gate enableSchedulingProfiler @@ -237,16 +274,16 @@ describe('SchedulingProfiler', () => { {unstable_isConcurrent: true}, ); - expect(marks).toEqual([ + expectMarksToEqual([ `--react-init-${ReactVersion}`, '--schedule-render-512', ]); - marks.splice(0); + clearPendingMarks(); expect(Scheduler).toFlushUntilNextPaint([]); - expect(marks).toEqual([ + expectMarksToEqual([ '--render-start-512', '--suspense-suspend-0-Example', '--render-stop', @@ -256,10 +293,10 @@ describe('SchedulingProfiler', () => { '--commit-stop', ]); - marks.splice(0); + clearPendingMarks(); await expect(fakeSuspensePromise).rejects.toThrow(); - expect(marks).toEqual(['--suspense-rejected-0-Example']); + expectMarksToEqual(['--suspense-rejected-0-Example']); }); // @gate enableSchedulingProfiler @@ -276,16 +313,16 @@ describe('SchedulingProfiler', () => { ReactTestRenderer.create(, {unstable_isConcurrent: true}); - expect(marks).toEqual([ + expectMarksToEqual([ `--react-init-${ReactVersion}`, '--schedule-render-512', ]); - marks.splice(0); + clearPendingMarks(); expect(Scheduler).toFlushUntilNextPaint([]); - expect(marks).toEqual([ + expectMarksToEqual([ '--render-start-512', '--render-stop', '--commit-start-512', @@ -313,16 +350,16 @@ describe('SchedulingProfiler', () => { ReactTestRenderer.create(, {unstable_isConcurrent: true}); - expect(marks).toEqual([ + expectMarksToEqual([ `--react-init-${ReactVersion}`, '--schedule-render-512', ]); - marks.splice(0); + clearPendingMarks(); expect(Scheduler).toFlushUntilNextPaint([]); - expect(marks).toEqual([ + expectMarksToEqual([ '--render-start-512', '--render-stop', '--commit-start-512', @@ -351,12 +388,12 @@ describe('SchedulingProfiler', () => { ReactTestRenderer.create(, {unstable_isConcurrent: true}); - expect(marks).toEqual([ + expectMarksToEqual([ `--react-init-${ReactVersion}`, '--schedule-render-512', ]); - marks.splice(0); + clearPendingMarks(); expect(() => { expect(Scheduler).toFlushUntilNextPaint([]); @@ -364,8 +401,8 @@ describe('SchedulingProfiler', () => { gate(({old}) => old - ? expect(marks).toContain('--schedule-state-update-1024-Example') - : expect(marks).toContain('--schedule-state-update-512-Example'), + ? expectMarksToContain('--schedule-state-update-1024-Example') + : expectMarksToContain('--schedule-state-update-512-Example'), ); }); @@ -383,12 +420,12 @@ describe('SchedulingProfiler', () => { ReactTestRenderer.create(, {unstable_isConcurrent: true}); - expect(marks).toEqual([ + expectMarksToEqual([ `--react-init-${ReactVersion}`, '--schedule-render-512', ]); - marks.splice(0); + clearPendingMarks(); expect(() => { expect(Scheduler).toFlushUntilNextPaint([]); @@ -396,8 +433,8 @@ describe('SchedulingProfiler', () => { gate(({old}) => old - ? expect(marks).toContain('--schedule-forced-update-1024-Example') - : expect(marks).toContain('--schedule-forced-update-512-Example'), + ? expectMarksToContain('--schedule-forced-update-1024-Example') + : expectMarksToContain('--schedule-forced-update-512-Example'), ); }); @@ -413,16 +450,16 @@ describe('SchedulingProfiler', () => { ReactTestRenderer.create(, {unstable_isConcurrent: true}); - expect(marks).toEqual([ + expectMarksToEqual([ `--react-init-${ReactVersion}`, '--schedule-render-512', ]); - marks.splice(0); + clearPendingMarks(); expect(Scheduler).toFlushUntilNextPaint([]); - expect(marks).toEqual([ + expectMarksToEqual([ '--render-start-512', '--render-stop', '--commit-start-512', @@ -451,7 +488,7 @@ describe('SchedulingProfiler', () => { ReactTestRenderer.create(, {unstable_isConcurrent: true}); }); - expect(marks).toEqual([ + expectMarksToEqual([ `--react-init-${ReactVersion}`, '--schedule-render-512', '--render-start-512', @@ -486,8 +523,8 @@ describe('SchedulingProfiler', () => { gate(({old}) => old - ? expect(marks).toContain('--schedule-state-update-1024-Example') - : expect(marks).toContain('--schedule-state-update-512-Example'), + ? expectMarksToContain('--schedule-state-update-1024-Example') + : expectMarksToContain('--schedule-state-update-512-Example'), ); }); }); diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js index 95ba1859e150..1321bf70bab2 100644 --- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js @@ -25,6 +25,8 @@ export const decoupleUpdatePriorityFromScheduler = __VARIANT__; // NOTE: This feature will only work in DEV mode; all callsights are wrapped with __DEV__. export const enableDebugTracing = __EXPERIMENTAL__; +export const enableSchedulingProfiler = __VARIANT__; + // This only has an effect in the new reconciler. But also, the new reconciler // is only enabled when __VARIANT__ is true. So this is set to the opposite of // __VARIANT__ so that it's `false` when running against the new reconciler. diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 947b05ab8530..70d68cef060b 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -40,7 +40,8 @@ export const enableProfilerNestedUpdateScheduledHook = __PROFILE__ && dynamicFeatureFlags.enableProfilerNestedUpdateScheduledHook; // Logs additional User Timing API marks for use with an experimental profiling tool. -export const enableSchedulingProfiler = __PROFILE__; +export const enableSchedulingProfiler = + __PROFILE__ && dynamicFeatureFlags.enableSchedulingProfiler; // Note: we'll want to remove this when we to userland implementation. // For now, we'll turn it on for everyone because it's *already* on for everyone in practice.