Skip to content

Commit 9abe745

Browse files
lunaruanblakef
andauthored
[DevTools][Timeline Profiler] Component Stacks Backend (#24776)
This PR adds a component stack field to the `schedule-state-update` event. The algorithm is as follows: * During profiling, whenever a state update happens collect the parents of the fiber that caused the state update and store it in a map * After profiling finishes, post process the `schedule-state-update` event and using the parent fibers, generate the component stack by using`describeFiber`, a function that uses error throwing to get the location of the component by calling the component without props. --- Co-authored-by: Blake Friedman <blake.friedman@gmail.com>
1 parent cf665c4 commit 9abe745

File tree

7 files changed

+167
-3
lines changed

7 files changed

+167
-3
lines changed

packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@
99

1010
'use strict';
1111

12+
function normalizeCodeLocInfo(str) {
13+
return (
14+
typeof str === 'string' &&
15+
str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
16+
return '\n in ' + name + ' (at **)';
17+
})
18+
);
19+
}
20+
1221
describe('Timeline profiler', () => {
1322
let React;
1423
let ReactDOMClient;
@@ -1175,6 +1184,18 @@ describe('Timeline profiler', () => {
11751184
if (timelineData) {
11761185
expect(timelineData).toHaveLength(1);
11771186

1187+
// normalize the location for component stack source
1188+
// for snapshot testing
1189+
timelineData.forEach(data => {
1190+
data.schedulingEvents.forEach(event => {
1191+
if (event.componentStack) {
1192+
event.componentStack = normalizeCodeLocInfo(
1193+
event.componentStack,
1194+
);
1195+
}
1196+
});
1197+
});
1198+
11781199
return timelineData[0];
11791200
} else {
11801201
return null;
@@ -1256,27 +1277,35 @@ describe('Timeline profiler', () => {
12561277
Array [
12571278
Object {
12581279
"componentName": "Example",
1280+
"componentStack": "
1281+
in Example (at **)",
12591282
"lanes": "0b0000000000000000000000000000100",
12601283
"timestamp": 10,
12611284
"type": "schedule-state-update",
12621285
"warning": null,
12631286
},
12641287
Object {
12651288
"componentName": "Example",
1289+
"componentStack": "
1290+
in Example (at **)",
12661291
"lanes": "0b0000000000000000000000001000000",
12671292
"timestamp": 10,
12681293
"type": "schedule-state-update",
12691294
"warning": null,
12701295
},
12711296
Object {
12721297
"componentName": "Example",
1298+
"componentStack": "
1299+
in Example (at **)",
12731300
"lanes": "0b0000000000000000000000001000000",
12741301
"timestamp": 10,
12751302
"type": "schedule-state-update",
12761303
"warning": null,
12771304
},
12781305
Object {
12791306
"componentName": "Example",
1307+
"componentStack": "
1308+
in Example (at **)",
12801309
"lanes": "0b0000000000000000000000000010000",
12811310
"timestamp": 10,
12821311
"type": "schedule-state-update",
@@ -1614,6 +1643,8 @@ describe('Timeline profiler', () => {
16141643
},
16151644
Object {
16161645
"componentName": "Example",
1646+
"componentStack": "
1647+
in Example (at **)",
16171648
"lanes": "0b0000000000000000000000000000001",
16181649
"timestamp": 20,
16191650
"type": "schedule-state-update",
@@ -1741,6 +1772,8 @@ describe('Timeline profiler', () => {
17411772
},
17421773
Object {
17431774
"componentName": "Example",
1775+
"componentStack": "
1776+
in Example (at **)",
17441777
"lanes": "0b0000000000000000000000000010000",
17451778
"timestamp": 10,
17461779
"type": "schedule-state-update",
@@ -1872,6 +1905,8 @@ describe('Timeline profiler', () => {
18721905
},
18731906
Object {
18741907
"componentName": "Example",
1908+
"componentStack": "
1909+
in Example (at **)",
18751910
"lanes": "0b0000000000000000000000000000001",
18761911
"timestamp": 21,
18771912
"type": "schedule-state-update",
@@ -1934,6 +1969,8 @@ describe('Timeline profiler', () => {
19341969
},
19351970
Object {
19361971
"componentName": "Example",
1972+
"componentStack": "
1973+
in Example (at **)",
19371974
"lanes": "0b0000000000000000000000000010000",
19381975
"timestamp": 21,
19391976
"type": "schedule-state-update",
@@ -1982,6 +2019,8 @@ describe('Timeline profiler', () => {
19822019
},
19832020
Object {
19842021
"componentName": "Example",
2022+
"componentStack": "
2023+
in Example (at **)",
19852024
"lanes": "0b0000000000000000000000000010000",
19862025
"timestamp": 20,
19872026
"type": "schedule-state-update",
@@ -2065,6 +2104,8 @@ describe('Timeline profiler', () => {
20652104
},
20662105
Object {
20672106
"componentName": "ErrorBoundary",
2107+
"componentStack": "
2108+
in ErrorBoundary (at **)",
20682109
"lanes": "0b0000000000000000000000000000001",
20692110
"timestamp": 20,
20702111
"type": "schedule-state-update",
@@ -2177,6 +2218,8 @@ describe('Timeline profiler', () => {
21772218
},
21782219
Object {
21792220
"componentName": "ErrorBoundary",
2221+
"componentStack": "
2222+
in ErrorBoundary (at **)",
21802223
"lanes": "0b0000000000000000000000000000001",
21812224
"timestamp": 30,
21822225
"type": "schedule-state-update",
@@ -2441,6 +2484,52 @@ describe('Timeline profiler', () => {
24412484
}
24422485
`);
24432486
});
2487+
2488+
it('should generate component stacks for state update', async () => {
2489+
function CommponentWithChildren({initialRender}) {
2490+
Scheduler.unstable_yieldValue('Render ComponentWithChildren');
2491+
return <Child initialRender={initialRender} />;
2492+
}
2493+
2494+
function Child({initialRender}) {
2495+
const [didRender, setDidRender] = React.useState(initialRender);
2496+
if (!didRender) {
2497+
setDidRender(true);
2498+
}
2499+
Scheduler.unstable_yieldValue('Render Child');
2500+
return null;
2501+
}
2502+
2503+
renderRootHelper(<CommponentWithChildren initialRender={false} />);
2504+
2505+
expect(Scheduler).toFlushAndYield([
2506+
'Render ComponentWithChildren',
2507+
'Render Child',
2508+
'Render Child',
2509+
]);
2510+
2511+
const timelineData = stopProfilingAndGetTimelineData();
2512+
expect(timelineData.schedulingEvents).toMatchInlineSnapshot(`
2513+
Array [
2514+
Object {
2515+
"lanes": "0b0000000000000000000000000010000",
2516+
"timestamp": 10,
2517+
"type": "schedule-render",
2518+
"warning": null,
2519+
},
2520+
Object {
2521+
"componentName": "Child",
2522+
"componentStack": "
2523+
in Child (at **)
2524+
in CommponentWithChildren (at **)",
2525+
"lanes": "0b0000000000000000000000000010000",
2526+
"timestamp": 10,
2527+
"type": "schedule-state-update",
2528+
"warning": null,
2529+
},
2530+
]
2531+
`);
2532+
});
24442533
});
24452534

24462535
describe('when not profiling', () => {

packages/react-devtools-shared/src/__tests__/preprocessData-test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@
99

1010
'use strict';
1111

12+
function normalizeCodeLocInfo(str) {
13+
return (
14+
typeof str === 'string' &&
15+
str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
16+
return '\n in ' + name + ' (at **)';
17+
})
18+
);
19+
}
20+
1221
describe('Timeline profiler', () => {
1322
let React;
1423
let ReactDOM;
@@ -2134,6 +2143,15 @@ describe('Timeline profiler', () => {
21342143
const data = store.profilerStore.profilingData?.timelineData;
21352144
expect(data).toHaveLength(1);
21362145
const timelineData = data[0];
2146+
2147+
// normalize the location for component stack source
2148+
// for snapshot testing
2149+
timelineData.schedulingEvents.forEach(event => {
2150+
if (event.componentStack) {
2151+
event.componentStack = normalizeCodeLocInfo(event.componentStack);
2152+
}
2153+
});
2154+
21372155
expect(timelineData).toMatchInlineSnapshot(`
21382156
Object {
21392157
"batchUIDToMeasuresMap": Map {
@@ -2415,6 +2433,8 @@ describe('Timeline profiler', () => {
24152433
},
24162434
Object {
24172435
"componentName": "App",
2436+
"componentStack": "
2437+
in App (at **)",
24182438
"lanes": "0b0000000000000000000000000010000",
24192439
"timestamp": 10,
24202440
"type": "schedule-state-update",

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
describeClassComponentFrame,
2222
} from './DevToolsComponentStackFrame';
2323

24-
function describeFiber(
24+
export function describeFiber(
2525
workTagMap: WorkTagMap,
2626
workInProgress: Fiber,
2727
currentDispatcherRef: CurrentDispatcherRef,

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

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import type {
1111
Lane,
1212
Lanes,
1313
DevToolsProfilingHooks,
14+
WorkTagMap,
15+
CurrentDispatcherRef,
1416
} from 'react-devtools-shared/src/backend/types';
1517
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
1618
import type {Wakeable} from 'shared/ReactTypes';
@@ -22,13 +24,16 @@ import type {
2224
ReactMeasureType,
2325
TimelineData,
2426
SuspenseEvent,
27+
SchedulingEvent,
28+
ReactScheduleStateUpdateEvent,
2529
} from 'react-devtools-timeline/src/types';
2630

2731
import isArray from 'shared/isArray';
2832
import {
2933
REACT_TOTAL_NUM_LANES,
3034
SCHEDULING_PROFILER_VERSION,
3135
} from 'react-devtools-timeline/src/constants';
36+
import {describeFiber} from './DevToolsFiberComponentStack';
3237

3338
// Add padding to the start/stop time of the profile.
3439
// This makes the UI nicer to use.
@@ -98,17 +103,22 @@ export function createProfilingHooks({
98103
getDisplayNameForFiber,
99104
getIsProfiling,
100105
getLaneLabelMap,
106+
workTagMap,
107+
currentDispatcherRef,
101108
reactVersion,
102109
}: {|
103110
getDisplayNameForFiber: (fiber: Fiber) => string | null,
104111
getIsProfiling: () => boolean,
105112
getLaneLabelMap?: () => Map<Lane, string> | null,
113+
currentDispatcherRef?: CurrentDispatcherRef,
114+
workTagMap: WorkTagMap,
106115
reactVersion: string,
107116
|}): Response {
108117
let currentBatchUID: BatchUID = 0;
109118
let currentReactComponentMeasure: ReactComponentMeasure | null = null;
110119
let currentReactMeasuresStack: Array<ReactMeasure> = [];
111120
let currentTimelineData: TimelineData | null = null;
121+
let currentFiberStacks: Map<SchedulingEvent, Array<Fiber>> = new Map();
112122
let isProfiling: boolean = false;
113123
let nextRenderShouldStartNewBatch: boolean = false;
114124

@@ -774,20 +784,34 @@ export function createProfilingHooks({
774784
}
775785
}
776786

787+
function getParentFibers(fiber: Fiber): Array<Fiber> {
788+
const parents = [];
789+
let parent = fiber;
790+
while (parent !== null) {
791+
parents.push(parent);
792+
parent = parent.return;
793+
}
794+
return parents;
795+
}
796+
777797
function markStateUpdateScheduled(fiber: Fiber, lane: Lane): void {
778798
if (isProfiling || supportsUserTimingV3) {
779799
const componentName = getDisplayNameForFiber(fiber) || 'Unknown';
780800

781801
if (isProfiling) {
782802
// TODO (timeline) Record and cache component stack
783803
if (currentTimelineData) {
784-
currentTimelineData.schedulingEvents.push({
804+
const event: ReactScheduleStateUpdateEvent = {
785805
componentName,
806+
// Store the parent fibers so we can post process
807+
// them after we finish profiling
786808
lanes: laneToLanesArray(lane),
787809
timestamp: getRelativeTime(),
788810
type: 'schedule-state-update',
789811
warning: null,
790-
});
812+
};
813+
currentFiberStacks.set(event, getParentFibers(fiber));
814+
currentTimelineData.schedulingEvents.push(event);
791815
}
792816
}
793817

@@ -831,6 +855,7 @@ export function createProfilingHooks({
831855
currentBatchUID = 0;
832856
currentReactComponentMeasure = null;
833857
currentReactMeasuresStack = [];
858+
currentFiberStacks = new Map();
834859
currentTimelineData = {
835860
// Session wide metadata; only collected once.
836861
internalModuleSourceToRanges,
@@ -858,6 +883,30 @@ export function createProfilingHooks({
858883
snapshotHeight: 0,
859884
};
860885
nextRenderShouldStartNewBatch = true;
886+
} else {
887+
// Postprocess Profile data
888+
if (currentTimelineData !== null) {
889+
currentTimelineData.schedulingEvents.forEach(event => {
890+
if (event.type === 'schedule-state-update') {
891+
// TODO(luna): We can optimize this by creating a map of
892+
// fiber to component stack instead of generating the stack
893+
// for every fiber every time
894+
const fiberStack = currentFiberStacks.get(event);
895+
if (fiberStack && currentDispatcherRef != null) {
896+
event.componentStack = fiberStack.reduce((trace, fiber) => {
897+
return (
898+
trace +
899+
describeFiber(workTagMap, fiber, currentDispatcherRef)
900+
);
901+
}, '');
902+
}
903+
}
904+
});
905+
}
906+
907+
// Clear the current fiber stacks so we don't hold onto the fibers
908+
// in memory after profiling finishes
909+
currentFiberStacks.clear();
861910
}
862911
}
863912
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,8 @@ export function attach(
660660
getDisplayNameForFiber,
661661
getIsProfiling: () => isProfiling,
662662
getLaneLabelMap,
663+
currentDispatcherRef: renderer.currentDispatcherRef,
664+
workTagMap: ReactTypeOfWork,
663665
reactVersion: version,
664666
});
665667

packages/react-devtools-timeline/src/types.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export type ReactScheduleRenderEvent = {|
5151
|};
5252
export type ReactScheduleStateUpdateEvent = {|
5353
...BaseReactScheduleEvent,
54+
+componentStack?: string,
5455
+type: 'schedule-state-update',
5556
|};
5657
export type ReactScheduleForceUpdateEvent = {|

packages/shared/ReactComponentStackFrame.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ export function describeNativeComponentFrame(
131131
} catch (x) {
132132
control = x;
133133
}
134+
// TODO(luna): This will currently only throw if the function component
135+
// tries to access React/ReactDOM/props. We should probably make this throw
136+
// in simple components too
134137
fn();
135138
}
136139
} catch (sample) {

0 commit comments

Comments
 (0)