@samitouri / QOS-React / commits / 24f824250f

DevTools Profiler: Improve how empty commits are filtered (#17771)

Previously, DevTools filtered empty commits on the backend, while profiling, through the use of a bailout heuristic that React currently happens to use. This approach was brittle and may have exacerbated the long-standing Profiler bug #16446. This PR removes that heuristic and adds as a post-processing filtering pass instead. This removes the coupling between DevTools and a React internal implementation detail that may change. I believe DevTools has two choices of criteria for this filtering: * Filter commits that have no actual duration metadata. * Filter commits that have no recorded operations (no mutations to the tree, no changed tree base durations). I chose the first option, filtering by commits that have no reported metadata. It will miss an edge case, e.g. , but we would have nothing meaningful to show in the Profiler for those cases anyway. (This particular edge case is why one of the snapshots changed with this commit.) The second option, filtering by recorded operations, could potentially miss a more important edge case: where a component *did* render, but its didn't change. (In that event, there would be no operations to send.)

Brian Vaughn committed Jan 4, 2020 at 16:50 UTC 24f824250fde6418569222f6e33b35ba9c1f1f46
3 files changed +54 -92
packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap
+4 -48
@@ -1500,17 +1500,7 @@ Object {
1500
1501 exports[`ProfilingCache should collect data for each root (including ones added or mounted after profiling started): Data for root Parent 3`] = `
1502 Object {
1503 - "commitData": Array [
1504 - Object {
1505 - "changeDescriptions": Map {},
1506 - "duration": 0,
1507 - "fiberActualDurations": Map {},
1508 - "fiberSelfDurations": Map {},
1509 - "interactionIDs": Array [],
1510 - "priorityLevel": "Immediate",
1511 - "timestamp": 34,
1512 - },
1513 - ],
1503 + "commitData": Array [],
1504 "displayName": "Parent",
1505 "initialTreeBaseDurations": Map {
1506 6 => 11,
@@ -1520,19 +1510,7 @@ Object {
1510 },
1511 "interactionCommits": Map {},
1512 "interactions": Map {},
1523 - "operations": Array [
1524 - Array [
1525 - 1,
1526 - 6,
1527 - 0,
1528 - 2,
1529 - 4,
1530 - 9,
1531 - 8,
1532 - 7,
1533 - 6,
1534 - ],
1535 - ],
1513 + "operations": Array [],
1514 "rootID": 6,
1515 "snapshots": Map {
1516 6 => Object {
@@ -2066,17 +2044,7 @@ Object {
2044 "snapshots": Array [],
2045 },
2046 Object {
2069 - "commitData": Array [
2070 - Object {
2071 - "changeDescriptions": Array [],
2072 - "duration": 0,
2073 - "fiberActualDurations": Array [],
2074 - "fiberSelfDurations": Array [],
2075 - "interactionIDs": Array [],
2076 - "priorityLevel": "Immediate",
2077 - "timestamp": 34,
2078 - },
2079 - ],
2047 + "commitData": Array [],
2048 "displayName": "Parent",
2049 "initialTreeBaseDurations": Array [
2050 Array [
@@ -2098,19 +2066,7 @@ Object {
2066 ],
2067 "interactionCommits": Array [],
2068 "interactions": Array [],
2101 - "operations": Array [
2102 - Array [
2103 - 1,
2104 - 6,
2105 - 0,
2106 - 2,
2107 - 4,
2108 - 9,
2109 - 8,
2110 - 7,
2111 - 6,
2112 - ],
2113 - ],
2069 + "operations": Array [],
2070 "rootID": 6,
2071 "snapshots": Array [
2072 Array [
packages/react-devtools-shared/src/backend/renderer.js
+7 -29
@@ -1023,28 +1023,13 @@ export function attach(
1023 ) {
1024 // If we aren't profiling, we can just bail out here.
1025 // No use sending an empty update over the bridge.
1026 - if (!isProfiling) {
1027 - return;
1028 - }
1029 -
1030 - const current = root.current;
1031 - const alternate = current.alternate;
1032 -
1033 - // Certain types of updates bail out at the root without doing any actual render work.
1034 - // React should probably not call the DevTools commit hook in this case,
1035 - // but if it does- we can detect it and filter them out from the profiler.
1036 - // NOTE: Keep this logic in sync with the one in handleCommitFiberRoot()
1037 - const didBailoutAtRoot =
1038 - alternate !== null &&
1039 - alternate.expirationTime === 0 &&
1040 - alternate.childExpirationTime === 0;
1041 -
1026 + //
1027 // The Profiler stores metadata for each commit and reconstructs the app tree per commit using:
1028 // (1) an initial tree snapshot and
1029 // (2) the operations array for each commit
1030 // Because of this, it's important that the operations and metadata arrays align,
1046 - // So the logic that skips metadata for bailout commits should also apply to filter operations.
1047 - if (didBailoutAtRoot) {
1031 + // So it's important not to ommit even empty operations while profiing is active.
1032 + if (!isProfiling) {
1033 return;
1034 }
1035 }
@@ -1388,6 +1373,8 @@ export function attach(
1373 if (isProfiling) {
1374 const {alternate} = fiber;
1375
1376 + // It's important to update treeBaseDuration even if the current Fiber did not render,
1377 + // becuase it's possible that one of its descednants did.
1378 if (
1379 alternate == null ||
1380 treeBaseDuration !== alternate.treeBaseDuration
@@ -1775,15 +1762,6 @@ export function attach(
1762 const current = root.current;
1763 const alternate = current.alternate;
1764
1778 - // Certain types of updates bail out at the root without doing any actual render work.
1779 - // React should probably not call the DevTools commit hook in this case,
1780 - // but if it does- we can detect it and filter them out from the profiler.
1781 - // NOTE: Keep this logic in sync with the one in flushPendingEvents()
1782 - const didBailoutAtRoot =
1783 - alternate !== null &&
1784 - alternate.expirationTime === 0 &&
1785 - alternate.childExpirationTime === 0;
1786 -
1765 currentRootID = getFiberID(getPrimaryFiber(current));
1766
1767 // Before the traversals, remember to start tracking
@@ -1800,7 +1778,7 @@ export function attach(
1778 // where some v16 renderers support profiling and others don't.
1779 const isProfilingSupported = root.memoizedInteractions != null;
1780
1803 - if (isProfiling && isProfilingSupported && !didBailoutAtRoot) {
1781 + if (isProfiling && isProfilingSupported) {
1782 // If profiling is active, store commit time and duration, and the current interactions.
1783 // The frontend may request this information after profiling has stopped.
1784 currentCommitProfilingMetadata = {
@@ -1844,7 +1822,7 @@ export function attach(
1822 mountFiberRecursively(current, null, false, false);
1823 }
1824
1847 - if (isProfiling && isProfilingSupported && !didBailoutAtRoot) {
1825 + if (isProfiling && isProfilingSupported) {
1826 const commitProfilingMetadata = ((rootToCommitProfilingMetadataMap: any): CommitProfilingMetadataMap).get(
1827 currentRootID,
1828 );
packages/react-devtools-shared/src/devtools/views/Profiler/utils.js
+43 -15
@@ -60,26 +60,54 @@ export function prepareProfilingDataFrontendFromBackendAndStore(
60 throw Error(`Could not find profiling snapshots for root ${rootID}`);
61 }
62
63 + const filteredCommitData = [];
64 + const filteredOperations = [];
65 +
66 + // Filter empty commits from the profiler data.
67 + // It is very important to keep operations and commit data arrays perfect in sync.
68 + // So we must use the same criteria to filter both.
69 + // If these two arrays were to get out of sync, the profiler would runtime error.
70 + // We choose to filter on commit metadata, rather than the operations array,
71 + // because the latter may have false positives,
72 + // (e.g. a commit that re-rendered a component with the same treeBaseDuration as before).
73 + commitData.forEach((commitDataBackend, commitIndex) => {
74 + if (commitDataBackend.fiberActualDurations.length > 0) {
75 + filteredCommitData.push({
76 + changeDescriptions:
77 + commitDataBackend.changeDescriptions != null
78 + ? new Map(commitDataBackend.changeDescriptions)
79 + : null,
80 + duration: commitDataBackend.duration,
81 + fiberActualDurations: new Map(
82 + commitDataBackend.fiberActualDurations,
83 + ),
84 + fiberSelfDurations: new Map(commitDataBackend.fiberSelfDurations),
85 + interactionIDs: commitDataBackend.interactionIDs,
86 + priorityLevel: commitDataBackend.priorityLevel,
87 + timestamp: commitDataBackend.timestamp,
88 + });
89 + filteredOperations.push(operations[commitIndex]);
90 + }
91 + });
92 +
93 dataForRoots.set(rootID, {
64 - commitData: commitData.map((commitDataBackend, commitIndex) => ({
65 - changeDescriptions:
66 - commitDataBackend.changeDescriptions != null
67 - ? new Map(commitDataBackend.changeDescriptions)
68 - : null,
69 - duration: commitDataBackend.duration,
70 - fiberActualDurations: new Map(
71 - commitDataBackend.fiberActualDurations,
72 - ),
73 - fiberSelfDurations: new Map(commitDataBackend.fiberSelfDurations),
74 - interactionIDs: commitDataBackend.interactionIDs,
75 - priorityLevel: commitDataBackend.priorityLevel,
76 - timestamp: commitDataBackend.timestamp,
77 - })),
94 + commitData: filteredCommitData,
95 + displayName,
96 + initialTreeBaseDurations: new Map(initialTreeBaseDurations),
97 + interactionCommits: new Map(interactionCommits),
98 + interactions: new Map(interactions),
99 + operations: filteredOperations,
100 + rootID,
101 + snapshots,
102 + });
103 +
104 + dataForRoots.set(rootID, {
105 + commitData: filteredCommitData,
106 displayName,
107 initialTreeBaseDurations: new Map(initialTreeBaseDurations),
108 interactionCommits: new Map(interactionCommits),
109 interactions: new Map(interactions),
82 - operations,
110 + operations: filteredOperations,
111 rootID,
112 snapshots,
113 });