Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't count the time inside flushes towards lifecycle hooks #6860

Merged
merged 2 commits into from
May 25, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 72 additions & 33 deletions src/renderers/shared/ReactDebugTool.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,13 @@ function emitEvent(handlerFunctionName, arg1, arg2, arg3, arg4, arg5) {

var isProfiling = false;
var flushHistory = [];
var lifeCycleTimerStack = [];
var currentFlushNesting = 0;
var currentFlushMeasurements = null;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be simpler to just store a reference to currentTimer here instead of leaving them split up.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it silly to worry about extra allocations here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ll probably do this, but as a separate PR.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. :) Doubly so because it's dev-only.

var currentFlushStartTime = null;
var currentTimerDebugID = null;
var currentTimerStartTime = null;
var currentTimerNestedFlushDuration = null;
var currentTimerType = null;

function clearHistory() {
Expand Down Expand Up @@ -103,6 +105,72 @@ function checkDebugID(debugID) {
warning(debugID, 'ReactDebugTool: debugID may not be empty.');
}

function beginLifeCycleTimer(debugID, timerType) {
if (!isProfiling || currentFlushNesting === 0) {
return;
}
warning(
!currentTimerType,
'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'
);
currentTimerStartTime = performanceNow();
currentTimerNestedFlushDuration = 0;
currentTimerDebugID = debugID;
currentTimerType = timerType;
}

function endLifeCycleTimer(debugID, timerType) {
if (!isProfiling || currentFlushNesting === 0) {
return;
}
warning(
currentTimerType === timerType,
'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'
);
currentFlushMeasurements.push({
timerType,
instanceID: debugID,
duration: performanceNow() - currentTimerStartTime - currentTimerNestedFlushDuration,
});
currentTimerStartTime = null;
currentTimerNestedFlushDuration = null;
currentTimerDebugID = null;
currentTimerType = null;
}

function pauseCurrentLifeCycleTimer() {
var currentTimer = {
startTime: currentTimerStartTime,
nestedFlushStartTime: performanceNow(),
debugID: currentTimerDebugID,
timerType: currentTimerType,
};
lifeCycleTimerStack.push(currentTimer);
currentTimerStartTime = null;
currentTimerNestedFlushDuration = null;
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 ReactDebugTool = {
addDevtool(devtool) {
eventHandlers.push(devtool);
Expand Down Expand Up @@ -148,58 +216,29 @@ var ReactDebugTool = {
if (__DEV__) {
currentFlushNesting++;
resetMeasurements();
pauseCurrentLifeCycleTimer();
}
emitEvent('onBeginFlush');
},
onEndFlush() {
if (__DEV__) {
resetMeasurements();
currentFlushNesting--;
resumeCurrentLifeCycleTimer();
}
emitEvent('onEndFlush');
},
onBeginLifeCycleTimer(debugID, timerType) {
checkDebugID(debugID);
emitEvent('onBeginLifeCycleTimer', debugID, timerType);
if (__DEV__) {
if (isProfiling && currentFlushNesting > 0) {
warning(
!currentTimerType,
'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'
);
currentTimerStartTime = performanceNow();
currentTimerDebugID = debugID;
currentTimerType = timerType;
}
beginLifeCycleTimer(debugID, timerType);
}
},
onEndLifeCycleTimer(debugID, timerType) {
checkDebugID(debugID);
if (__DEV__) {
if (isProfiling && currentFlushNesting > 0) {
warning(
currentTimerType === timerType,
'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'
);
currentFlushMeasurements.push({
timerType,
instanceID: debugID,
duration: performanceNow() - currentTimerStartTime,
});
currentTimerStartTime = null;
currentTimerDebugID = null;
currentTimerType = null;
}
endLifeCycleTimer(debugID, timerType);
}
emitEvent('onEndLifeCycleTimer', debugID, timerType);
},
Expand Down
56 changes: 56 additions & 0 deletions src/renderers/shared/__tests__/ReactPerf-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,62 @@ describe('ReactPerf', function() {
}]);
});

it('should not count time in a portal towards lifecycle method', function() {
function Foo() {
return null;
}

var portalContainer = document.createElement('div');
class Portal extends React.Component {
componentDidMount() {
ReactDOM.render(<Foo />, portalContainer);
}
render() {
return null;
}
}

var container = document.createElement('div');
var measurements = measure(() => {
ReactDOM.render(<Portal />, container);
});

expect(ReactPerf.getExclusive(measurements)).toEqual([{
key: 'Portal',
instanceCount: 1,
totalDuration: 6,
counts: {
ctor: 1,
componentDidMount: 1,
render: 1,
},
durations: {
ctor: 1,
// We want to exclude nested imperative ReactDOM.render() from lifecycle hook's own time.
// Otherwise it would artificially float to the top even though its exclusive time is small.
// This is how we get 4 as a number with the performanceNow() mock:
// - we capture the time we enter componentDidMount (n = 0)
// - we capture the time when we enter a nested flush (n = 1)
// - in the nested flush, we call it twice: before and after <Foo /> rendering. (n = 3)
// - we capture the time when we exit a nested flush (n = 4)
// - we capture the time we exit componentDidMount (n = 5)
// Time spent in componentDidMount = (5 - 0 - (4 - 3)) = 4.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not super happy about this. If it’s too brittle, I can probably do something nasty like call ReactDOM.render() 100 times and assert that the lifecycle own time is smaller than the inner element’s render time.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works for me.

componentDidMount: 4,
render: 1,
},
}, {
key: 'Foo',
instanceCount: 1,
totalDuration: 1,
counts: {
render: 1,
},
durations: {
render: 1,
},
}]);
});

it('warns once when using getMeasurementsSummaryMap', function() {
var measurements = measure(() => {});
spyOn(console, 'error');
Expand Down