Skip to content

Commit efd29ef

Browse files
issackjohnDevtools-frontend LUCI CQ
authored andcommitted
Select one CPU profile stream per thread by source
DevTools merges concurrent CPU profile streams on the same thread, which corrupts JavaScript attribution when multiple profiling sources are active simultaneously. For example, when JS Self-Profiling API runs concurrently with internal V8 tracing, their samples get incorrectly merged into a single profile. This CL ingests the optional args.data.source field on "Profile"/"ProfileChunk" trace events and selects exactly one stream per thread based on source priority. Models are built only for the chosen stream, preventing corruption. This enables reliable profiling when multiple sources are active, with backward compatibility for profiles without source tags Related CLs: V8 CL: https://crrev.com/c/7004962 Chromium CL: https://crrev.com/c/6874588 Bug: 375614293 Change-Id: I4032b8570dfc7b70046ce5b7b979dc6f27306fda Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6877206 Reviewed-by: Jack Franklin <[email protected]> Reviewed-by: Yang Guo <[email protected]> Commit-Queue: Issack John <[email protected]>
1 parent cfa5963 commit efd29ef

File tree

3 files changed

+351
-6
lines changed

3 files changed

+351
-6
lines changed

front_end/models/trace/handlers/SamplesHandler.test.ts

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,277 @@ describeWithEnvironment('SamplesHandler', function() {
288288
});
289289
});
290290

291+
describe('profile source selection', () => {
292+
const pid = Trace.Types.Events.ProcessID(42);
293+
const tid = Trace.Types.Events.ThreadID(7);
294+
295+
type ProfileStreamEvent = Trace.Types.Events.Profile|Trace.Types.Events.ProfileChunk;
296+
297+
function makeProfileEvent(id: Trace.Types.Events.ProfileID, source: string): Trace.Types.Events.Profile {
298+
return {
299+
cat: '',
300+
name: Trace.Types.Events.Name.PROFILE,
301+
ph: Trace.Types.Events.Phase.SAMPLE,
302+
pid,
303+
tid,
304+
ts: Trace.Types.Timing.Micro(0),
305+
id,
306+
args: {data: {startTime: Trace.Types.Timing.Micro(0), source: source as Trace.Types.Events.ProfileSource}},
307+
};
308+
}
309+
310+
function makeProfileChunkEvent(
311+
id: Trace.Types.Events.ProfileID,
312+
source: string,
313+
samples: number[],
314+
timeDeltas: number[],
315+
nodes: Array<{id: number, children: number[]}> =
316+
[
317+
{id: 0, children: [1]},
318+
{id: 1, children: []},
319+
],
320+
): Trace.Types.Events.ProfileChunk {
321+
return {
322+
cat: '',
323+
name: Trace.Types.Events.Name.PROFILE_CHUNK,
324+
ph: Trace.Types.Events.Phase.SAMPLE,
325+
pid,
326+
tid,
327+
ts: Trace.Types.Timing.Micro(1),
328+
id,
329+
args: {
330+
data: {
331+
source: source as Trace.Types.Events.ProfileSource,
332+
cpuProfile: {
333+
samples: samples.map(Trace.Types.Events.CallFrameID),
334+
nodes: nodes.map(n => ({
335+
id: Trace.Types.Events.CallFrameID(n.id),
336+
children: n.children,
337+
callFrame: {functionName: '', scriptId: 0, columnNumber: 0, lineNumber: 0, url: ''},
338+
})),
339+
},
340+
timeDeltas: timeDeltas.map(Trace.Types.Timing.Micro),
341+
},
342+
},
343+
};
344+
}
345+
346+
function makeProfileStream({
347+
id,
348+
source,
349+
samples = [1],
350+
timeDeltas = [10],
351+
nodes,
352+
}: {
353+
id: Trace.Types.Events.ProfileID,
354+
source: string,
355+
samples?: number[],
356+
timeDeltas?: number[],
357+
nodes?: Array<{id: number, children: number[]}>,
358+
}): ProfileStreamEvent[] {
359+
const events: ProfileStreamEvent[] = [];
360+
events.push(makeProfileEvent(id, source));
361+
events.push(makeProfileChunkEvent(id, source, samples, timeDeltas, nodes ?? [
362+
{id: 0, children: [1]},
363+
{id: 1, children: []},
364+
]));
365+
return events;
366+
}
367+
368+
async function runSelectionScenario(events: ProfileStreamEvent[], isCPUProfile = false) {
369+
Trace.Handlers.ModelHandlers.Samples.reset();
370+
Trace.Handlers.ModelHandlers.Meta.reset();
371+
372+
for (const event of events) {
373+
Trace.Handlers.ModelHandlers.Samples.handleEvent(event);
374+
}
375+
376+
await Trace.Handlers.ModelHandlers.Meta.finalize();
377+
await Trace.Handlers.ModelHandlers.Samples.finalize({isCPUProfile});
378+
379+
const profileByThread = Trace.Handlers.ModelHandlers.Samples.data().profilesInProcess.get(pid);
380+
assert.exists(profileByThread);
381+
const selected = profileByThread?.get(tid);
382+
assert.exists(selected);
383+
return selected!;
384+
}
385+
386+
it('selects Internal stream over SelfProfiling in performance trace', async () => {
387+
const internalId = Trace.Types.Events.ProfileID('0xE');
388+
const selfProfilingId = Trace.Types.Events.ProfileID('0xJ');
389+
390+
const selected = await runSelectionScenario([
391+
...makeProfileStream({
392+
id: internalId,
393+
source: 'Internal',
394+
}),
395+
...makeProfileStream({
396+
id: selfProfilingId,
397+
source: 'SelfProfiling',
398+
}),
399+
]);
400+
401+
assert.strictEqual(selected.profileId, internalId);
402+
});
403+
404+
it('prefers Inspector stream over Internal in CPU profile mode', async () => {
405+
const inspectorId = Trace.Types.Events.ProfileID('0xD');
406+
const internalId = Trace.Types.Events.ProfileID('0xE');
407+
408+
const selected = await runSelectionScenario(
409+
[
410+
...makeProfileStream({
411+
id: inspectorId,
412+
source: 'Inspector',
413+
}),
414+
...makeProfileStream({
415+
id: internalId,
416+
source: 'Internal',
417+
}),
418+
],
419+
true);
420+
421+
assert.strictEqual(selected.profileId, inspectorId);
422+
});
423+
424+
it('prefers Internal stream over Inspector in performance trace mode', async () => {
425+
const internalId = Trace.Types.Events.ProfileID('0xI');
426+
const inspectorId = Trace.Types.Events.ProfileID('0xD');
427+
428+
const selected = await runSelectionScenario([
429+
...makeProfileStream({
430+
id: internalId,
431+
source: 'Internal',
432+
}),
433+
...makeProfileStream({
434+
id: inspectorId,
435+
source: 'Inspector',
436+
}),
437+
]);
438+
439+
assert.strictEqual(selected.profileId, internalId);
440+
});
441+
442+
it('selects complete priority order in CPU profile mode', async () => {
443+
const inspectorId = Trace.Types.Events.ProfileID('0xD');
444+
const internalId = Trace.Types.Events.ProfileID('0xE');
445+
const selfProfilingId = Trace.Types.Events.ProfileID('0xJ');
446+
447+
const selected = await runSelectionScenario(
448+
[
449+
...makeProfileStream({
450+
id: selfProfilingId,
451+
source: 'SelfProfiling',
452+
}),
453+
...makeProfileStream({
454+
id: internalId,
455+
source: 'Internal',
456+
}),
457+
...makeProfileStream({
458+
id: inspectorId,
459+
source: 'Inspector',
460+
}),
461+
],
462+
true);
463+
464+
assert.strictEqual(selected.profileId, inspectorId);
465+
});
466+
467+
it('selects complete priority order in performance trace mode', async () => {
468+
const inspectorId = Trace.Types.Events.ProfileID('0xD');
469+
const internalId = Trace.Types.Events.ProfileID('0xE');
470+
const selfProfilingId = Trace.Types.Events.ProfileID('0xJ');
471+
472+
const selected = await runSelectionScenario([
473+
...makeProfileStream({
474+
id: selfProfilingId,
475+
source: 'SelfProfiling',
476+
}),
477+
...makeProfileStream({
478+
id: inspectorId,
479+
source: 'Inspector',
480+
}),
481+
...makeProfileStream({
482+
id: internalId,
483+
source: 'Internal',
484+
}),
485+
]);
486+
487+
assert.strictEqual(selected.profileId, internalId);
488+
});
489+
490+
it('falls back to first candidate when no recognized sources exist', async () => {
491+
const firstUnknownId = Trace.Types.Events.ProfileID('0xU1');
492+
const secondUnknownId = Trace.Types.Events.ProfileID('0xU2');
493+
494+
const events: ProfileStreamEvent[] = [
495+
...makeProfileStream({
496+
id: firstUnknownId,
497+
source: 'Unspecified',
498+
}),
499+
...makeProfileStream({
500+
id: secondUnknownId,
501+
source: 'CustomSource',
502+
}),
503+
];
504+
505+
const selected = await runSelectionScenario(events);
506+
507+
// Falls back to candidates[0] when no priority matches
508+
assert.strictEqual(selected.profileId, firstUnknownId);
509+
});
510+
511+
it('ignores profiles with unknown sources in priority matching', async () => {
512+
const inspectorId = Trace.Types.Events.ProfileID('0xD');
513+
const unknownId = Trace.Types.Events.ProfileID('0xU');
514+
515+
const events: ProfileStreamEvent[] = [
516+
...makeProfileStream({
517+
id: unknownId,
518+
source: 'Unspecified',
519+
}),
520+
...makeProfileStream({
521+
id: inspectorId,
522+
source: 'Inspector',
523+
}),
524+
];
525+
526+
const selected = await runSelectionScenario(events, true);
527+
528+
assert.strictEqual(selected.profileId, inspectorId);
529+
});
530+
531+
it('falls back to SelfProfiling when higher priority sources absent', async () => {
532+
const selfProfilingId = Trace.Types.Events.ProfileID('0xJ');
533+
534+
const selected = await runSelectionScenario(
535+
[
536+
...makeProfileStream({
537+
id: selfProfilingId,
538+
source: 'SelfProfiling',
539+
}),
540+
],
541+
true);
542+
543+
assert.strictEqual(selected.profileId, selfProfilingId);
544+
});
545+
546+
it('selects available source even when not in priority list for CPU profile mode', async () => {
547+
const internalId = Trace.Types.Events.ProfileID('0xE');
548+
549+
const selected = await runSelectionScenario(
550+
[
551+
...makeProfileStream({
552+
id: internalId,
553+
source: 'Internal',
554+
}),
555+
],
556+
true);
557+
558+
assert.strictEqual(selected.profileId, internalId);
559+
});
560+
});
561+
291562
describe('getProfileCallFunctionName', () => {
292563
/**
293564
* Find an event from the trace that represents some work. The use of

0 commit comments

Comments
 (0)