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

Profiler integration with interaction-tracking package #13253

Merged

Conversation

bvaughn
Copy link
Contributor

@bvaughn bvaughn commented Jul 23, 2018

TODO before merge

  • Land interaction-tracking/subscribers target via PR Add interaction-tracking/subscriptions #13426
  • Verify that React supports multi-subscribers via interaction-tracking/subscribers
  • Fix the build process so that the interaction-tracking imports/requires are stripped if the enableInteractionTracking feature flag is not enabled. Rollup is not currently removing the unused require. This will be handle via a follow-up task. See this comment for more context.

Implementation details

For the purposes of this PR, I have created a standalone package (interaction-tracking) that events can be registered with. Usage of this package is optional, and it has no dependencies on React. We may not end up using this implementation, but will probably use something that's conceptually similar.

Scheduling work for an interaction

Profiling builds of React track when renders are caused by an interaction(s). React does this by asking the interaction-tracking library if it's currently tracking anything when new work is scheduled. (This is done inside of scheduleWork and scheduleCallbackWithExpirationTime.) When there are active interactions, React stores them– along with the expiration time they're being scheduled with– on the FiberRoot in a pendingInteractionMap Map.

Associating an interaction with async work

When React begins working on a new batch of work (in performWorkOnRoot) it scans the pendingInteractionMap Map and filters by the current expiration time in order to determine which interactions scheduled the current work. This filtered list of interactions is stored on the root as memoizedInteractions.

Before calling any user code (in commitRoot and renderRoot) React temporarily restores these cached interactions using an advanced part of the interaction-tracking package API. This ensures that new async work (cascading work) that's created will also be associated with the original interaction(s).

React also manges notifying any interaction subscribers when new work has been started/stopped.

Notifying Profiler callbacks

When committing a Profiler, React reads the memoizedInteractions Set from the current root and passes the interactions along to the onRender callback.

DevTools integration

When the DevTools onCommitRoot is called, it reads the memoizedInteractions Set from the committed root in order to determine if any interactions were processed.

Open questions

  • With this implementation, we are storing interactions globally (at the root level) for simplicity. This means that we won't have the ability to scope an interaction with a part of the tree that it occurred in, only that it was related to some part of the tree during the current render. This may be sufficient. If it turns out not to be, and we need fine grained interaction tracking– then we would have to add a field to every Fiber to store the pending interactions when an update is scheduled. Then in begin/complete work, we'd have to start/stop (push/pop) continuations to properly handle cascading updates and to ensure the right onRender params.
  • Are we comfortable adding a new dependency (interaction-tracking) to all renderers (e.g. react-dom, react-native, react-test-renderer, etc)?

@pull-bot
Copy link

pull-bot commented Jul 23, 2018

ReactDOM: size: 🔺+0.1%, gzip: 🔺+0.1%

Details of bundled changes.

Comparing: 2967ebd...e61eb6c

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.development.js +1.6% +1.5% 649.89 KB 660.24 KB 152.69 KB 154.94 KB UMD_DEV
react-dom.production.min.js 🔺+0.1% 🔺+0.1% 95.27 KB 95.36 KB 31.15 KB 31.18 KB UMD_PROD
react-dom.development.js +1.6% +1.5% 646.02 KB 656.33 KB 151.58 KB 153.81 KB NODE_DEV
react-dom.production.min.js 0.0% 🔺+0.1% 95.26 KB 95.3 KB 30.89 KB 30.91 KB NODE_PROD
ReactDOM-dev.js +1.6% +1.6% 653.12 KB 663.69 KB 149.58 KB 151.92 KB FB_WWW_DEV
ReactDOM-prod.js -0.1% 🔺+0.1% 287.93 KB 287.74 KB 53.27 KB 53.32 KB FB_WWW_PROD
react-dom.profiling.min.js +1.8% +0.7% 96.45 KB 98.18 KB 31.28 KB 31.51 KB NODE_PROFILING
ReactDOM-profiling.js +1.0% +1.4% 290.28 KB 293.15 KB 53.88 KB 54.64 KB FB_WWW_PROFILING

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-art.development.js +2.4% +2.4% 442.33 KB 452.83 KB 100.03 KB 102.41 KB UMD_DEV
react-art.production.min.js 0.0% 🔺+0.1% 86.62 KB 86.64 KB 26.73 KB 26.75 KB UMD_PROD
react-art.development.js +2.8% +2.9% 374.87 KB 385.38 KB 82.94 KB 85.32 KB NODE_DEV
react-art.production.min.js 0.0% 🔺+0.2% 51.61 KB 51.63 KB 16.11 KB 16.14 KB NODE_PROD
ReactART-dev.js +2.9% +3.2% 365.04 KB 375.79 KB 77.66 KB 80.13 KB FB_WWW_DEV
ReactART-prod.js 🔺+0.1% 🔺+0.2% 159.79 KB 159.89 KB 27.1 KB 27.14 KB FB_WWW_PROD

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-test-renderer.development.js +2.8% +2.8% 371.7 KB 382.05 KB 81.33 KB 83.58 KB UMD_DEV
react-test-renderer.production.min.js 🔺+0.2% 🔺+0.3% 50.73 KB 50.83 KB 15.5 KB 15.54 KB UMD_PROD
react-test-renderer.development.js +2.8% +2.8% 367.83 KB 378.14 KB 80.37 KB 82.61 KB NODE_DEV
react-test-renderer.production.min.js 🔺+0.1% 0.0% 50.43 KB 50.49 KB 15.35 KB 15.36 KB NODE_PROD
ReactTestRenderer-dev.js +2.8% +2.9% 372.69 KB 383.26 KB 79.33 KB 81.66 KB FB_WWW_DEV

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler.development.js +2.9% +2.9% 356.11 KB 366.42 KB 77.03 KB 79.28 KB NODE_DEV
react-reconciler.production.min.js 🔺+0.1% 🔺+0.3% 49.09 KB 49.13 KB 14.73 KB 14.77 KB NODE_PROD
react-reconciler-persistent.development.js +2.9% +3.0% 354.68 KB 364.99 KB 76.45 KB 78.71 KB NODE_DEV
react-reconciler-persistent.production.min.js 🔺+0.1% 🔺+0.3% 49.1 KB 49.14 KB 14.74 KB 14.78 KB NODE_PROD

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactNativeRenderer-dev.js +2.2% +2.2% 486.67 KB 497.24 KB 107.63 KB 109.95 KB RN_FB_DEV
ReactNativeRenderer-prod.js 🔺+0.1% 🔺+0.2% 214.58 KB 214.7 KB 37.41 KB 37.47 KB RN_FB_PROD
ReactNativeRenderer-dev.js +2.2% +2.2% 486.4 KB 496.98 KB 107.57 KB 109.89 KB RN_OSS_DEV
ReactNativeRenderer-prod.js 🔺+0.1% 🔺+0.2% 204.39 KB 204.52 KB 35.76 KB 35.82 KB RN_OSS_PROD
ReactFabric-dev.js +2.2% +2.2% 476.85 KB 487.43 KB 105.2 KB 107.54 KB RN_FB_DEV
ReactFabric-prod.js 🔺+0.1% 🔺+0.2% 196.81 KB 197.03 KB 34.27 KB 34.33 KB RN_FB_PROD
ReactFabric-dev.js +2.2% +2.2% 476.89 KB 487.46 KB 105.22 KB 107.55 KB RN_OSS_DEV
ReactFabric-prod.js 🔺+0.1% 🔺+0.2% 196.85 KB 197.06 KB 34.29 KB 34.34 KB RN_OSS_PROD
ReactNativeRenderer-profiling.js +0.1% +0.2% 207.82 KB 207.94 KB 36.42 KB 36.49 KB RN_OSS_PROFILING
ReactFabric-profiling.js +0.1% +0.2% 199.81 KB 200.03 KB 34.84 KB 34.9 KB RN_OSS_PROFILING
ReactNativeRenderer-profiling.js 0.0% +0.1% 217.99 KB 218.1 KB 38.07 KB 38.12 KB RN_FB_PROFILING
ReactFabric-profiling.js +0.1% +0.2% 199.77 KB 199.99 KB 34.82 KB 34.88 KB RN_FB_PROFILING

Generated by 🚫 dangerJS

@facebook facebook deleted a comment from facebook-github-bot Jul 26, 2018
@facebook facebook deleted a comment from facebook-github-bot Jul 26, 2018
@bvaughn bvaughn changed the title Added proof of concept interaction-tracking package POC Profiler integration with interaction-tracking package Jul 26, 2018
return;
}
if (__DEV__) {
invariant(
Copy link
Contributor

Choose a reason for hiding this comment

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

Oops?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, is our current convention just to throw explicitly?

Copy link
Contributor

Choose a reason for hiding this comment

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

Well maybe if this is going to be a separate package. But what I meant is we shouldn't have dev- or prod-only invariants.


let root = finishedWork;
while (root.return !== null) {
root = root.return;
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's pass this to commitWork instead

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I dig that.

onRender(
finishedWork.memoizedProps.id,
current === null ? 'mount' : 'update',
finishedWork.actualDuration,
finishedWork.treeBaseDuration,
finishedWork.actualStartTime,
getCommitTime(),
interactions === null ? [] : Array.from(interactions),
Copy link
Contributor

Choose a reason for hiding this comment

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

Why .from?

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh nvm, I see

// Secondly, this is how commitWork() will access them to pass them to any Profiler onRender() hooks.
// commitWork() can't use getInteractionsForExpirationTime() because the interactions have already been cleared.
// We can't wait to clear them out of the Map after commitWork() either, or we'll wipe out cascading updates.
finishedWork.stateNode.committedInteractions = interactions;
Copy link
Contributor

Choose a reason for hiding this comment

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

You can move this and the other assignment in createFiberRoot to the begin phase. Also let's call it memoizedInteractions instead? "Committed" is misleading because it hasn't committed yet at this point. It's more like memoizedProps and memoizedState.

@@ -1043,6 +1070,19 @@ function renderRoot(

const expirationTime = root.nextExpirationTimeToWorkOn;

let interactions = null;
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: This can go inside the if

@bvaughn bvaughn changed the title POC Profiler integration with interaction-tracking package Profiler integration with interaction-tracking package Aug 6, 2018
@bvaughn
Copy link
Contributor Author

bvaughn commented Aug 7, 2018

Chatted with Sebastian about how heavy-weight the implementation from the weekend has become. Here's some notes for myself about a possible alternative API that we'll need to discuss with web speed folks:

  • Rather than calling InteractionEmitter start and stop methods every frame React does work, React could start call start for an interaction once (when it's initially scheduled) and stop once (when the last update is committed for it).
    • To manage this, React would explicitly reference count async interactions by calling +1 any time work was scheduled for an interaction and -1 any time work is committed for the interaction.
  • Rather than re-calculating the set of interactions we're working on at the beginning of renderRoot and commitRoot– we could calculate it the first time we rendered and cache that value across frames until we later committed it.

@bvaughn bvaughn changed the title Profiler integration with interaction-tracking package Profiler integration with interaction-tracking package [WIP] Aug 8, 2018
@bvaughn
Copy link
Contributor Author

bvaughn commented Aug 10, 2018

Sebastian, Andrew, and I continued talking yesterday at some length and came up with a new proposed observer API documented here. I've just pushed this implementation, and tests, and will take a pass at updating the PR description now.

@bvaughn bvaughn requested a review from sebmarkbage August 10, 2018 21:20
if (enableInteractionTracking) {
((root: any): FiberRoot).interactionThreadID = getThreadID();
((root: any): FiberRoot).memoizedInteractions = new Set();
((root: any): FiberRoot).pendingInteractionMap = new Map();
Copy link
Collaborator

@gaearon gaearon Aug 14, 2018

Choose a reason for hiding this comment

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

Can adding these later affect hidden class? I wonder if that or the number of fields itself could mess with some optimizations. Seems like it's important that we try to make the profiling build as close to prod characteristics as possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm! I don't know. If so, I could move the definition of root within the if/else to ensure that we always added the same number of fields based on the feature flag.

const threadID =
committedExpirationTime * 1000 + root.interactionThreadID;
((__subscribers: any): Subscribers).forEach(observer =>
observer.onWorkStopped(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Related to my concerns in #13234 (comment), I think we need to be very confident in user-level code to call it from this place in the scheduler. Since it's not inside the render phase or one of protected places in the commit phase, there's a risk that any error in it (and folks who want to use it are humans and may make mistakes) can mess up the scheduler state and cause infinite loops etc.

Or we could wrap any user code call like this in a try/catch.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great point. Although I'm not sure what the expected error recover behavior really should be in this case. I'll take a stab at it– with some new tests– and we can discuss it from there.

// Wrap suspense callback so that the current interaction are preserved.
// Interaction threads are unique per root and expiration time.
const threadID =
renderExpirationTime * 1000 + root.interactionThreadID;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Will it break if I change the formula here but not in the other place it's used?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It will "break" in that the callbacks will receive a different thread ID and so any reporting done based on those callbacks would be inaccurate. I'll add a comment to each of these places noting that we shouldn't change one without changing the others. Good call!

@bvaughn bvaughn changed the title Profiler integration with interaction-tracking package [WIP] Profiler integration with interaction-tracking package Aug 15, 2018
finishedWork.treeBaseDuration,
finishedWork.actualStartTime,
getCommitTime(),
Array.from((root.memoizedInteractions: any)),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why pay the cost of cloning this data? Why not just pass the set and let the consumer decide?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

memoizedInteractions is a Set and the I initially proposed passing onRender an Array. We could change this so that the API passes a Set instead, then I wouldn't need to clone it.

Users could mutate it though, and I think a Set can't be object-frozen...?

Open to suggestions here 😄

interactionThreadID: getThreadID(),
memoizedInteractions: new Set(),
pendingInteractionMap: new Map(),
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

Good VM field hints. Nice.

@@ -338,7 +346,7 @@ function resetStack() {
nextUnitOfWork = null;
}

function commitAllHostEffects() {
function commitAllHostEffects(root: FiberRoot) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hm. Unfortunate to pass another arg just for this. @acdlite does this look right to you? Can we get this from somewhere else?

Copy link
Contributor

Choose a reason for hiding this comment

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

Should be able to use nextRoot, but I assume Brian has passed it as an argument to avoid another type check, since Flow doesn't know that it's not null.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The chain is ... -> performWorkOnRoot(root) -> completeRoot(root) -> commitRoot(root) -> commitAllHostEffects(root)

Why is passing root to this specific function a problem? 😄

Copy link
Collaborator

Choose a reason for hiding this comment

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

Some functions get inlined so those variables don't matter much.

Extra variables can be much worse than closures or property reads. Because if we tip over the number of variables used then the VM will try to guess which ones to pop out of the register and back to main memory.

The key here is that this variable isn't used by the mainline path here. Just the special case path of the profiler.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should be able to use nextRoot, but I assume Brian has passed it as an argument to avoid another type check, since Flow doesn't know that it's not null.

Maybe I'm missing something... nextRoot is actually null in many cases though.

renderRoot() sets nextRoot to null to indicate that there's no more work to be done for the current batch. This means that if performWorkOnRoot() then calls completeRoot() – the value will be null.

I assume this is why we've been passing around a root param to these other functions in the first place.

// correct value.
didTimeout: false,
});
} catch (error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Use try/finally instead. This is what it is for.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe catch is appropriate here, because I specifically want to reset the scheduledCallback only when an error occurs that prevents it from being processed.

// correct value.
didTimeout: false,
});
} catch (error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

try/finally

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same as above. (I think catch is right here.)

let prevInteractions: Set<Interaction> | null = null;
let committedInteractions: Array<
Interaction,
> | null = enableInteractionTrackingObserver ? [] : null;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Unconditionally allocating an array for every commit seems unfortunate.

if (enableInteractionTracking) {
// We're about to start new tracked work.
// Restore pending interactions so cascading work triggered during the render phase will be accounted for.
prevInteractions = ((__interactionsRef: any): InteractionsRef).current;
Copy link
Collaborator

Choose a reason for hiding this comment

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

All these any casts are too fragile. Let’s make sure that we cast this at the export type in one place.

// DO NOT CHANGE THIS VALUE without also updating other threadID values.
const threadID =
renderExpirationTime * 1000 + root.interactionThreadID;
onResolveOrReject = wrap(onResolveOrReject, threadID);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is not obvious to me. Wouldn’t there already be an interaction tracked for this expiration time? And these callbacks can’t do arbitrary things. They just schedule work on something that is already scheduled. So seems to me that this shouldn’t need to be wrapped.

@acdlite

Copy link
Contributor Author

@bvaughn bvaughn Aug 17, 2018

Choose a reason for hiding this comment

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

Yes, we're within an active interaction zone at this point. (That's why wrap works in the first place.)

The issue is that we attach onResolveOrReject to our thenable to resume work when the e.g. promise has finished loading. wrap ensures that we restore the right set of interactions at that time.

Without this, our interaction zone expires when React finishes this batch of work– and there's nothing to restore it when the thenable completes.

Copy link
Contributor

Choose a reason for hiding this comment

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

Without this, our interaction zone expires when React finishes this batch of work– and there's nothing to restore it when the thenable completes.

This is only an issue if the work times out, which we determine here: https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberScheduler.js#L1449

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, maybe I'm being dense, but I'm not sure I understand how this feedback applies.

@@ -1988,6 +2166,40 @@ function performWorkOnRoot(
'by a bug in React. Please file an issue.',
);

if (enableInteractionTracking) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

While everything below here makes sense. This is it the right place for it.

This gets called for every scheduled work (requestIdleCallback).

We only want to call this when we’re restarting work on a root. Not when we’re resuming.

@acdlite Can probably point to the right place to do this.

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

@bvaughn bvaughn Aug 22, 2018

Choose a reason for hiding this comment

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

Hm, I think this breaks the suspense case– (e.g. covered by test "tracks both the temporary placeholder and the finished render for an interaction")– since renderRoot only resets the stack once but commitRoot is potentially called multiple times. (This causes the interaction pending working "count" to be off.)

);

if (caughtError) {
throw caughtError;
Copy link
Collaborator

Choose a reason for hiding this comment

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

try/finally will just rethrow the error by default. You can remove the catch and this line.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right. This one's a bit tricky, but I'm manually catching and re-throwing to guard against an error in one of the onInteractionScheduledWorkCompleted callbacks– so that it doesn't block subsequent callbacks. (I'm iterating over a set of interactions and calling this callback for any that have completed.)

@bvaughn
Copy link
Contributor Author

bvaughn commented Aug 18, 2018

I'm about to force-push a cleaned up version of this branch. All of the original commits have been squashed and the branch has be rebased on master.

The PR responses made so far are in atomic commits so they should be easy to spot. I haven't made some of them (e.g. try/finally) because I think they're misunderstandings.

I mention the force push because it might clobber some of the comments Sebastian and I have left so far. (Sorry.)

@bvaughn bvaughn force-pushed the interaction-tracking-profiler-integration branch from f7693b3 to 956674a Compare August 18, 2018 03:22
@bvaughn bvaughn force-pushed the interaction-tracking-profiler-integration branch from 676e3ca to c9b6c87 Compare August 28, 2018 17:48
@bvaughn
Copy link
Contributor Author

bvaughn commented Aug 28, 2018

Andrew, Sebastian, and I chatted about adding a new interaction-tracking NPM dependency to renderers (e.g. react-dom, react-native, etc) and I think we're all okay with this.

The only downside is that Rollup DCE isn't stripping the unused require("interaction-tracking"); statement from the production bundle. It would be nice to have a hacky post-build step that strips this from the production bundle. I'll do this as a follow up PR– but we should probably try to get it in before release just to avoid adding an (unused) require statement to the production bundle.

@gaearon
Copy link
Collaborator

gaearon commented Aug 28, 2018

We should most definitely avoid this in production bundle. This should be a blocker for cutting a release. If you need help I’d be happy to look — just let me know.

* Remove interaction-tracking wrap() references from unwind work in favor of managing suspense/interaction continuations in the scheduler
* Moved the logic for calling work-started hook from performWorkOnRoot() to renderRoot()
@bvaughn
Copy link
Contributor Author

bvaughn commented Aug 28, 2018

Okay @acdlite, I think I've addressed both the unwind work / wrap feedback and the renderRoot/performWorkOnRoot feedback on 235642b

@bvaughn
Copy link
Contributor Author

bvaughn commented Aug 28, 2018

We should most definitely avoid this in production bundle. This should be a blocker for cutting a release. If you need help I’d be happy to look — just let me know.

Hey @gaearon 👋 Great! Let's chat. It's possible we're thinking about slightly different things– but if not I'd love to get your input. 😄

FWIW, I was planning to add a post-build step that strips the unused import out of the production bundle– but as a follow up PR.

@bvaughn
Copy link
Contributor Author

bvaughn commented Aug 28, 2018

@acdlite I renamed and replaced one of the freeze-interaction flags with b308fee

@@ -1929,9 +1925,9 @@ function onTimeout(root, finishedWork, suspendedExpirationTime) {
if (enableInteractionTracking) {
// Don't update pending interaction counts for suspense timeouts,
// Because we know we still need to do more work in this case.
freezeInteractionCount = true;
suspenseDTimeout = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

Typo?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Subtle nod to DJT

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nah I just can't type for shit

@bvaughn bvaughn force-pushed the interaction-tracking-profiler-integration branch from b308fee to a173f36 Compare August 28, 2018 22:40
@@ -217,7 +217,7 @@ function throwException(
: renderExpirationTime;

// Attach a listener to the promise to "ping" the root and retry.
const onResolveOrReject = retrySuspendedRoot.bind(
let onResolveOrReject = retrySuspendedRoot.bind(
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: change back to const plz

Copy link
Contributor

@acdlite acdlite left a comment

Choose a reason for hiding this comment

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

yay

@acdlite
Copy link
Contributor

acdlite commented Aug 29, 2018

Seems fine to fix the packaging issue in a follow up (before we release)

@bvaughn
Copy link
Contributor Author

bvaughn commented Aug 29, 2018

Thanks for the thorough review and feedback folks! 🙇

@bvaughn bvaughn merged commit 6e4f7c7 into facebook:master Aug 29, 2018
@bvaughn bvaughn deleted the interaction-tracking-profiler-integration branch August 29, 2018 01:58
@bvaughn bvaughn mentioned this pull request Aug 29, 2018
3 tasks
nextScheduledRoot: null,
}: BaseFiberRootProperties);
}

Copy link
Contributor

@NE-SmallTown NE-SmallTown Sep 1, 2018

Choose a reason for hiding this comment

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

@bvaughn Can I ask that why we do the repetitive logic, i.e. only interactionThreadID, memoizedInteractions and pendingInteractionMap is related to enableInteractionTracking, we don't need to copy the remaining fields here, why not just do something like if (enableInteractionTracking) { root.interactionThreadID === xxx} or if (enableInteractionTracking) { root === { ...root, interactionThreadID: xxx } }( or use Object.assign here).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We do it for performance since this code is really "hot" (it runs a lot). Seb would be a much better person to answer this question but I'll take a stab at it. 😄

JavaScript VM makes property lookup faster for objects by creating a class in the native-code layer based on the "shape" of the object (e.g. what keys it has). This is called the "hidden class", and it's a runtime optimization. If the VM isn't sure about the object's shape, it uses a slower (hash table) method to lookup properties. When fields are added/deleted after an object is created, it breaks the VM's "hidden class" assumptions and de-opts to hash table lookup.

Probably worth remembering that enableInteractionTracking is a compile time flag so this if/else won't exist in the built bundle. 😄

Copy link
Contributor

@NE-SmallTown NE-SmallTown Sep 1, 2018

Choose a reason for hiding this comment

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

jetoneza pushed a commit to jetoneza/react that referenced this pull request Jan 23, 2019
* Updated suspense fixture to use new interaction-tracking API

* Integrated Profiler API with interaction-tracking API (and added tests)

* Pass interaction Set (rather than Array) to Profiler onRender callback

* Removed some :any casts for enableInteractionTracking fields in FiberRoot type

* Refactored threadID calculation into a helper method

* Errors thrown by interaction tracking hooks use unhandledError to rethrow more safely.
Reverted try/finally change to ReactTestRendererScheduling

* Added a $FlowFixMe above the FiberRoot :any cast

* Reduce overhead from calling work-started hook

* Remove interaction-tracking wrap() references from unwind work in favor of managing suspense/interaction continuations in the scheduler
* Moved the logic for calling work-started hook from performWorkOnRoot() to renderRoot()

* Add interaction-tracking to bundle externals. Set feature flag to __PROFILE__

* Renamed the freezeInteractionCount flag and replaced one use-case with a method param

* let -> const

* Updated suspense fixture to handle recent API changes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants