Skip to content

Commit 24f8242

Browse files
author
Brian Vaughn
authored
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.)
1 parent edeea07 commit 24f8242

File tree

3 files changed

+54
-92
lines changed

3 files changed

+54
-92
lines changed

packages/react-devtools-shared/src/__tests__/__snapshots__/profilingCache-test.js.snap

Lines changed: 4 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1500,17 +1500,7 @@ Object {
15001500

15011501
exports[`ProfilingCache should collect data for each root (including ones added or mounted after profiling started): Data for root Parent 3`] = `
15021502
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 [],
15141504
"displayName": "Parent",
15151505
"initialTreeBaseDurations": Map {
15161506
6 => 11,
@@ -1520,19 +1510,7 @@ Object {
15201510
},
15211511
"interactionCommits": Map {},
15221512
"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 [],
15361514
"rootID": 6,
15371515
"snapshots": Map {
15381516
6 => Object {
@@ -2066,17 +2044,7 @@ Object {
20662044
"snapshots": Array [],
20672045
},
20682046
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 [],
20802048
"displayName": "Parent",
20812049
"initialTreeBaseDurations": Array [
20822050
Array [
@@ -2098,19 +2066,7 @@ Object {
20982066
],
20992067
"interactionCommits": Array [],
21002068
"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 [],
21142070
"rootID": 6,
21152071
"snapshots": Array [
21162072
Array [

packages/react-devtools-shared/src/backend/renderer.js

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,28 +1023,13 @@ export function attach(
10231023
) {
10241024
// If we aren't profiling, we can just bail out here.
10251025
// 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+
//
10421027
// The Profiler stores metadata for each commit and reconstructs the app tree per commit using:
10431028
// (1) an initial tree snapshot and
10441029
// (2) the operations array for each commit
10451030
// 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) {
10481033
return;
10491034
}
10501035
}
@@ -1388,6 +1373,8 @@ export function attach(
13881373
if (isProfiling) {
13891374
const {alternate} = fiber;
13901375

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.
13911378
if (
13921379
alternate == null ||
13931380
treeBaseDuration !== alternate.treeBaseDuration
@@ -1775,15 +1762,6 @@ export function attach(
17751762
const current = root.current;
17761763
const alternate = current.alternate;
17771764

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-
17871765
currentRootID = getFiberID(getPrimaryFiber(current));
17881766

17891767
// Before the traversals, remember to start tracking
@@ -1800,7 +1778,7 @@ export function attach(
18001778
// where some v16 renderers support profiling and others don't.
18011779
const isProfilingSupported = root.memoizedInteractions != null;
18021780

1803-
if (isProfiling && isProfilingSupported && !didBailoutAtRoot) {
1781+
if (isProfiling && isProfilingSupported) {
18041782
// If profiling is active, store commit time and duration, and the current interactions.
18051783
// The frontend may request this information after profiling has stopped.
18061784
currentCommitProfilingMetadata = {
@@ -1844,7 +1822,7 @@ export function attach(
18441822
mountFiberRecursively(current, null, false, false);
18451823
}
18461824

1847-
if (isProfiling && isProfilingSupported && !didBailoutAtRoot) {
1825+
if (isProfiling && isProfilingSupported) {
18481826
const commitProfilingMetadata = ((rootToCommitProfilingMetadataMap: any): CommitProfilingMetadataMap).get(
18491827
currentRootID,
18501828
);

packages/react-devtools-shared/src/devtools/views/Profiler/utils.js

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -60,26 +60,54 @@ export function prepareProfilingDataFrontendFromBackendAndStore(
6060
throw Error(`Could not find profiling snapshots for root ${rootID}`);
6161
}
6262

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+
6393
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,
78106
displayName,
79107
initialTreeBaseDurations: new Map(initialTreeBaseDurations),
80108
interactionCommits: new Map(interactionCommits),
81109
interactions: new Map(interactions),
82-
operations,
110+
operations: filteredOperations,
83111
rootID,
84112
snapshots,
85113
});

0 commit comments

Comments
 (0)