react-native-macos
Version:
A framework for building native macOS apps using React
402 lines (359 loc) • 12.3 kB
JavaScript
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactDebugFiberPerf
* @flow
*/
import type {Fiber} from 'ReactFiber';
type MeasurementPhase =
| 'componentWillMount'
| 'componentWillUnmount'
| 'componentWillReceiveProps'
| 'shouldComponentUpdate'
| 'componentWillUpdate'
| 'componentDidUpdate'
| 'componentDidMount'
| 'getChildContext';
// Trust the developer to only use this with a __DEV__ check
let ReactDebugFiberPerf = ((null: any): typeof ReactDebugFiberPerf);
if (__DEV__) {
const {
HostRoot,
HostComponent,
HostText,
HostPortal,
YieldComponent,
Fragment,
} = require('ReactTypeOfWork');
const getComponentName = require('getComponentName');
// Prefix measurements so that it's possible to filter them.
// Longer prefixes are hard to read in DevTools.
const reactEmoji = '\u269B';
const warningEmoji = '\u26D4';
const supportsUserTiming =
typeof performance !== 'undefined' &&
typeof performance.mark === 'function' &&
typeof performance.clearMarks === 'function' &&
typeof performance.measure === 'function' &&
typeof performance.clearMeasures === 'function';
// Keep track of current fiber so that we know the path to unwind on pause.
// TODO: this looks the same as nextUnitOfWork in scheduler. Can we unify them?
let currentFiber: Fiber | null = null;
// If we're in the middle of user code, which fiber and method is it?
// Reusing `currentFiber` would be confusing for this because user code fiber
// can change during commit phase too, but we don't need to unwind it (since
// lifecycles in the commit phase don't resemble a tree).
let currentPhase: MeasurementPhase | null = null;
let currentPhaseFiber: Fiber | null = null;
// Did lifecycle hook schedule an update? This is often a performance problem,
// so we will keep track of it, and include it in the report.
// Track commits caused by cascading updates.
let isCommitting: boolean = false;
let hasScheduledUpdateInCurrentCommit: boolean = false;
let hasScheduledUpdateInCurrentPhase: boolean = false;
let commitCountInCurrentWorkLoop: number = 0;
let effectCountInCurrentCommit: number = 0;
// During commits, we only show a measurement once per method name
// to avoid stretch the commit phase with measurement overhead.
const labelsInCurrentCommit: Set<string> = new Set();
const formatMarkName = (markName: string) => {
return `${reactEmoji} ${markName}`;
};
const formatLabel = (label: string, warning: string | null) => {
const prefix = warning ? `${warningEmoji} ` : `${reactEmoji} `;
const suffix = warning ? ` Warning: ${warning}` : '';
return `${prefix}${label}${suffix}`;
};
const beginMark = (markName: string) => {
performance.mark(formatMarkName(markName));
};
const clearMark = (markName: string) => {
performance.clearMarks(formatMarkName(markName));
};
const endMark = (label: string, markName: string, warning: string | null) => {
const formattedMarkName = formatMarkName(markName);
const formattedLabel = formatLabel(label, warning);
try {
performance.measure(formattedLabel, formattedMarkName);
} catch (err) {
// If previous mark was missing for some reason, this will throw.
// This could only happen if React crashed in an unexpected place earlier.
// Don't pile on with more errors.
}
// Clear marks immediately to avoid growing buffer.
performance.clearMarks(formattedMarkName);
performance.clearMeasures(formattedLabel);
};
const getFiberMarkName = (label: string, debugID: number) => {
return `${label} (#${debugID})`;
};
const getFiberLabel = (
componentName: string,
isMounted: boolean,
phase: MeasurementPhase | null,
) => {
if (phase === null) {
// These are composite component total time measurements.
return `${componentName} [${isMounted ? 'update' : 'mount'}]`;
} else {
// Composite component methods.
return `${componentName}.${phase}`;
}
};
const beginFiberMark = (
fiber: Fiber,
phase: MeasurementPhase | null,
): boolean => {
const componentName = getComponentName(fiber) || 'Unknown';
const debugID = ((fiber._debugID: any): number);
const isMounted = fiber.alternate !== null;
const label = getFiberLabel(componentName, isMounted, phase);
if (isCommitting && labelsInCurrentCommit.has(label)) {
// During the commit phase, we don't show duplicate labels because
// there is a fixed overhead for every measurement, and we don't
// want to stretch the commit phase beyond necessary.
return false;
}
labelsInCurrentCommit.add(label);
const markName = getFiberMarkName(label, debugID);
beginMark(markName);
return true;
};
const clearFiberMark = (fiber: Fiber, phase: MeasurementPhase | null) => {
const componentName = getComponentName(fiber) || 'Unknown';
const debugID = ((fiber._debugID: any): number);
const isMounted = fiber.alternate !== null;
const label = getFiberLabel(componentName, isMounted, phase);
const markName = getFiberMarkName(label, debugID);
clearMark(markName);
};
const endFiberMark = (
fiber: Fiber,
phase: MeasurementPhase | null,
warning: string | null,
) => {
const componentName = getComponentName(fiber) || 'Unknown';
const debugID = ((fiber._debugID: any): number);
const isMounted = fiber.alternate !== null;
const label = getFiberLabel(componentName, isMounted, phase);
const markName = getFiberMarkName(label, debugID);
endMark(label, markName, warning);
};
const shouldIgnoreFiber = (fiber: Fiber): boolean => {
// Host components should be skipped in the timeline.
// We could check typeof fiber.type, but does this work with RN?
switch (fiber.tag) {
case HostRoot:
case HostComponent:
case HostText:
case HostPortal:
case YieldComponent:
case Fragment:
return true;
default:
return false;
}
};
const clearPendingPhaseMeasurement = () => {
if (currentPhase !== null && currentPhaseFiber !== null) {
clearFiberMark(currentPhaseFiber, currentPhase);
}
currentPhaseFiber = null;
currentPhase = null;
hasScheduledUpdateInCurrentPhase = false;
};
const pauseTimers = () => {
// Stops all currently active measurements so that they can be resumed
// if we continue in a later deferred loop from the same unit of work.
let fiber = currentFiber;
while (fiber) {
if (fiber._debugIsCurrentlyTiming) {
endFiberMark(fiber, null, null);
}
fiber = fiber.return;
}
};
const resumeTimersRecursively = (fiber: Fiber) => {
if (fiber.return !== null) {
resumeTimersRecursively(fiber.return);
}
if (fiber._debugIsCurrentlyTiming) {
beginFiberMark(fiber, null);
}
};
const resumeTimers = () => {
// Resumes all measurements that were active during the last deferred loop.
if (currentFiber !== null) {
resumeTimersRecursively(currentFiber);
}
};
ReactDebugFiberPerf = {
recordEffect(): void {
effectCountInCurrentCommit++;
},
recordScheduleUpdate(): void {
if (isCommitting) {
hasScheduledUpdateInCurrentCommit = true;
}
if (
currentPhase !== null &&
currentPhase !== 'componentWillMount' &&
currentPhase !== 'componentWillReceiveProps'
) {
hasScheduledUpdateInCurrentPhase = true;
}
},
startWorkTimer(fiber: Fiber): void {
if (!supportsUserTiming || shouldIgnoreFiber(fiber)) {
return;
}
// If we pause, this is the fiber to unwind from.
currentFiber = fiber;
if (!beginFiberMark(fiber, null)) {
return;
}
fiber._debugIsCurrentlyTiming = true;
},
cancelWorkTimer(fiber: Fiber): void {
if (!supportsUserTiming || shouldIgnoreFiber(fiber)) {
return;
}
// Remember we shouldn't complete measurement for this fiber.
// Otherwise flamechart will be deep even for small updates.
fiber._debugIsCurrentlyTiming = false;
clearFiberMark(fiber, null);
},
stopWorkTimer(fiber: Fiber): void {
if (!supportsUserTiming || shouldIgnoreFiber(fiber)) {
return;
}
// If we pause, its parent is the fiber to unwind from.
currentFiber = fiber.return;
if (!fiber._debugIsCurrentlyTiming) {
return;
}
fiber._debugIsCurrentlyTiming = false;
endFiberMark(fiber, null, null);
},
startPhaseTimer(fiber: Fiber, phase: MeasurementPhase): void {
if (!supportsUserTiming) {
return;
}
clearPendingPhaseMeasurement();
if (!beginFiberMark(fiber, phase)) {
return;
}
currentPhaseFiber = fiber;
currentPhase = phase;
},
stopPhaseTimer(): void {
if (!supportsUserTiming) {
return;
}
if (currentPhase !== null && currentPhaseFiber !== null) {
const warning = hasScheduledUpdateInCurrentPhase
? 'Scheduled a cascading update'
: null;
endFiberMark(currentPhaseFiber, currentPhase, warning);
}
currentPhase = null;
currentPhaseFiber = null;
},
startWorkLoopTimer(): void {
if (!supportsUserTiming) {
return;
}
commitCountInCurrentWorkLoop = 0;
// This is top level call.
// Any other measurements are performed within.
beginMark('(React Tree Reconciliation)');
// Resume any measurements that were in progress during the last loop.
resumeTimers();
},
stopWorkLoopTimer(): void {
if (!supportsUserTiming) {
return;
}
const warning = commitCountInCurrentWorkLoop > 1
? 'There were cascading updates'
: null;
commitCountInCurrentWorkLoop = 0;
// Pause any measurements until the next loop.
pauseTimers();
endMark(
'(React Tree Reconciliation)',
'(React Tree Reconciliation)',
warning,
);
},
startCommitTimer(): void {
if (!supportsUserTiming) {
return;
}
isCommitting = true;
hasScheduledUpdateInCurrentCommit = false;
labelsInCurrentCommit.clear();
beginMark('(Committing Changes)');
},
stopCommitTimer(): void {
if (!supportsUserTiming) {
return;
}
let warning = null;
if (hasScheduledUpdateInCurrentCommit) {
warning = 'Lifecycle hook scheduled a cascading update';
} else if (commitCountInCurrentWorkLoop > 0) {
warning = 'Caused by a cascading update in earlier commit';
}
hasScheduledUpdateInCurrentCommit = false;
commitCountInCurrentWorkLoop++;
isCommitting = false;
labelsInCurrentCommit.clear();
endMark('(Committing Changes)', '(Committing Changes)', warning);
},
startCommitHostEffectsTimer(): void {
if (!supportsUserTiming) {
return;
}
effectCountInCurrentCommit = 0;
beginMark('(Committing Host Effects)');
},
stopCommitHostEffectsTimer(): void {
if (!supportsUserTiming) {
return;
}
const count = effectCountInCurrentCommit;
effectCountInCurrentCommit = 0;
endMark(
`(Committing Host Effects: ${count} Total)`,
'(Committing Host Effects)',
null,
);
},
startCommitLifeCyclesTimer(): void {
if (!supportsUserTiming) {
return;
}
effectCountInCurrentCommit = 0;
beginMark('(Calling Lifecycle Methods)');
},
stopCommitLifeCyclesTimer(): void {
if (!supportsUserTiming) {
return;
}
const count = effectCountInCurrentCommit;
effectCountInCurrentCommit = 0;
endMark(
`(Calling Lifecycle Methods: ${count} Total)`,
'(Calling Lifecycle Methods)',
null,
);
},
};
}
module.exports = ReactDebugFiberPerf;