Skip to content

Commit

Permalink
feat(profiling): read v8 profiles and create a profile timeline entry (
Browse files Browse the repository at this point in the history
…getsentry#35674)

* feat(profiling): add chrometrace sample trace

* feat(profiling): fix type guards

* feat(profiling): add guard tests

* feat(profiling): split profilechunks by process/thread

* ref(review): use find

* fix(guards): update ts comment

* fix(profiling): trim trace
  • Loading branch information
JonasBa authored and pull[bot] committed Feb 23, 2024
1 parent b6096e6 commit 4f720ff
Show file tree
Hide file tree
Showing 5 changed files with 30,225 additions and 19 deletions.
54 changes: 52 additions & 2 deletions static/app/types/chromeTrace.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ declare namespace ChromeTrace {
samples: ReadonlyArray<any>;
}

type ArrayFormat = ReadonlyArray<Event>;
type ArrayFormat = Array<Event | ProfileEvent>;
type DurationEvent = 'B' | 'E';
// Instant event
type CompleteEvent = 'X';
Expand Down Expand Up @@ -73,10 +73,60 @@ declare namespace ChromeTrace {
tdur?: number;
pid: number;
tid: number;
id?: string;
cname?: string;
args: Record<string, any | Record<string, any>>;
}

// Thread metadata event
interface ThreadMetadataEvent extends Event {
cat: '__metadata';
name: 'thread_name';
ph: 'M';
args: {name: string};
}

interface ProfileEvent extends Event {
cat: string;
id: string;
name: 'Profile';
ph: 'P';
pid: number;
tid: number;
ts: number;
tts: number;
args: {data: CpuProfile};
}

interface ProfileChunkEvent extends Event {
cat: string;
id: string;
name: 'ProfileChunk';
ph: 'P';
pid: number;
tid: number;
ts: number;
tts: number;
args: {data: {cpuProfile: CpuProfile}};
}

// https://github.com/v8/v8/blob/b8626ca445554b8376b5a01f651b70cb8c01b7dd/src/inspector/js_protocol.json#L1496
interface PositionTickInfo {
line: number;
ticks: number;
}

// https://github.com/v8/v8/blob/b8626ca445554b8376b5a01f651b70cb8c01b7dd/src/inspector/js_protocol.json#L2292
interface CallFrame {
functionName: string;
scriptId: string;
url: string;
lineNumber: number;
columnNumber: number;
// This seems to be present in some profiles with value "JS"
codeType?: string;
}

// https://github.com/v8/v8/blob/b8626ca445554b8376b5a01f651b70cb8c01b7dd/src/inspector/js_protocol.json#L1399
interface ProfileNode {
id: number;
Expand All @@ -89,7 +139,7 @@ declare namespace ChromeTrace {
}

// https://github.com/v8/v8/blob/b8626ca445554b8376b5a01f651b70cb8c01b7dd/src/inspector/js_protocol.json#L1453
interface Profile {
interface CpuProfile {
nodes: ProfileNode[];
startTime: number;
endTime: number;
Expand Down
32 changes: 25 additions & 7 deletions static/app/utils/profiling/guards/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,33 @@ export function isJSProfile(profile: any): profile is JSSelfProfiling.Trace {
return !('type' in profile) && Array.isArray(profile.resources);
}

export function isChromeTraceFormat(input: any): input is ChromeTrace.ProfileType {
return isChromeTraceArrayFormat(input) || isChromeTraceObjectFormat(input);
}

export function isChromeTraceObjectFormat(input: any): input is ChromeTrace.ObjectFormat {
return typeof input === 'object' && 'traceEvents' in input;
}

export function isChromeTraceArrayFormat(input: any): input is ChromeTrace.ArrayFormat {
// @TODO we need to check if the profile actually includes the v8 profile nodes.
return Array.isArray(input);
// We check for the presence of at least one ProfileChunk event in the trace
export function isChromeTraceArrayFormat(input: any): input is ChromeTrace.ProfileType {
return (
Array.isArray(input) && input.some(p => p.ph === 'P' && p.name === 'ProfileChunk')
);
}

// Typescript uses only a subset of the event types (only B and E cat),
// so we need to inspect the contents of the trace to determine the type of the profile.
// The TS trace can still contain other event types like metadata events, meaning we cannot
// use array.every() and need to check all the events to make sure no P events are present
export function isTypescriptChromeTraceArrayFormat(
input: any
): input is ChromeTrace.ArrayFormat {
return (
Array.isArray(input) && !input.some(p => p.ph === 'P' && p.name === 'ProfileChunk')
);
}

export function isChromeTraceFormat(input: any): input is ChromeTrace.ArrayFormat {
return (
isTypescriptChromeTraceArrayFormat(input) ||
isChromeTraceObjectFormat(input) ||
isChromeTraceArrayFormat(input)
);
}
129 changes: 129 additions & 0 deletions static/app/utils/profiling/profile/chromeTraceProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export function splitEventsByProcessAndTraceId(
return collections;
}

function chronologicalSort(a: ChromeTrace.Event, b: ChromeTrace.Event): number {
return a.ts - b.ts;
}

function reverseChronologicalSort(a: ChromeTrace.Event, b: ChromeTrace.Event): number {
return b.ts - a.ts;
}
Expand Down Expand Up @@ -285,3 +289,128 @@ export function parseChromeTraceArrayFormat(
profiles,
};
}

function isProfileEvent(event: ChromeTrace.Event): event is ChromeTrace.ProfileEvent {
return event.ph === 'P' && event.name === 'Profile';
}

function isProfileChunk(
event: ChromeTrace.Event
): event is ChromeTrace.ProfileChunkEvent {
return event.ph === 'P' && event.name === 'ProfileChunk';
}

function isThreadmetaData(
event: ChromeTrace.Event
): event is ChromeTrace.ThreadMetadataEvent {
event.name === 'Thread';

return event.ph === 'M' && event.name === 'Thread';
}

type Required<T> = {
[P in keyof T]-?: T[P];
};

// This mostly follows what speedscope does for the Chrome Trace format, but we do minor adjustments (not sure if they are correct atm),
// but the protocol format seems out of date and is not well documented, so this is a best effort.
function collectEventsByProfile(input: ChromeTrace.ArrayFormat): {
profiles: Map<string, Required<ChromeTrace.CpuProfile>>;
threadNames: Map<string, string>;
} {
const sorted = input.sort(chronologicalSort);

const threadNames = new Map<string, string>();
const profileIdToProcessAndThreadIds = new Map<string, [number, number]>();
const profiles = new Map<string, Required<ChromeTrace.CpuProfile>>();

for (let i = 0; i < sorted.length; i++) {
const event = sorted[i];

if (isThreadmetaData(event)) {
threadNames.set(`${event.pid}:${event.tid}`, event.args.name);
continue;
}

// A profile entry will happen before we see any ProfileChunks, so the order here matters
if (isProfileEvent(event)) {
profileIdToProcessAndThreadIds.set(event.id, [event.pid, event.tid]);

if (profiles.has(event.id)) {
continue;
}

// Judging by https://github.com/v8/v8/blob/b8626ca445554b8376b5a01f651b70cb8c01b7dd/src/inspector/js_protocol.json#L1453,
// the only optional properties of a profile event are the samples and the timeDelta, however looking at a few sample traces
// this does not seem to be the case. For example, in our chrometrace/trace.json there is a profile entry where only startTime is present
profiles.set(event.id, {
samples: [],
timeDeltas: [],
// @ts-ignore
startTime: 0,
// @ts-ignore
endTime: 0,
// @ts-ignore
nodes: [],
...event.args.data,
});
continue;
}

if (isProfileChunk(event)) {
const profile = profiles.get(event.id);

if (!profile) {
throw new Error('No entry for Profile was found before ProfileChunk');
}

// If we have a chunk, then append our values to it. Eventually we end up with a single profile with all of the chunks and samples merged
const cpuProfile = event.args.data.cpuProfile;
if (cpuProfile.nodes) {
profile.nodes = profile.nodes.concat(cpuProfile.nodes ?? []);
}
if (cpuProfile.samples) {
profile.samples = profile.samples.concat(cpuProfile.samples ?? []);
}
if (cpuProfile.timeDeltas) {
profile.timeDeltas = profile.timeDeltas.concat(cpuProfile.timeDeltas ?? []);
}
if (cpuProfile.startTime !== null) {
// Make sure we dont overwrite the startTime if it is already set
if (typeof profile.startTime === 'number') {
profile.startTime = Math.min(profile.startTime, cpuProfile.startTime);
} else {
profile.startTime = cpuProfile.startTime;
}
}
// Make sure we dont overwrite the endTime if it is already set
if (cpuProfile.endTime !== null) {
if (typeof profile.endTime === 'number') {
profile.endTime = Math.max(profile.endTime, cpuProfile.endTime);
} else {
profile.endTime = cpuProfile.endTime;
}
}
}
continue;
}

return {profiles, threadNames};
}

export function parseChromeTraceFormat(
input: ChromeTrace.ArrayFormat,
traceID: string,
_options?: ImportOptions
): ProfileGroup {
const profiles: Profile[] = [];

collectEventsByProfile(input);

return {
name: 'chrometrace',
traceID,
activeProfileIndex: 0,
profiles,
};
}
Loading

0 comments on commit 4f720ff

Please sign in to comment.