diff --git a/sample/profiles/trace-event/multi-thread-with-samples.json b/sample/profiles/trace-event/multi-thread-with-samples.json new file mode 100644 index 000000000..11d832b37 --- /dev/null +++ b/sample/profiles/trace-event/multi-thread-with-samples.json @@ -0,0 +1,150 @@ +{ + "traceEvents": [ + { + "name": "process_name", + "ph": "M", + "cat": "__metadata", + "pid": 7512, + "ts": "11550183666", + "tid": "-1", + "args": { + "name": "hermes" + } + }, + { + "name": "thread_name", + "ph": "M", + "cat": "__metadata", + "pid": 7512, + "ts": "11550183666", + "tid": "1", + "args": { + "name": "mqt_js" + } + }, + { + "name": "mqt_js", + "cat": "mqt_js", + "ph": "X", + "dur": 0, + "pid": 7512, + "ts": "11550183666", + "tid": "1", + "args": {} + }, + { + "name": "mqt_js", + "cat": "mqt_js", + "ph": "X", + "dur": 0, + "pid": 7512, + "ts": "11550183666", + "tid": "2", + "args": {} + } + ], + "samples": [ + { + "cpu": "-1", + "name": "", + "ts": "11552125241", + "pid": 7512, + "tid": "1", + "weight": "1", + "sf": 1 + }, + { + "cpu": "-1", + "name": "", + "ts": "11552125241", + "pid": 7512, + "tid": "2", + "weight": "1", + "sf": 4 + }, + { + "cpu": "-1", + "name": "", + "ts": "11552146011", + "pid": 7512, + "tid": "1", + "weight": "1", + "sf": 2 + }, + { + "cpu": "-1", + "name": "", + "ts": "11552146011", + "pid": 7512, + "tid": "2", + "weight": "1", + "sf": 5 + }, + { + "cpu": "-1", + "name": "", + "ts": "11552159337", + "pid": 7512, + "tid": "1", + "weight": "1", + "sf": 3 + }, + { + "cpu": "-1", + "name": "", + "ts": "11552159337", + "pid": 7512, + "tid": "2", + "weight": "1", + "sf": 6 + }, + { + "cpu": "-1", + "name": "", + "ts": "11552169337", + "pid": 7512, + "tid": "2", + "weight": "1", + "sf": 4 + }, + { + "cpu": "-1", + "name": "", + "ts": "11552179337", + "pid": 7512, + "tid": "2", + "weight": "1", + "sf": 4 + } + ], + "stackFrames": { + "1": { + "name": "[root]", + "category": "root" + }, + "2": { + "name": "function1", + "category": "JavaScript", + "parent": 1 + }, + "3": { + "name": "[GC Young Gen]", + "category": "Metadata", + "parent": 1 + }, + "4": { + "name": "[root thread 2]", + "category": "root" + }, + "5": { + "name": "function3", + "category": "JavaScript", + "parent": 4 + }, + "6": { + "name": "function4", + "category": "Metadata", + "parent": 4 + } + } + } \ No newline at end of file diff --git a/sample/profiles/trace-event/simple-with-samples.json b/sample/profiles/trace-event/simple-with-samples.json new file mode 100644 index 000000000..ae8456ac6 --- /dev/null +++ b/sample/profiles/trace-event/simple-with-samples.json @@ -0,0 +1,127 @@ +{ + "traceEvents": [ + { + "name": "process_name", + "ph": "M", + "cat": "__metadata", + "pid": 7512, + "ts": "11550183666", + "tid": "-1", + "args": { + "name": "hermes" + } + }, + { + "name": "thread_name", + "ph": "M", + "cat": "__metadata", + "pid": 7512, + "ts": "11550183666", + "tid": "7695", + "args": { + "name": "mqt_js" + } + }, + { + "name": "mqt_js", + "cat": "mqt_js", + "ph": "X", + "dur": 0, + "pid": 7512, + "ts": "11550183666", + "tid": "7695", + "args": {} + } + ], + "samples": [ + { + "cpu": "-1", + "name": "", + "ts": "11552125241", + "pid": 7512, + "tid": "7695", + "weight": "1", + "sf": 1 + }, + { + "cpu": "-1", + "name": "", + "ts": "11552129681", + "pid": 7512, + "tid": "7695", + "weight": "1", + "sf": 1 + }, + { + "cpu": "-1", + "name": "", + "ts": "11552146011", + "pid": 7512, + "tid": "7695", + "weight": "1", + "sf": 2 + }, + { + "cpu": "-1", + "name": "", + "ts": "11552159337", + "pid": 7512, + "tid": "7695", + "weight": "1", + "sf": 4 + }, + { + "cpu": "-1", + "name": "", + "ts": "11552169942", + "pid": 7512, + "tid": "7695", + "weight": "1", + "sf": 1 + }, + { + "cpu": "-1", + "name": "", + "ts": "11552179951", + "pid": 7512, + "tid": "7695", + "weight": "1", + "sf": 5 + }, + { + "cpu": "-1", + "name": "", + "ts": "11552189951", + "pid": 7512, + "tid": "7695", + "weight": "1", + "sf": 5 + } + ], + "stackFrames": { + "1": { + "name": "[root]", + "category": "root" + }, + "2": { + "name": "function1(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:1:1)", + "category": "JavaScript", + "parent": 1 + }, + "3": { + "name": "function2(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:418874:4)", + "category": "JavaScript", + "parent": 1 + }, + "4": { + "name": "function3(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:17:3)", + "category": "JavaScript", + "parent": 3 + }, + "5": { + "name": "[GC Young Gen]", + "category": "Metadata", + "parent": 1 + } + } +} \ No newline at end of file diff --git a/src/import/__snapshots__/trace-event.test.ts.snap b/src/import/__snapshots__/trace-event.test.ts.snap index efc317ad3..e13503076 100644 --- a/src/import/__snapshots__/trace-event.test.ts.snap +++ b/src/import/__snapshots__/trace-event.test.ts.snap @@ -530,6 +530,81 @@ exports[`importTraceEvents mismatched name: indexToView 1`] = `0`; exports[`importTraceEvents mismatched name: profileGroup.name 1`] = `"mismatched-name.json"`; +exports[`importTraceEvents multi-thread profile with samples 1`] = ` +Object { + "frames": Array [ + Frame { + "col": undefined, + "file": undefined, + "key": "[root]:root", + "line": undefined, + "name": "[root]", + "selfWeight": 20770, + "totalWeight": 34096, + }, + Frame { + "col": undefined, + "file": undefined, + "key": "function1:JavaScript", + "line": undefined, + "name": "function1", + "selfWeight": 13326, + "totalWeight": 13326, + }, + ], + "name": "hermes (pid 7512), mqt_js (tid 1)", + "stacks": Array [ + "[root] 20.77ms", + "[root];function1 13.33ms", + ], +} +`; + +exports[`importTraceEvents multi-thread profile with samples 2`] = ` +Object { + "frames": Array [ + Frame { + "col": undefined, + "file": undefined, + "key": "[root thread 2]:root", + "line": undefined, + "name": "[root thread 2]", + "selfWeight": 30770, + "totalWeight": 54096, + }, + Frame { + "col": undefined, + "file": undefined, + "key": "function3:JavaScript", + "line": undefined, + "name": "function3", + "selfWeight": 13326, + "totalWeight": 13326, + }, + Frame { + "col": undefined, + "file": undefined, + "key": "function4:Metadata", + "line": undefined, + "name": "function4", + "selfWeight": 10000, + "totalWeight": 10000, + }, + ], + "name": "hermes (pid 7512, tid 2)", + "stacks": Array [ + "[root thread 2] 20.77ms", + "[root thread 2];function3 13.33ms", + "[root thread 2];function4 10.00ms", + "[root thread 2] 10.00ms", + ], +} +`; + +exports[`importTraceEvents multi-thread profile with samples: indexToView 1`] = `0`; + +exports[`importTraceEvents multi-thread profile with samples: profileGroup.name 1`] = `"multi-thread-with-samples.json"`; + exports[`importTraceEvents multiprocess 1`] = ` Object { "frames": Array [ @@ -1286,6 +1361,70 @@ exports[`importTraceEvents simple object: indexToView 1`] = `0`; exports[`importTraceEvents simple object: profileGroup.name 1`] = `"simple-object.json"`; +exports[`importTraceEvents simple profile with samples 1`] = ` +Object { + "frames": Array [ + Frame { + "col": undefined, + "file": undefined, + "key": "[root]:root", + "line": undefined, + "name": "[root]", + "selfWeight": 30779, + "totalWeight": 64710, + }, + Frame { + "col": undefined, + "file": undefined, + "key": "function1(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:1:1):JavaScript", + "line": undefined, + "name": "function1(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:1:1)", + "selfWeight": 13326, + "totalWeight": 13326, + }, + Frame { + "col": undefined, + "file": undefined, + "key": "function2(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:418874:4):JavaScript", + "line": undefined, + "name": "function2(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:418874:4)", + "selfWeight": 0, + "totalWeight": 10605, + }, + Frame { + "col": undefined, + "file": undefined, + "key": "function3(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:17:3):JavaScript", + "line": undefined, + "name": "function3(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:17:3)", + "selfWeight": 10605, + "totalWeight": 10605, + }, + Frame { + "col": undefined, + "file": undefined, + "key": "[GC Young Gen]:Metadata", + "line": undefined, + "name": "[GC Young Gen]", + "selfWeight": 10000, + "totalWeight": 10000, + }, + ], + "name": "hermes (pid 7512), mqt_js (tid 7695)", + "stacks": Array [ + "[root] 20.77ms", + "[root];function1(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:1:1) 13.33ms", + "[root];function2(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:418874:4);function3(http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=org.toshi&modulesOnly=false&runModule=true:17:3) 10.61ms", + "[root] 10.01ms", + "[root];[GC Young Gen] 10.00ms", + ], +} +`; + +exports[`importTraceEvents simple profile with samples: indexToView 1`] = `0`; + +exports[`importTraceEvents simple profile with samples: profileGroup.name 1`] = `"simple-with-samples.json"`; + exports[`importTraceEvents simple: indexToView 1`] = `0`; exports[`importTraceEvents simple: profileGroup.name 1`] = `"simple.json"`; diff --git a/src/import/index.ts b/src/import/index.ts index 155ff079a..29d5e09cd 100644 --- a/src/import/index.ts +++ b/src/import/index.ts @@ -23,7 +23,7 @@ import {decodeBase64} from '../lib/utils' import {importFromChromeHeapProfile} from './v8heapalloc' import {isTraceEventFormatted, importTraceEvents} from './trace-event' import {importFromCallgrind} from './callgrind' -import {importFromPapyrus} from "./papyrus"; +import {importFromPapyrus} from './papyrus' export async function importProfileGroupFromText( fileName: string, @@ -189,8 +189,8 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise { @@ -20,9 +20,11 @@ describe('importCpuProfileWithProperWeights', () => { }) test('uses samples count for weight when importing cpu profile', async () => { - const profileFile = readFileSync('./sample/profiles/stackprof/simple-cpu-stackprof.json') - const profileGroup = await importProfileGroupFromText('simple-cpu-stackprof.json', profileFile.toString()) + const profileGroup = await importProfileGroupFromText( + 'simple-cpu-stackprof.json', + profileFile.toString(), + ) expect(profileGroup).not.toBeNull() if (profileGroup) { diff --git a/src/import/stackprof.ts b/src/import/stackprof.ts index c6c23e52d..90d2e6ce2 100644 --- a/src/import/stackprof.ts +++ b/src/import/stackprof.ts @@ -33,9 +33,9 @@ export function importFromStackprof(stackprofProfile: StackprofProfile): Profile let stack: FrameInfo[] = [] for (let j = 0; j < stackHeight; j++) { const id = raw[i++] - let frameName = frames[id].name; + let frameName = frames[id].name if (frameName == null) { - frameName = '(unknown)'; + frameName = '(unknown)' } const frame = { key: id, diff --git a/src/import/trace-event.test.ts b/src/import/trace-event.test.ts index 6a8e956c6..75fe547c2 100644 --- a/src/import/trace-event.test.ts +++ b/src/import/trace-event.test.ts @@ -122,3 +122,11 @@ test('importTraceEvents invalid x nesting', async () => { test('importTraceEvents event reordering name match', async () => { await checkProfileSnapshot('./sample/profiles/trace-event/event-reordering-name-match.json') }) + +test('importTraceEvents simple profile with samples', async () => { + await checkProfileSnapshot('./sample/profiles/trace-event/simple-with-samples.json') +}) + +test('importTraceEvents multi-thread profile with samples', async () => { + await checkProfileSnapshot('./sample/profiles/trace-event/multi-thread-with-samples.json') +}) diff --git a/src/import/trace-event.ts b/src/import/trace-event.ts index 642707075..3396d8c2f 100644 --- a/src/import/trace-event.ts +++ b/src/import/trace-event.ts @@ -1,5 +1,11 @@ import {sortBy, zeroPad, getOrInsert, lastOf} from '../lib/utils' -import {ProfileGroup, CallTreeProfileBuilder, FrameInfo, Profile} from '../lib/profile' +import { + ProfileGroup, + CallTreeProfileBuilder, + FrameInfo, + Profile, + StackListProfileBuilder, +} from '../lib/profile' import {TimeFormatter} from '../lib/value-formatters' // This file concerns import from the "Trace Event Format", authored by Google @@ -65,16 +71,53 @@ interface XTraceEvent extends TraceEvent { // The trace format supports a number of event types that we ignore. type ImportableTraceEvent = BTraceEvent | ETraceEvent | XTraceEvent +interface StackFrame { + line: string + column: string + funcLine: string + funcColumn: string + name: string + category: string + // A parent function may or may not exist + parent?: number +} + +interface Sample { + cpu: string + name: string + ts: string + pid: number + tid: string + weight: string + // Will refer to an element in the stackFrames object + sf: number + stackFrameData?: StackFrame +} + +interface TraceWithSamples { + traceEvents: TraceEvent[] + samples: Sample[] + stackFrames: {[key: string]: StackFrame} +} + +interface TraceEventObject { + traceEvents: TraceEvent[] +} + +type Trace = TraceEvent[] | TraceEventObject | TraceWithSamples + function pidTidKey(pid: number, tid: number): string { // We zero-pad the PID and TID to make sorting them by pid/tid pair later easier. return `${zeroPad('' + pid, 10)}:${zeroPad('' + tid, 10)}` } -function partitionByPidTid(events: ImportableTraceEvent[]): Map { - const map = new Map() +function partitionByPidTid( + events: T[], +): Map { + const map = new Map() for (let ev of events) { - const list = getOrInsert(map, pidTidKey(ev.pid, ev.tid), () => []) + const list = getOrInsert(map, pidTidKey(Number(ev.pid), Number(ev.tid)), () => []) list.push(ev) } @@ -244,110 +287,150 @@ function frameInfoForEvent(event: TraceEvent): FrameInfo { } } -function eventListToProfileGroup(events: TraceEvent[]): ProfileGroup { - const importableEvents = filterIgnoredEventTypes(events) - const partitioned = partitionByPidTid(importableEvents) - +/** + * Constructs an array mapping pid-tid keys to profile builders. Both the traceEvent[] + * format and the sample + stack frame based object format specify the process and thread + * names based on metadata so we share this logic. + * + * See https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview#heading=h.xqopa5m0e28f + */ +function getProfileNameByPidTid( + events: TraceEvent[], + partitionedTraceEvents: Map, +): Map { const processNamesByPid = getProcessNamesByPid(events) const threadNamesByPidTid = getThreadNamesByPidTid(events) - const profilePairs: [string, Profile][] = [] - - partitioned.forEach(eventsForThread => { - if (eventsForThread.length === 0) return + const profileNamesByPidTid = new Map() - const {pid, tid} = eventsForThread[0] + partitionedTraceEvents.forEach(importableEvents => { + if (importableEvents.length === 0) return - const profile = new CallTreeProfileBuilder() - profile.setValueFormatter(new TimeFormatter('microseconds')) + const {pid, tid} = importableEvents[0] + const profileKey = pidTidKey(pid, tid) const processName = processNamesByPid.get(pid) - const threadName = threadNamesByPidTid.get(pidTidKey(pid, tid)) + const threadName = threadNamesByPidTid.get(profileKey) if (processName != null && threadName != null) { - profile.setName(`${processName} (pid ${pid}), ${threadName} (tid ${tid})`) + profileNamesByPidTid.set( + profileKey, + `${processName} (pid ${pid}), ${threadName} (tid ${tid})`, + ) } else if (processName != null) { - profile.setName(`${processName} (pid ${pid}, tid ${tid})`) + profileNamesByPidTid.set(profileKey, `${processName} (pid ${pid}, tid ${tid})`) } else if (threadName != null) { - profile.setName(`${threadName} (pid ${pid}, tid ${tid})`) + profileNamesByPidTid.set(profileKey, `${threadName} (pid ${pid}, tid ${tid})`) } else { - profile.setName(`pid ${pid}, tid ${tid}`) + profileNamesByPidTid.set(profileKey, `pid ${pid}, tid ${tid}`) } + }) - // The trace event format is hard to deal with because it specifically - // allows events to be recorded out of order, *but* event ordering is still - // important for events with the same timestamp. Because of this, rather - // than thinking about the entire event stream as a single queue of events, - // we're going to first construct two time-ordered lists of events: - // - // 1. ts ordered queue of 'B' events - // 2. ts ordered queue of 'E' events - // - // We deal with 'X' events by converting them to one entry in the 'B' event - // queue and one entry in the 'E' event queue. - // - // The high level goal is to deal with 'B' events in 'ts' order, breaking - // ties by the order the events occurred in the file, and deal with 'E' - // events in 'ts' order, breaking ties in whatever order causes the 'E' - // events to match whatever is on the top of the stack. - const [bEventQueue, eEventQueue] = convertToEventQueues(eventsForThread) - - const frameStack: BTraceEvent[] = [] - const enterFrame = (b: BTraceEvent) => { - frameStack.push(b) - profile.enterFrame(frameInfoForEvent(b), b.ts) - } + return profileNamesByPidTid +} - const tryToLeaveFrame = (e: ETraceEvent) => { - const b = lastOf(frameStack) +function eventListToProfile( + importableEvents: ImportableTraceEvent[], + name: string, +): Profile { + // The trace event format is hard to deal with because it specifically + // allows events to be recorded out of order, *but* event ordering is still + // important for events with the same timestamp. Because of this, rather + // than thinking about the entire event stream as a single queue of events, + // we're going to first construct two time-ordered lists of events: + // + // 1. ts ordered queue of 'B' events + // 2. ts ordered queue of 'E' events + // + // We deal with 'X' events by converting them to one entry in the 'B' event + // queue and one entry in the 'E' event queue. + // + // The high level goal is to deal with 'B' events in 'ts' order, breaking + // ties by the order the events occurred in the file, and deal with 'E' + // events in 'ts' order, breaking ties in whatever order causes the 'E' + // events to match whatever is on the top of the stack. + const [bEventQueue, eEventQueue] = convertToEventQueues(importableEvents) + + const profileBuilder = new CallTreeProfileBuilder() + profileBuilder.setValueFormatter(new TimeFormatter('microseconds')) + profileBuilder.setName(name) + + const frameStack: BTraceEvent[] = [] + const enterFrame = (b: BTraceEvent) => { + frameStack.push(b) + profileBuilder.enterFrame(frameInfoForEvent(b), b.ts) + } - if (b == null) { - console.warn( - `Tried to end frame "${ - frameInfoForEvent(e).key - }", but the stack was empty. Doing nothing instead.`, - ) - return - } + const tryToLeaveFrame = (e: ETraceEvent) => { + const b = lastOf(frameStack) - const eFrameInfo = frameInfoForEvent(e) - const bFrameInfo = frameInfoForEvent(b) + if (b == null) { + console.warn( + `Tried to end frame "${ + frameInfoForEvent(e).key + }", but the stack was empty. Doing nothing instead.`, + ) + return + } - if (e.name !== b.name) { - console.warn( - `ts=${e.ts}: Tried to end "${eFrameInfo.key}" when "${bFrameInfo.key}" was on the top of the stack. Doing nothing instead.`, - ) - return - } + const eFrameInfo = frameInfoForEvent(e) + const bFrameInfo = frameInfoForEvent(b) - if (eFrameInfo.key !== bFrameInfo.key) { - console.warn( - `ts=${e.ts}: Tried to end "${eFrameInfo.key}" when "${bFrameInfo.key}" was on the top of the stack. Ending ${bFrameInfo.key} instead.`, - ) - } + if (e.name !== b.name) { + console.warn( + `ts=${e.ts}: Tried to end "${eFrameInfo.key}" when "${bFrameInfo.key}" was on the top of the stack. Doing nothing instead.`, + ) + return + } - frameStack.pop() - profile.leaveFrame(bFrameInfo, e.ts) + if (eFrameInfo.key !== bFrameInfo.key) { + console.warn( + `ts=${e.ts}: Tried to end "${eFrameInfo.key}" when "${bFrameInfo.key}" was on the top of the stack. Ending ${bFrameInfo.key} instead.`, + ) } - while (bEventQueue.length > 0 || eEventQueue.length > 0) { - const queueName = selectQueueToTakeFromNext(bEventQueue, eEventQueue) - switch (queueName) { - case 'B': { - enterFrame(bEventQueue.shift()!) - break - } - case 'E': { - // Before we take the first event in the 'E' queue, let's first see if - // there are any e events that exactly match the top of the stack. - // We'll prioritize first by key, then by name if we can't find a key - // match. - const stackTop = lastOf(frameStack) - if (stackTop != null) { - const bFrameInfo = frameInfoForEvent(stackTop) + frameStack.pop() + profileBuilder.leaveFrame(bFrameInfo, e.ts) + } + + while (bEventQueue.length > 0 || eEventQueue.length > 0) { + const queueName = selectQueueToTakeFromNext(bEventQueue, eEventQueue) + switch (queueName) { + case 'B': { + enterFrame(bEventQueue.shift()!) + break + } + case 'E': { + // Before we take the first event in the 'E' queue, let's first see if + // there are any e events that exactly match the top of the stack. + // We'll prioritize first by key, then by name if we can't find a key + // match. + const stackTop = lastOf(frameStack) + if (stackTop != null) { + const bFrameInfo = frameInfoForEvent(stackTop) + + let swapped: boolean = false + + for (let i = 1; i < eEventQueue.length; i++) { + const eEvent = eEventQueue[i] + if (eEvent.ts > eEventQueue[0].ts) { + // Only consider 'E' events with the same ts as the front of the queue. + break + } - let swapped: boolean = false + const eFrameInfo = frameInfoForEvent(eEvent) + if (bFrameInfo.key === eFrameInfo.key) { + // We have a match! Process this one first. + const temp = eEventQueue[0] + eEventQueue[0] = eEventQueue[i] + eEventQueue[i] = temp + swapped = true + break + } + } + if (!swapped) { + // There was no key match, let's see if we can find a name match for (let i = 1; i < eEventQueue.length; i++) { const eEvent = eEventQueue[i] if (eEvent.ts > eEventQueue[0].ts) { @@ -355,8 +438,7 @@ function eventListToProfileGroup(events: TraceEvent[]): ProfileGroup { break } - const eFrameInfo = frameInfoForEvent(eEvent) - if (bFrameInfo.key === eFrameInfo.key) { + if (eEvent.name === stackTop.name) { // We have a match! Process this one first. const temp = eEventQueue[0] eEventQueue[0] = eEventQueue[i] @@ -365,50 +447,166 @@ function eventListToProfileGroup(events: TraceEvent[]): ProfileGroup { break } } - - if (!swapped) { - // There was no key match, let's see if we can find a name match - for (let i = 1; i < eEventQueue.length; i++) { - const eEvent = eEventQueue[i] - if (eEvent.ts > eEventQueue[0].ts) { - // Only consider 'E' events with the same ts as the front of the queue. - break - } - - if (eEvent.name === stackTop.name) { - // We have a match! Process this one first. - const temp = eEventQueue[0] - eEventQueue[0] = eEventQueue[i] - eEventQueue[i] = temp - swapped = true - break - } - } - } - - // If swapped is still false at this point, it means we're about to - // pop a stack frame that doesn't even match by name. Bummer. } - const e = eEventQueue.shift()! - - tryToLeaveFrame(e) - break + // If swapped is still false at this point, it means we're about to + // pop a stack frame that doesn't even match by name. Bummer. } - default: - const _exhaustiveCheck: never = queueName - return _exhaustiveCheck + const e = eEventQueue.shift()! + + tryToLeaveFrame(e) + break } + + default: + const _exhaustiveCheck: never = queueName + return _exhaustiveCheck } + } + + for (let i = frameStack.length - 1; i >= 0; i--) { + const frame = frameInfoForEvent(frameStack[i]) + console.warn(`Frame "${frame.key}" was still open at end of profile. Closing automatically.`) + profileBuilder.leaveFrame(frame, profileBuilder.getTotalWeight()) + } + + return profileBuilder.build() +} + +/** + * Returns an array containing the time difference in microseconds between the current + * sample and the next sample + */ +function getTimeDeltasForSamples(samples: Sample[]): number[] { + const timeDeltas: number[] = [] + let lastTimeStamp = Number(samples[0].ts) + + samples.forEach((sample: Sample, idx: number) => { + if (idx === 0) return + + const timeDiff = Number(sample.ts) - lastTimeStamp + lastTimeStamp = Number(sample.ts) + timeDeltas.push(timeDiff) + }) + + timeDeltas.push(0) - for (let i = frameStack.length - 1; i >= 0; i--) { - const frame = frameInfoForEvent(frameStack[i]) - console.warn(`Frame "${frame.key}" was still open at end of profile. Closing automatically.`) - profile.leaveFrame(frame, profile.getTotalWeight()) + return timeDeltas +} + +/** + * The chrome json trace event spec only specifies name and category + * as required stack frame properties + * + * https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview#heading=h.b4y98p32171 + */ +function frameInfoForSampleFrame({name, category}: StackFrame): FrameInfo { + return { + key: `${name}:${category}`, + name: name, + } +} + +function getActiveFramesForSample( + stackFrames: {[key: string]: StackFrame}, + frameId: number, +): FrameInfo[] { + const frames = [] + let parent: number | undefined = frameId + + while (parent) { + const frame: StackFrame = stackFrames[parent] + + if (!frame) { + throw new Error(`Could not find frame for id ${parent}`) + } + + frames.push(frameInfoForSampleFrame(frame)) + parent = frame.parent + } + + return frames.reverse() +} + +function sampleListToProfile( + contents: TraceWithSamples, + samples: Sample[], + name: string, +): Profile { + const profileBuilder = new StackListProfileBuilder() + + profileBuilder.setValueFormatter(new TimeFormatter('microseconds')) + profileBuilder.setName(name) + + const timeDeltas = getTimeDeltasForSamples(samples) + + samples.forEach((sample, index) => { + const timeDelta = timeDeltas[index] + const activeFrames = getActiveFramesForSample(contents.stackFrames, sample.sf) + + profileBuilder.appendSampleWithWeight(activeFrames, timeDelta) + }) + + return profileBuilder.build() +} + +function eventListToProfileGroup(events: TraceEvent[]): ProfileGroup { + const importableEvents = filterIgnoredEventTypes(events) + const partitionedTraceEvents = partitionByPidTid(importableEvents) + const profileNamesByPidTid = getProfileNameByPidTid(events, partitionedTraceEvents) + + const profilePairs: [string, Profile][] = [] + + profileNamesByPidTid.forEach((name, profileKey) => { + const importableEventsForPidTid = partitionedTraceEvents.get(profileKey) + + if (!importableEventsForPidTid) { + throw new Error(`Could not find events for key: ${importableEventsForPidTid}`) } - profilePairs.push([pidTidKey(pid, tid), profile.build()]) + profilePairs.push([ + profileKey, + eventListToProfile(importableEventsForPidTid, name), + ]) + }) + + // For now, we just sort processes by pid & tid. + // TODO: The standard specifies that metadata events with the name + // "process_sort_index" and "thread_sort_index" can be used to influence the + // order, but for simplicity we'll ignore that until someone complains :) + sortBy(profilePairs, p => p[0]) + + return { + name: '', + indexToView: 0, + profiles: profilePairs.map(p => p[1]), + } +} + +function sampleListToProfileGroup(contents: TraceWithSamples): ProfileGroup { + const importableEvents = filterIgnoredEventTypes(contents.traceEvents) + const partitionedTraceEvents = partitionByPidTid(importableEvents) + const partitionedSamples = partitionByPidTid(contents.samples) + const profileNamesByPidTid = getProfileNameByPidTid(contents.traceEvents, partitionedTraceEvents) + + const profilePairs: [string, Profile][] = [] + + profileNamesByPidTid.forEach((name, profileKey) => { + const samplesForPidTid = partitionedSamples.get(profileKey) + + if (!samplesForPidTid) { + throw new Error(`Could not find samples for key: ${samplesForPidTid}`) + } + + if (samplesForPidTid.length === 0) { + return + } + + profilePairs.push([ + profileKey, + sampleListToProfile(contents, samplesForPidTid, name), + ]) }) // For now, we just sort processes by pid & tid. @@ -456,26 +654,33 @@ function isTraceEventList(maybeEventList: any): maybeEventList is TraceEvent[] { return true } -function isTraceEventObject( - maybeTraceEventObject: any, -): maybeTraceEventObject is {traceEvents: TraceEvent[]} { +function isTraceEventObject(maybeTraceEventObject: any): maybeTraceEventObject is TraceEventObject { if (!('traceEvents' in maybeTraceEventObject)) return false return isTraceEventList(maybeTraceEventObject['traceEvents']) } -export function isTraceEventFormatted( - rawProfile: any, -): rawProfile is {traceEvents: TraceEvent[]} | TraceEvent[] { +function isTraceEventWithSamples( + maybeTraceEventObject: any, +): maybeTraceEventObject is TraceWithSamples { + return ( + 'traceEvents' in maybeTraceEventObject && + 'stackFrames' in maybeTraceEventObject && + 'samples' in maybeTraceEventObject && + isTraceEventList(maybeTraceEventObject['traceEvents']) + ) +} + +export function isTraceEventFormatted(rawProfile: any): rawProfile is Trace { // We're only going to support the JSON formatted profiles for now. // The spec also discusses support for data embedded in ftrace supported data: https://lwn.net/Articles/365835/. return isTraceEventObject(rawProfile) || isTraceEventList(rawProfile) } -export function importTraceEvents( - rawProfile: {traceEvents: TraceEvent[]} | TraceEvent[], -): ProfileGroup { - if (isTraceEventObject(rawProfile)) { +export function importTraceEvents(rawProfile: Trace): ProfileGroup { + if (isTraceEventWithSamples(rawProfile)) { + return sampleListToProfileGroup(rawProfile) + } else if (isTraceEventObject(rawProfile)) { return eventListToProfileGroup(rawProfile.traceEvents) } else if (isTraceEventList(rawProfile)) { return eventListToProfileGroup(rawProfile) diff --git a/src/views/application.tsx b/src/views/application.tsx index ee1c22506..cf7ec9ec6 100644 --- a/src/views/application.tsx +++ b/src/views/application.tsx @@ -416,7 +416,7 @@ export class Application extends StatelessComponent { } async maybeLoadHashParamProfile() { - const {profileURL} = this.props.hashParams; + const {profileURL} = this.props.hashParams if (profileURL) { if (!canUseXHR) { alert( diff --git a/src/views/flamechart-pan-zoom-view.tsx b/src/views/flamechart-pan-zoom-view.tsx index 2cf5c0866..9734051e6 100644 --- a/src/views/flamechart-pan-zoom-view.tsx +++ b/src/views/flamechart-pan-zoom-view.tsx @@ -239,10 +239,7 @@ export class FlamechartPanZoomView extends Component { - if (!el) return - - const clientRect = el.getBoundingClientRect() - - // Place the hovertip to the right of the cursor. - let leftEdgeX = offset.x + OFFSET_FROM_MOUSE - - // If this would cause it to overflow the container, align the right - // edge of the hovertip with the right edge of the container. - if (leftEdgeX + clientRect.width > containerWidth - 1) { - leftEdgeX = containerWidth - clientRect.width - 1 - - // If aligning the right edge overflows the container, align the left edge - // of the hovertip with the left edge of the container. - if (leftEdgeX < 1) { leftEdgeX = 1 } - } - el.style.left = `${leftEdgeX}px` - - // Place the tooltip below the cursor - let topEdgeY = offset.y + OFFSET_FROM_MOUSE - - // If this would cause it to overflow the container, place the hovertip - // above the cursor instead. This intentionally differs from the horizontal - // axis logic to avoid the cursor being in the middle of a hovertip when - // possible. - if (topEdgeY + clientRect.height > containerHeight - 1) { - topEdgeY = offset.y - clientRect.height - 1 - - // If placing the hovertip above the cursor overflows the container, align - // the top edge of the hovertip with the top edge of the container. - if (topEdgeY < 1) { topEdgeY = 1 } - } - el.style.top = `${topEdgeY}px` - - }, [containerWidth, containerHeight, offset.x, offset.y]) + const updateLocation = useCallback( + (el: HTMLDivElement | null) => { + if (!el) return + + const clientRect = el.getBoundingClientRect() + + // Place the hovertip to the right of the cursor. + let leftEdgeX = offset.x + OFFSET_FROM_MOUSE + + // If this would cause it to overflow the container, align the right + // edge of the hovertip with the right edge of the container. + if (leftEdgeX + clientRect.width > containerWidth - 1) { + leftEdgeX = containerWidth - clientRect.width - 1 + + // If aligning the right edge overflows the container, align the left edge + // of the hovertip with the left edge of the container. + if (leftEdgeX < 1) { + leftEdgeX = 1 + } + } + el.style.left = `${leftEdgeX}px` + + // Place the tooltip below the cursor + let topEdgeY = offset.y + OFFSET_FROM_MOUSE + + // If this would cause it to overflow the container, place the hovertip + // above the cursor instead. This intentionally differs from the horizontal + // axis logic to avoid the cursor being in the middle of a hovertip when + // possible. + if (topEdgeY + clientRect.height > containerHeight - 1) { + topEdgeY = offset.y - clientRect.height - 1 + + // If placing the hovertip above the cursor overflows the container, align + // the top edge of the hovertip with the top edge of the container. + if (topEdgeY < 1) { + topEdgeY = 1 + } + } + el.style.top = `${topEdgeY}px` + }, + [containerWidth, containerHeight, offset.x, offset.y], + ) return (