mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
eaefd9052a
In order to properly type an `Operation`, we need to change the call site from having two arguments: one for `type` and one for `payload` into an object that contains both. This isn't a perf regression because we were already constructing this object in the first place and doesn't change the emitted event so shouldn't affect the dev tools. None of the call sites are actually flow-ified so it isn't technically used but once we will, it'll make sure that we don't send random strings and payload through those very generic methods.
427 lines
12 KiB
JavaScript
427 lines
12 KiB
JavaScript
/**
|
|
* Copyright 2016-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 ReactDebugTool
|
|
* @flow
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
var ReactInvalidSetStateWarningHook = require('ReactInvalidSetStateWarningHook');
|
|
var ReactHostOperationHistoryHook = require('ReactHostOperationHistoryHook');
|
|
var ReactComponentTreeHook = require('ReactComponentTreeHook');
|
|
var ReactChildrenMutationWarningHook = require('ReactChildrenMutationWarningHook');
|
|
var ExecutionEnvironment = require('ExecutionEnvironment');
|
|
|
|
var performanceNow = require('performanceNow');
|
|
var warning = require('warning');
|
|
|
|
import type { ReactElement } from 'ReactElementType';
|
|
import type { DebugID } from 'ReactInstanceType';
|
|
import type { Operation } from 'ReactHostOperationHistoryHook';
|
|
|
|
type Hook = any;
|
|
|
|
type TimerType =
|
|
'ctor' |
|
|
'render' |
|
|
'componentWillMount' |
|
|
'componentWillUnmount' |
|
|
'componentWillReceiveProps' |
|
|
'shouldComponentUpdate' |
|
|
'componentWillUpdate' |
|
|
'componentDidUpdate' |
|
|
'componentDidMount';
|
|
|
|
type Measurement = {
|
|
timerType: TimerType,
|
|
instanceID: DebugID,
|
|
duration: number,
|
|
};
|
|
|
|
type TreeSnapshot = {
|
|
[key: DebugID]: {
|
|
displayName: string,
|
|
text: string,
|
|
updateCount: number,
|
|
childIDs: Array<DebugID>,
|
|
ownerID: DebugID,
|
|
parentID: DebugID,
|
|
}
|
|
};
|
|
|
|
type HistoryItem = {
|
|
duration: number,
|
|
measurements: Array<Measurement>,
|
|
operations: Array<Operation>,
|
|
treeSnapshot: TreeSnapshot,
|
|
};
|
|
|
|
export type FlushHistory = Array<HistoryItem>;
|
|
|
|
var hooks = [];
|
|
var didHookThrowForEvent = {};
|
|
|
|
function callHook(event, fn, context, arg1, arg2, arg3, arg4, arg5) {
|
|
try {
|
|
fn.call(context, arg1, arg2, arg3, arg4, arg5);
|
|
} catch (e) {
|
|
warning(
|
|
didHookThrowForEvent[event],
|
|
'Exception thrown by hook while handling %s: %s',
|
|
event,
|
|
e + '\n' + e.stack
|
|
);
|
|
didHookThrowForEvent[event] = true;
|
|
}
|
|
}
|
|
|
|
function emitEvent(event, arg1, arg2, arg3, arg4, arg5) {
|
|
for (var i = 0; i < hooks.length; i++) {
|
|
var hook = hooks[i];
|
|
var fn = hook[event];
|
|
if (fn) {
|
|
callHook(event, fn, hook, arg1, arg2, arg3, arg4, arg5);
|
|
}
|
|
}
|
|
}
|
|
|
|
var isProfiling = false;
|
|
var flushHistory = [];
|
|
var lifeCycleTimerStack = [];
|
|
var currentFlushNesting = 0;
|
|
var currentFlushMeasurements = [];
|
|
var currentFlushStartTime = 0;
|
|
var currentTimerDebugID = null;
|
|
var currentTimerStartTime = 0;
|
|
var currentTimerNestedFlushDuration = 0;
|
|
var currentTimerType = null;
|
|
|
|
var lifeCycleTimerHasWarned = false;
|
|
|
|
function clearHistory() {
|
|
ReactComponentTreeHook.purgeUnmountedComponents();
|
|
ReactHostOperationHistoryHook.clearHistory();
|
|
}
|
|
|
|
function getTreeSnapshot(registeredIDs) {
|
|
return registeredIDs.reduce((tree, id) => {
|
|
var ownerID = ReactComponentTreeHook.getOwnerID(id);
|
|
var parentID = ReactComponentTreeHook.getParentID(id);
|
|
tree[id] = {
|
|
displayName: ReactComponentTreeHook.getDisplayName(id),
|
|
text: ReactComponentTreeHook.getText(id),
|
|
updateCount: ReactComponentTreeHook.getUpdateCount(id),
|
|
childIDs: ReactComponentTreeHook.getChildIDs(id),
|
|
// Text nodes don't have owners but this is close enough.
|
|
ownerID: ownerID ||
|
|
parentID && ReactComponentTreeHook.getOwnerID(parentID) ||
|
|
0,
|
|
parentID,
|
|
};
|
|
return tree;
|
|
}, {});
|
|
}
|
|
|
|
function resetMeasurements() {
|
|
var previousStartTime = currentFlushStartTime;
|
|
var previousMeasurements = currentFlushMeasurements;
|
|
var previousOperations = ReactHostOperationHistoryHook.getHistory();
|
|
|
|
if (currentFlushNesting === 0) {
|
|
currentFlushStartTime = 0;
|
|
currentFlushMeasurements = [];
|
|
clearHistory();
|
|
return;
|
|
}
|
|
|
|
if (previousMeasurements.length || previousOperations.length) {
|
|
var registeredIDs = ReactComponentTreeHook.getRegisteredIDs();
|
|
flushHistory.push({
|
|
duration: performanceNow() - previousStartTime,
|
|
measurements: previousMeasurements || [],
|
|
operations: previousOperations || [],
|
|
treeSnapshot: getTreeSnapshot(registeredIDs),
|
|
});
|
|
}
|
|
|
|
clearHistory();
|
|
currentFlushStartTime = performanceNow();
|
|
currentFlushMeasurements = [];
|
|
}
|
|
|
|
function checkDebugID(debugID, allowRoot = false) {
|
|
if (allowRoot && debugID === 0) {
|
|
return;
|
|
}
|
|
if (!debugID) {
|
|
warning(false, 'ReactDebugTool: debugID may not be empty.');
|
|
}
|
|
}
|
|
|
|
function beginLifeCycleTimer(debugID, timerType) {
|
|
if (currentFlushNesting === 0) {
|
|
return;
|
|
}
|
|
if (currentTimerType && !lifeCycleTimerHasWarned) {
|
|
warning(
|
|
false,
|
|
'There is an internal error in the React performance measurement code. ' +
|
|
'Did not expect %s timer to start while %s timer is still in ' +
|
|
'progress for %s instance.',
|
|
timerType,
|
|
currentTimerType || 'no',
|
|
(debugID === currentTimerDebugID) ? 'the same' : 'another'
|
|
);
|
|
lifeCycleTimerHasWarned = true;
|
|
}
|
|
currentTimerStartTime = performanceNow();
|
|
currentTimerNestedFlushDuration = 0;
|
|
currentTimerDebugID = debugID;
|
|
currentTimerType = timerType;
|
|
}
|
|
|
|
function endLifeCycleTimer(debugID, timerType) {
|
|
if (currentFlushNesting === 0) {
|
|
return;
|
|
}
|
|
if (currentTimerType !== timerType && !lifeCycleTimerHasWarned) {
|
|
warning(
|
|
false,
|
|
'There is an internal error in the React performance measurement code. ' +
|
|
'We did not expect %s timer to stop while %s timer is still in ' +
|
|
'progress for %s instance. Please report this as a bug in React.',
|
|
timerType,
|
|
currentTimerType || 'no',
|
|
(debugID === currentTimerDebugID) ? 'the same' : 'another'
|
|
);
|
|
lifeCycleTimerHasWarned = true;
|
|
}
|
|
if (isProfiling) {
|
|
currentFlushMeasurements.push({
|
|
timerType,
|
|
instanceID: debugID,
|
|
duration: performanceNow() - currentTimerStartTime - currentTimerNestedFlushDuration,
|
|
});
|
|
}
|
|
currentTimerStartTime = 0;
|
|
currentTimerNestedFlushDuration = 0;
|
|
currentTimerDebugID = null;
|
|
currentTimerType = null;
|
|
}
|
|
|
|
function pauseCurrentLifeCycleTimer() {
|
|
var currentTimer = {
|
|
startTime: currentTimerStartTime,
|
|
nestedFlushStartTime: performanceNow(),
|
|
debugID: currentTimerDebugID,
|
|
timerType: currentTimerType,
|
|
};
|
|
lifeCycleTimerStack.push(currentTimer);
|
|
currentTimerStartTime = 0;
|
|
currentTimerNestedFlushDuration = 0;
|
|
currentTimerDebugID = null;
|
|
currentTimerType = null;
|
|
}
|
|
|
|
function resumeCurrentLifeCycleTimer() {
|
|
var {startTime, nestedFlushStartTime, debugID, timerType} = lifeCycleTimerStack.pop();
|
|
var nestedFlushDuration = performanceNow() - nestedFlushStartTime;
|
|
currentTimerStartTime = startTime;
|
|
currentTimerNestedFlushDuration += nestedFlushDuration;
|
|
currentTimerDebugID = debugID;
|
|
currentTimerType = timerType;
|
|
}
|
|
|
|
var lastMarkTimeStamp = 0;
|
|
var canUsePerformanceMeasure: boolean =
|
|
// $FlowFixMe https://github.com/facebook/flow/issues/2345
|
|
typeof performance !== 'undefined' &&
|
|
typeof performance.mark === 'function' &&
|
|
typeof performance.clearMarks === 'function' &&
|
|
typeof performance.measure === 'function' &&
|
|
typeof performance.clearMeasures === 'function';
|
|
|
|
function shouldMark(debugID) {
|
|
if (!isProfiling || !canUsePerformanceMeasure) {
|
|
return false;
|
|
}
|
|
var element = ReactComponentTreeHook.getElement(debugID);
|
|
if (element == null || typeof element !== 'object') {
|
|
return false;
|
|
}
|
|
var isHostElement = typeof element.type === 'string';
|
|
if (isHostElement) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function markBegin(debugID, markType) {
|
|
if (!shouldMark(debugID)) {
|
|
return;
|
|
}
|
|
|
|
var markName = `${debugID}::${markType}`;
|
|
lastMarkTimeStamp = performanceNow();
|
|
performance.mark(markName);
|
|
}
|
|
|
|
function markEnd(debugID, markType) {
|
|
if (!shouldMark(debugID)) {
|
|
return;
|
|
}
|
|
|
|
var markName = `${debugID}::${markType}`;
|
|
var displayName = ReactComponentTreeHook.getDisplayName(debugID) || 'Unknown';
|
|
|
|
// Chrome has an issue of dropping markers recorded too fast:
|
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=640652
|
|
// To work around this, we will not report very small measurements.
|
|
// I determined the magic number by tweaking it back and forth.
|
|
// 0.05ms was enough to prevent the issue, but I set it to 0.1ms to be safe.
|
|
// When the bug is fixed, we can `measure()` unconditionally if we want to.
|
|
var timeStamp = performanceNow();
|
|
if (timeStamp - lastMarkTimeStamp > 0.1) {
|
|
var measurementName = `${displayName} [${markType}]`;
|
|
performance.measure(measurementName, markName);
|
|
}
|
|
|
|
performance.clearMarks(markName);
|
|
performance.clearMeasures(measurementName);
|
|
}
|
|
|
|
var ReactDebugTool = {
|
|
addHook(hook: Hook): void {
|
|
hooks.push(hook);
|
|
},
|
|
removeHook(hook: Hook): void {
|
|
for (var i = 0; i < hooks.length; i++) {
|
|
if (hooks[i] === hook) {
|
|
hooks.splice(i, 1);
|
|
i--;
|
|
}
|
|
}
|
|
},
|
|
isProfiling(): boolean {
|
|
return isProfiling;
|
|
},
|
|
beginProfiling(): void {
|
|
if (isProfiling) {
|
|
return;
|
|
}
|
|
|
|
isProfiling = true;
|
|
flushHistory.length = 0;
|
|
resetMeasurements();
|
|
ReactDebugTool.addHook(ReactHostOperationHistoryHook);
|
|
},
|
|
endProfiling(): void {
|
|
if (!isProfiling) {
|
|
return;
|
|
}
|
|
|
|
isProfiling = false;
|
|
resetMeasurements();
|
|
ReactDebugTool.removeHook(ReactHostOperationHistoryHook);
|
|
},
|
|
getFlushHistory(): FlushHistory {
|
|
return flushHistory;
|
|
},
|
|
onBeginFlush(): void {
|
|
currentFlushNesting++;
|
|
resetMeasurements();
|
|
pauseCurrentLifeCycleTimer();
|
|
emitEvent('onBeginFlush');
|
|
},
|
|
onEndFlush(): void {
|
|
resetMeasurements();
|
|
currentFlushNesting--;
|
|
resumeCurrentLifeCycleTimer();
|
|
emitEvent('onEndFlush');
|
|
},
|
|
onBeginLifeCycleTimer(debugID: DebugID, timerType: TimerType): void {
|
|
checkDebugID(debugID);
|
|
emitEvent('onBeginLifeCycleTimer', debugID, timerType);
|
|
markBegin(debugID, timerType);
|
|
beginLifeCycleTimer(debugID, timerType);
|
|
},
|
|
onEndLifeCycleTimer(debugID: DebugID, timerType: TimerType): void {
|
|
checkDebugID(debugID);
|
|
endLifeCycleTimer(debugID, timerType);
|
|
markEnd(debugID, timerType);
|
|
emitEvent('onEndLifeCycleTimer', debugID, timerType);
|
|
},
|
|
onBeginProcessingChildContext(): void {
|
|
emitEvent('onBeginProcessingChildContext');
|
|
},
|
|
onEndProcessingChildContext(): void {
|
|
emitEvent('onEndProcessingChildContext');
|
|
},
|
|
onHostOperation(operation: Operation) {
|
|
checkDebugID(operation.instanceID);
|
|
emitEvent('onHostOperation', operation);
|
|
},
|
|
onSetState(): void {
|
|
emitEvent('onSetState');
|
|
},
|
|
onSetChildren(debugID: DebugID, childDebugIDs: Array<DebugID>) {
|
|
checkDebugID(debugID);
|
|
childDebugIDs.forEach(checkDebugID);
|
|
emitEvent('onSetChildren', debugID, childDebugIDs);
|
|
},
|
|
onBeforeMountComponent(debugID: DebugID, element: ReactElement, parentDebugID: DebugID): void {
|
|
checkDebugID(debugID);
|
|
checkDebugID(parentDebugID, true);
|
|
emitEvent('onBeforeMountComponent', debugID, element, parentDebugID);
|
|
markBegin(debugID, 'mount');
|
|
},
|
|
onMountComponent(debugID: DebugID): void {
|
|
checkDebugID(debugID);
|
|
markEnd(debugID, 'mount');
|
|
emitEvent('onMountComponent', debugID);
|
|
},
|
|
onBeforeUpdateComponent(debugID: DebugID, element: ReactElement): void {
|
|
checkDebugID(debugID);
|
|
emitEvent('onBeforeUpdateComponent', debugID, element);
|
|
markBegin(debugID, 'update');
|
|
},
|
|
onUpdateComponent(debugID: DebugID): void {
|
|
checkDebugID(debugID);
|
|
markEnd(debugID, 'update');
|
|
emitEvent('onUpdateComponent', debugID);
|
|
},
|
|
onBeforeUnmountComponent(debugID: DebugID): void {
|
|
checkDebugID(debugID);
|
|
emitEvent('onBeforeUnmountComponent', debugID);
|
|
markBegin(debugID, 'unmount');
|
|
},
|
|
onUnmountComponent(debugID: DebugID): void {
|
|
checkDebugID(debugID);
|
|
markEnd(debugID, 'unmount');
|
|
emitEvent('onUnmountComponent', debugID);
|
|
},
|
|
onTestEvent(): void {
|
|
emitEvent('onTestEvent');
|
|
},
|
|
};
|
|
|
|
// TODO remove these when RN/www gets updated
|
|
(ReactDebugTool: any).addDevtool = ReactDebugTool.addHook;
|
|
(ReactDebugTool: any).removeDevtool = ReactDebugTool.removeHook;
|
|
|
|
ReactDebugTool.addHook(ReactInvalidSetStateWarningHook);
|
|
ReactDebugTool.addHook(ReactComponentTreeHook);
|
|
ReactDebugTool.addHook(ReactChildrenMutationWarningHook);
|
|
var url = (ExecutionEnvironment.canUseDOM && window.location.href) || '';
|
|
if ((/[?&]react_perf\b/).test(url)) {
|
|
ReactDebugTool.beginProfiling();
|
|
}
|
|
|
|
module.exports = ReactDebugTool;
|