Skip to content

Commit e8826bc

Browse files
committed
timeline profiler component stacks backend
1 parent 229c86a commit e8826bc

File tree

7 files changed

+158
-3
lines changed

7 files changed

+158
-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: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@ import type {
2222
ReactMeasureType,
2323
TimelineData,
2424
SuspenseEvent,
25+
SchedulingEvent,
2526
} from 'react-devtools-timeline/src/types';
2627

2728
import isArray from 'shared/isArray';
2829
import {
2930
REACT_TOTAL_NUM_LANES,
3031
SCHEDULING_PROFILER_VERSION,
3132
} from 'react-devtools-timeline/src/constants';
33+
import {describeFiber} from './DevToolsFiberComponentStack';
3234

3335
// Add padding to the start/stop time of the profile.
3436
// This makes the UI nicer to use.
@@ -98,6 +100,8 @@ export function createProfilingHooks({
98100
getDisplayNameForFiber,
99101
getIsProfiling,
100102
getLaneLabelMap,
103+
workTagMap,
104+
currentDispatcherRef,
101105
reactVersion,
102106
}: {|
103107
getDisplayNameForFiber: (fiber: Fiber) => string | null,
@@ -109,6 +113,7 @@ export function createProfilingHooks({
109113
let currentReactComponentMeasure: ReactComponentMeasure | null = null;
110114
let currentReactMeasuresStack: Array<ReactMeasure> = [];
111115
let currentTimelineData: TimelineData | null = null;
116+
let currentFiberStacks: Map<SchedulingEvent, Array<Fiber>> = new Map();
112117
let isProfiling: boolean = false;
113118
let nextRenderShouldStartNewBatch: boolean = false;
114119

@@ -774,20 +779,34 @@ export function createProfilingHooks({
774779
}
775780
}
776781

782+
function getParentFibers(fiber: Fiber): Array<Fiber> {
783+
const parents = [];
784+
let parent = fiber;
785+
while (parent !== null) {
786+
parents.push(parent);
787+
parent = parent.return;
788+
}
789+
return parents;
790+
}
791+
777792
function markStateUpdateScheduled(fiber: Fiber, lane: Lane): void {
778793
if (isProfiling || supportsUserTimingV3) {
779794
const componentName = getDisplayNameForFiber(fiber) || 'Unknown';
780795

781796
if (isProfiling) {
782797
// TODO (timeline) Record and cache component stack
783798
if (currentTimelineData) {
784-
currentTimelineData.schedulingEvents.push({
799+
const event = {
785800
componentName,
801+
// Store the parent fibers so we can post process
802+
// them after we finish profiling
786803
lanes: laneToLanesArray(lane),
787804
timestamp: getRelativeTime(),
788805
type: 'schedule-state-update',
789806
warning: null,
790-
});
807+
};
808+
currentFiberStacks.set(event, getParentFibers(fiber));
809+
currentTimelineData.schedulingEvents.push(event);
791810
}
792811
}
793812

@@ -831,6 +850,7 @@ export function createProfilingHooks({
831850
currentBatchUID = 0;
832851
currentReactComponentMeasure = null;
833852
currentReactMeasuresStack = [];
853+
currentFiberStacks = new Map();
834854
currentTimelineData = {
835855
// Session wide metadata; only collected once.
836856
internalModuleSourceToRanges,
@@ -858,6 +878,26 @@ export function createProfilingHooks({
858878
snapshotHeight: 0,
859879
};
860880
nextRenderShouldStartNewBatch = true;
881+
} else {
882+
// Postprocess Profile data
883+
if (currentTimelineData !== null) {
884+
currentTimelineData.schedulingEvents.forEach(event => {
885+
if (event.type === 'schedule-state-update') {
886+
// TODO(luna): We can optimize this by creating a map of
887+
// fiber to component stack instead of generating the stack
888+
// for every fiber every time
889+
const fiberStack = currentFiberStacks.get(event);
890+
if (fiberStack) {
891+
event.componentStack = fiberStack.reduce((trace, fiber) => {
892+
return (
893+
trace +
894+
describeFiber(workTagMap, fiber, currentDispatcherRef)
895+
);
896+
}, '');
897+
}
898+
}
899+
});
900+
}
861901
}
862902
}
863903
}

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)