-
Notifications
You must be signed in to change notification settings - Fork 47k
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
Add ProfileRoot component for collecting new time metrics #12745
Conversation
Does this include deep |
React: size: 🔺+0.9%, gzip: 🔺+0.6% ReactDOM: size: 🔺+0.8%, gzip: 🔺+1.0% Details of bundled changes.Comparing: a9abd27...e3a2562 react
react-dom
react-art
react-test-renderer
react-reconciler
react-is
react-native-renderer
Generated by 🚫 dangerJS |
Yes, anything that re-renders the root. There are a few tests that touch on this in |
Makes sense, just wanted to note that in external communication people generally don’t consider a deep rerender to “re-render a parent/root”. The fact that it’s structured this way is more of a Fiber architecture artefact. I agree it’s not docs but this tripped me a little 🙂 |
Understand and agree. Without this behavior though, it would not be possible to e.g. measure the cost of a <Application>
<ProfileRoot id="Header" callback="...">
<Header />
</ProfileRoot>
<Body />
</Application> Unless I've overlooked something. 😄 |
switch (finishedWork.tag) { | ||
case Profiler: { | ||
if (enableProfileModeMetrics) { | ||
finishedWork.pendingProps.onRender.call( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let onRender = finishedWork.memoizedProps.onRender;
onRender(...);
I think we should use memoizedProps.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious- why?
Lint complains about that form (no-useless-call
) but I can add an ignore rule if you prefer it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think @sebmarkbage means we need memoizedProps
, not pendingProps
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, haha. I misread that 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It’s two issues. We should use memoizedProps and avoid .call
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Already using memoized.
I'll remove .call
- but I'd like to learn more about why it's good to avoid it in this case. 😄
} | ||
|
||
// Never bail out early for Profilers. | ||
// We always want to re-measure the subtree. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems wrong. You want to measure the subtree only if there is pending work in the subtree. We shouldn't reconcile the children unless there are new props.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe my wording is wrong?
This is not forcing children to re-render. It's just measuring the "cost" of re-rendering. If the tree bails out b'c of e.g. sCU then the "actual" render time is small, which is good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But why are you reconciling the children? You should only do that if the props have changed. Otherwise you should skip reconciling, which is what bailoutOnAlreadyFinishedWork
does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha.
I'll take another look at this this evening, after dinner. 👍
|
||
// Reset actualTime after successful commit. | ||
// By default, we append to this time to account for errors and pauses. | ||
finishedWork.stateNode = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think Dan ran some performance tests that showed the stateNode
should always use an object or null, so the hidden class is monomorphic, for performance reasons.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh? cc @gaearon
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not ideal but since we're already doing this for context I guess this doesn't make it worse.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's easy enough to add a wrapper object if that would be better in any way. (I actually had one earlier and removed it last minute.)
@@ -404,6 +421,20 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>( | |||
renderExpirationTime: ExpirationTime, | |||
): Fiber | null { | |||
const newProps = workInProgress.pendingProps; | |||
|
|||
if (enableProfileModeMetrics) { | |||
if (workInProgress.mode & ProfileMode) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you move this resetExpirationTime
in the scheduler? That's where we "bubble up" expiration time, which is conceptually similar.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. No strong preference.
@@ -451,6 +468,12 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>( | |||
commitAttachRef(nextEffect); | |||
} | |||
|
|||
if (enableProfileModeMetrics) { | |||
if (effectTag & CommitProfile) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you reuse the Update
effect type? I don't think we need a new tag here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Profiler is the only type of work that might have this effect, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm. Maybe. Not obvious to me why that would be an improvement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our comments got crossed.
Profiler is the only type of work that might have this effect, right?
Yes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're adding an extra check to every single unit of work. We already check for the Update
effect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
@@ -216,6 +220,12 @@ export default function<C, CX>( | |||
} | |||
|
|||
function unwindInterruptedWork(interruptedWork: Fiber) { | |||
if (enableProfileModeMetrics) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as previous comment. Can we move this into the switch case for Profilers?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
I agree that's how it should work, I'm just saying that if we explain it as "when ProfileRoot re-rendered" many people won't realize that's what it means. Sorry I'm bikeshedding, let's discuss later in chat if this doesn't make sense :-) |
const onRender = finishedWork.memoizedProps.onRender; | ||
onRender( | ||
finishedWork.memoizedProps.id, | ||
finishedWork.alternate === null ? 'mount' : 'update', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use current === null
instead. We shouldn’t use alternate in most places. We tend to pass it so that we’re free to switch to a non-alternate based model in the future.
now = function() { | ||
return Date.now(); | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have this in the host config. Can we reuse that?
We should ideally avoid platform specific code in shared code like this. Some renderers will have different implementations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I used the host config function originally but removed it because my understanding was that we planned to change that in the future in a way that would not be compatible with this use case. cc @acdlite
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is not the case, I can change it back to using the host config function. I'll have to make a change two test renderer to support mocking that function but that should be easy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bvaughn I miscommunicated. I thought you were talking about the scheduler’s recalculateCurrentTime
function. HostConfig.now
is fine to use. My bad!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah! No problem. Thanks for clarifying. 😄 I'll revert that particular change then.
* If a fiber bails out (sCU false) then its "base" timer is cancelled and the fiber is not updated. | ||
*/ | ||
|
||
let baseStartTime: number | null = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In wasm this wouldn’t work. Some VMs may want to use a specific representation for numbers based on type info.
We shouldn’t mix null and numbers. Null is meant for missing objects, not missing numbers.
Let’s use 0 or -1 instead of null.
} | ||
|
||
export function markActualRenderTimeStarted(fiber: Fiber): void { | ||
fiber.stateNode -= now() - totalElapsedPauseTime; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let’s use a temp variable here so we don’t have to read the property again below.
if (enableProfileModeMetrics) { | ||
// Stop "base" render timer again (after the re-thrown error). | ||
stopBaseRenderTimer(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this include both renders in the time spent? Similarly for the double render flag, does it include both?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Both renders" meaning- if the side effect flag is enabled?
Yes. The tests disable the side effects flag for this reason.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this particular case it doesn’t matter I guess because it won’t update the base time since it errored?
In the normal case of double render I suppose we have to include it in the render time because otherwise the relative time to actual time will be misleading.
DEV is already slower than prod so the absolute numbers probably isn’t actionable regardless.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, in the event of an error, if the replayFailedUnitOfWorkWithInvokeGuardedCallback
flag is enabled, then when replayUnitOfWork
is called- it also stops (and discards) the base time. I think this is right, because that time is kind of meaningless.
@@ -645,6 +660,12 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>( | |||
} | |||
} | |||
|
|||
if (__DEV__) { | |||
if (enableProfileModeMetrics) { | |||
checkActualRenderTimeStackEmpty(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this just check that the stack is empty? Aren’t we already doing that? If we need an extra safety check, shouldn’t this just be a generic check of the stack, not specifically to the profiler?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a DEV-only sanity check to make sure we don't forget to pop anything from the timing stack.
We do the same DEV-only check with the context stack in scheduler.
@@ -702,6 +723,19 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>( | |||
child = child.sibling; | |||
} | |||
workInProgress.expirationTime = newExpirationTime; | |||
|
|||
if (enableProfileModeMetrics) { | |||
if (workInProgress.mode & ProfileMode) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let’s do the expiration time bubbling in here too. You can duplicate that code.
Let’s only do the existing path if we are not on profile mode.
That lets us reuse the same iteration instead of two iterations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you're saying:
if (enableProfileModeMetrics) {
if (workInProgress.mode & ProfileMode) {
// Bubble both times
} else {
// Bubble only expiration time
} else {
// Bubble only expiration time
}
if (enableProfileModeMetrics) { | ||
// If we didn't finish, pause the "actual" render timer. | ||
// We'll restart it when we resume work. | ||
if (nextUnitOfWork !== null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we did finish, can we always just call pause so we don’t need the check? Simplifies things a bit. pauseActualRenderTimerIfRunning is fast anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I...think so.
next = beginWork(current, workInProgress, nextRenderExpirationTime); | ||
|
||
// Update "base" time if the render wasn't bailed out on. | ||
if (isBaseRenderTimerRunning()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a clever way to model the bailout problem. Nice.
|
||
// Update "base" time if the render wasn't bailed out on. | ||
if (isBaseRenderTimerRunning()) { | ||
workInProgress.selfBaseTime = getElapsedBaseRenderTime(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because performance.now is throttled in browsers and many components are pretty fast, I wouldn't be surprised if this is actually zero or one most of the time. Either way, when we add them up, it'll be misleading. :(
Not sure how to solve that. Maybe eventually performance.now will be accurate again. At least it'll be better on RN.
if (enableProfileModeMetrics) { | ||
resumeActualRenderTimerIfPaused(); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you move the user timing block above findHighestPriorityRoot so that it is next to this one? Since they do the same thing, it's nice to have them colocated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moving that one up would break things, since findHighestPriorityRoot()
sets nextFlushedExpirationTime
. I can move this one down though.
if (enableProfileModeMetrics) { | ||
// If we didn't finish, pause the "actual" render timer. | ||
// We'll restart it when we resume work. | ||
if (nextUnitOfWork !== null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same question as the other spot. Is this check needed or can we just always call pauseActualRenderTimerIfRunning?
* It is paused (and accumulated) in the event of an interruption or an aborted render. | ||
*/ | ||
|
||
const {checkThatStackIsEmpty, createCursor, push, pop} = ReactFiberStack(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should never create new fiber stacks anywhere except once.
We should reuse the one from here:
react/packages/react-reconciler/src/ReactFiberScheduler.js
Lines 161 to 164 in b548b3c
const stack = ReactFiberStack(); | |
const hostContext = ReactFiberHostContext(config, stack); | |
const legacyContext = ReactFiberLegacyContext(stack); | |
const newContext = ReactFiberNewContext(stack); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting!
I avoided doing this initially because:
- The existing stack is complete context-centric, so I wasn't sure if it was appropriate to intermingle timing values. (I was also concerned about adding complexity in terms of push/pop timing when adding more hooks into the existing stack.)
- Re-using the existing stack cursor makes the profiler functions more awkward to use (since it requires initializing the module explicitly, and passing the profiler to the begin/unwind/complete modules explicitly).
- The stack seemed like a lightweight enough structure that I didn't think the cost of creating a new one for profiling mode (not production mode) was a big concern.
That being said, I'll make this change 👍
|
||
const {checkThatStackIsEmpty, createCursor, push, pop} = ReactFiberStack(); | ||
|
||
let stackCursor: StackCursor<Fiber> = createCursor(null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like very odd types. I'm not sure how Flow lets you do this.
You're passing a default argument for null
which is not compatible with the type Fiber
which you're assigning to the StackCursor.
Then later you actually pass a number
to it. So it's three way inconsistent.
I think it's not noticed because you never actually use the values stored in this stack.
@@ -1093,6 +1132,9 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>( | |||
case ContextProvider: | |||
pushProvider(workInProgress); | |||
break; | |||
case Profiler: | |||
markActualRenderTimeStarted(workInProgress); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be wrapped in if (enableProfileModeMetrics)
?
// Let the "complete" phase know to stop the timer, | ||
// And the scheduler to record the measured time. | ||
workInProgress.effectTag |= Update; | ||
} else if (workInProgress.memoizedProps === nextProps) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If enableProfileModeMetrics is enabled, we should still bailout on equality, right? Seems odd to disable that optimization.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, good catch. This should have been two ifs rather than an if/else
@@ -911,6 +959,14 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>( | |||
while (nextUnitOfWork !== null && !shouldYield()) { | |||
nextUnitOfWork = performUnitOfWork(nextUnitOfWork); | |||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Running the base timer for everything in the tree regardless of if this fiber is part of a profiled tree or not makes this a cost paid by everyone, whether you use the feature or not. This is probably fine for now since it's off by default.
If we want to turn it on my default, then we should probably move the startBaseRenderTimer
here. That way we can start a work loop that doesn't have a startBaseRenderTimer
call in it, unless the first fiber is already in a profiled tree. Then when we hit a Profiler fiber, we can start another work loop that has the startBaseRenderTimer
call in it.
We can do that later though. No need to do it in this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There’s something odd about not resetting the pause time. The other issue is that you don’t use the stack.
I suspect they’re related. Nested profilers will have issues.
Let’s talk through the scenarios.
if (enableProfileModeMetrics) { | ||
// Stop "base" render timer again (after the re-thrown error). | ||
stopBaseRenderTimer(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this particular case it doesn’t matter I guess because it won’t update the base time since it errored?
In the normal case of double render I suppose we have to include it in the render time because otherwise the relative time to actual time will be misleading.
DEV is already slower than prod so the absolute numbers probably isn’t actionable regardless.
@@ -150,6 +153,10 @@ export type Fiber = {| | |||
// memory if we need to. | |||
alternate: Fiber | null, | |||
|
|||
// Profiling metrics | |||
selfBaseTime: number | null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same thing as the other thing. We should keep this as number only. Use a number like 0 or -1 if you need to indicate missing value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just bad Flow typing on my part. I actually do initialize both to 0 below. My bad.
} | ||
} | ||
|
||
return baseStartTime === -1 ? 0 : now() - baseStartTime; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This baseStartTime === -1 is an error anyway we don’t have to do this check at runtime. If this was all inlined in the same function the compiler could maybe know that but it is broken apart so it might not.
That’s a perfect example why I prefer to avoid abstractions like these functions, and moving to other modules, because it makes it harder to see that you’re actually doing the same comparison twice.
If they were together in the same function it would be obvious.
You could refactor this to pass the fiber instead and do the condition and update in the same function. Like updateBaseTime(workInProgress).
|
||
let stackCursor: StackCursor<Fiber> = createCursor(null); | ||
let timerPausedAt: number = 0; | ||
let totalElapsedPauseTime: number = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This only increases but never gets reset. That seems odd. At some point we need to reset it right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we? Why? This time is accounted for in both the start and stop time methods.
I guess I can reset it in commitRoot
if you think it's best.
For nested profilers I guess the semantics are not really clear. There are two possible semantics: a) We include the time of the nested profiler in both the base time and actual time of the nested profiler. This seems intuitive to me but is also misleading since when you add up the time of several profilers they won’t be representative of the time if the tree. b) The parent time include only the time of the children that are not nested in other profilers. The base time that bubbles up from a profiler would be zero. I think with this strategy we won’t need a stack which is nice. Not sure what is more useful. You can infer some data if you have tree info in the id. So I guess it’s more about when you don’t want to have tree info in the id so that they accumulate across usages in different trees. |
I believe I have made all of the edits suggested above. Each one is a single commit, so hopefully it will be easy to review. |
I don't think this is actually an issue, but let's talk about it.
I have tests for nested profilers. Can you think of cases I'm not testing? I'd love to improve the tests.
WRT nested profilers, I think option A makes the most sense (and is the behavior my test cases currently verify). Option B seems like it would be too easy for e.g. on person to mess up another person's timing metrics unintentionally by adding a profiler at a lower level in the tree. |
@@ -226,7 +226,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>( | |||
const nextProps = workInProgress.pendingProps; | |||
if (enableProfileModeMetrics) { | |||
// Start render timer here and push start time onto queue | |||
markActualRenderTimeStarted(workInProgress); | |||
markActualRenderTimeStarted(workInProgress, now); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should pass this when we initial the module instead of an argument, so that it can be statically known which function is called. This will be a lot easier when we move to static injection instead of dynamic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. Duh, me.
} | ||
workInProgress.expirationTime = newExpirationTime; | ||
|
||
// (And "base" render timers if that feature flag is enabled) | ||
if (enableProfileModeMetrics) { | ||
if (workInProgress.mode & ProfileMode) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you change this to enableProfileModeMetrics && (workInProgress.mode & ProfileMode)
you only need one else
block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But then we won't strip the code when Disregard. I'm silly.enableProfileModeMetrics
is false. I assumed this was more important.
import type {StackCursor, Stack} from './ReactFiberStack'; | ||
|
||
import warning from 'fbjs/lib/warning'; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This whole module needs to be wrapped in a closure and initialized because it is stateful. Otherwise these timers will be shared with other renderers that could be rendering interleaved with this one.
That will also let you get a static reference to now
instead of passing it as an argument.
Doing that might however mess up closure compiler and lead everything in here to not be inlined which would be bad for file size, compile perf and possibly runtime perf. Who knows?
We need to move to static host configs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Otherwise these timers will be shared with other renderers that could be rendering interleaved with this one.
D'oh. Good point.
I always forget about this use case.
@@ -607,7 +597,12 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>( | |||
case Mode: | |||
return null; | |||
case Profiler: | |||
updateProfiler(workInProgress); | |||
if (enableProfileModeMetrics) { | |||
((actualRenderTimer: any): ActualRenderTimer).recordElapsedActualRenderTime( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Destructure these at the top like we do with everything else so that we don't have to look up this property dynamically every time. Hopefully this also lets us inline this call. (We should confirm whether it does.)
selfBaseTime: number, | ||
treeBaseTime: number, | ||
selfBaseTime?: number, | ||
treeBaseTime?: number, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is wrong. We should always assign them and they should always a consistent type value. E.g. 0.
Making them optional is a huge no, no since it might break the hidden class of the fiber.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't sure how to type them- given that we only assign these values if the feature flag is enabled. (We don't want to add them to every fiber if the flag isn't even enabled- right?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be clear, they are always either on or off. It's based on the feature flag. This will not break the hidden class of a fiber, since this is a bundle-wide thing.
if (enableProfileModeMetrics) {
this.selfBaseTime = 0;
this.treeBaseTime = 0;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm. I see. I suppose we do that with DEV only too. Seems odd that Flow doesn't force you to check for their existence everywhere then? We shouldn't need to check if they're undefined/null but seems like Flow would want us to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. I expected that also.
resumeActualRenderTimerIfPaused(now: Now): void, | ||
}; | ||
|
||
export function createActualRenderTimer(stack: Stack): ActualRenderTimer { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry didn't see that you wrapped this in a later commit. This needs to also include the base time to be safe.
You can pass now
to this instead of each function.
@@ -88,8 +93,9 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>( | |||
hydrationContext: HydrationContext<C, CX>, | |||
scheduleWork: (fiber: Fiber, expirationTime: ExpirationTime) => void, | |||
computeExpirationForFiber: (fiber: Fiber) => ExpirationTime, | |||
actualRenderTimer: ActualRenderTimer | null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's always pass it the module and assume that the compiler will strip it all out. We do assume that everywhere else, whether it does or not.
Add a new component type, Profiler, that can be used to collect new render time metrics. Since this is a new, experimental API, it will be exported as React.unstable_Profiler initially. Most of the functionality for this component has been added behind a feature flag, enableProfileModeMetrics. When the feature flag is disabled, the component will just render its children with no additional behavior. When the flag is enabled, React will also collect timing information and pass it to the onRender function (as described below).
Add a new component type,
Profiler
, that can be used to collect new render time metrics. Since this is a new, experimental API, it will be exported asReact.unstable_Profiler
initially.Most of the functionality for this component has been added behind a feature flag,
enableProfileModeMetrics
. When the feature flag is disabled, the component will just render its children with no additional behavior. When the flag is enabled, React will also collect timing information and pass it to theonRender
function (as described below).This flag will be enabled for the DEV bundle in the future. A special profiling+production build will likely be created as well, in order to use these new metrics in production mode without impacting existing applications.
How is it used?
Profiler
can be declared anywhere within a React tree and can be nested to measure multiple components within the same tree.The
onRender
callback is called each time the root renders. It receives the following parameters:id: string
- Theid
value of theProfiler
tag that was measured.phase: string
- Either "mount" or "update" (depending on whether this root was newly mounted or has just been updated).actualTime: number
- Time spent rendering theProfiler
and its descendants for the most recent "mount" or "update" render. 1baseTime: number
- Duration of the most recentrender
time for each individual component within theProfiler
tree. 11: See below for more detailed information about what this time represents.
Timing metrics
Here is a review of the types of timing React is now capable of reporting:
User Timing API
Measures start/stop times for each component lifecycle.
“Actual” render time (new)
Time spent rendering the
Profiler
and its descendants for the most recent render/update.<Profiler>
Profiler
onlyProfiler
is re-renderedshouldComponentUpdate
for memoization?“Base” render time (new)
Duration of the most recent
render
time for each individual component within theProfiler
tree.<Profiler>
Profiler
onlyProfiler
component.shouldComponentUpdate
Profiler
during “complete” phaseProfiler
(not for individual fibers)Profiler
is re-rendered2: Until "resume" behavior is implemented, interruptions will not accumulate time.
TODO
getComponentName
(so flame graph labels will appear correctly)ReactIs
toJSON
andtoTree
methods