Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hermes-specific support for the trace event format #458

Merged
merged 5 commits into from
Dec 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions sample/profiles/trace-event/simple-hermes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
[
{
"pid": 0,
"tid": 0,
"ph": "B",
"name": "[root]",
"ts": 0,
"args": {
"name": "[root]",
"category": "root",
"url": null,
"line": null,
"column": null,
"params": null,
"allocatedCategory": "root",
"allocatedName": "[root]"
}
},
{
"pid": 0,
"tid": 0,
"ph": "B",
"name": "beta",
"ts": 1,
"args": {
"line": 54,
"column": 12,
"funcLine": "1",
"funcColumn": "1",
"name": "beta",
"category": "JavaScript",
"parent": 1,
"url": "/Users/example/test_project/node_modules/metro-runtime/src/polyfills/require.js",
"params": null,
"allocatedCategory": "JavaScript",
"allocatedName": "beta"
}
},
{
"pid": 0,
"tid": 0,
"ph": "E",
"name": "beta",
"ts": 13,
"args": {
"line": 54,
"column": 12,
"funcLine": "1",
"funcColumn": "1",
"name": "beta",
"category": "JavaScript",
"parent": 1,
"url": "/Users/example/test_project/node_modules/metro-runtime/src/polyfills/require.js",
"params": null,
"allocatedCategory": "JavaScript",
"allocatedName": "beta"
}
},
{
"pid": 0,
"tid": 0,
"ph": "E",
"name": "[root]",
"ts": 14,
"args": {
"name": "[root]",
"category": "root",
"url": null,
"line": null,
"column": null,
"params": null,
"allocatedCategory": "root",
"allocatedName": "[root]"
}
}
]
35 changes: 35 additions & 0 deletions src/import/__snapshots__/trace-event.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1315,6 +1315,41 @@ Object {
}
`;

exports[`importTraceEvents simple hermes profile 1`] = `
Object {
"frames": Array [
Frame {
"col": null,
"file": null,
"key": "[root] {\\"name\\":\\"[root]\\",\\"category\\":\\"root\\",\\"url\\":null,\\"line\\":null,\\"column\\":null,\\"params\\":null,\\"allocatedCategory\\":\\"root\\",\\"allocatedName\\":\\"[root]\\"}",
"line": null,
"name": "[root]",
"selfWeight": 2,
"totalWeight": 14,
},
Frame {
"col": 12,
"file": "/Users/example/test_project/node_modules/metro-runtime/src/polyfills/require.js",
"key": "beta {\\"line\\":54,\\"column\\":12,\\"funcLine\\":\\"1\\",\\"funcColumn\\":\\"1\\",\\"name\\":\\"beta\\",\\"category\\":\\"JavaScript\\",\\"parent\\":1,\\"url\\":\\"/Users/example/test_project/node_modules/metro-runtime/src/polyfills/require.js\\",\\"params\\":null,\\"allocatedCategory\\":\\"JavaScript\\",\\"allocatedName\\":\\"beta\\"}",
"line": 54,
"name": "beta",
"selfWeight": 12,
"totalWeight": 12,
},
],
"name": "pid 0, tid 0",
"stacks": Array [
"[root] 1.00µs",
"[root];beta 12.00µs",
"[root] 1.00µs",
],
}
`;

exports[`importTraceEvents simple hermes profile: indexToView 1`] = `0`;

exports[`importTraceEvents simple hermes profile: profileGroup.name 1`] = `"simple-hermes.json"`;

exports[`importTraceEvents simple object 1`] = `
Object {
"frames": Array [
Expand Down
4 changes: 4 additions & 0 deletions src/import/trace-event.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ test('importTraceEvents event reordering name match', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/event-reordering-name-match.json')
})

test('importTraceEvents simple hermes profile', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/simple-hermes.json')
})

test('importTraceEvents simple profile with samples', async () => {
await checkProfileSnapshot('./sample/profiles/trace-event/simple-with-samples.json')
})
Expand Down
115 changes: 99 additions & 16 deletions src/import/trace-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,46 @@ interface TraceEvent {
cat?: string

// Any arguments provided for the event. Some of the event types have required argument fields, otherwise, you can put any information you wish in here. The arguments are displayed in Trace Viewer when you view an event in the analysis section.
args: any
args?: any
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the spec this is not a required field for some event types, so marked it as optional. Happy to change back if this was intentional

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems fine


// A fixed color name to associate with the event. If provided, cname must be one of the names listed in trace-viewer's base color scheme's reserved color names list
cname?: string
}

enum ExporterSource {
HERMES = 'HERMES',
UNKNOWN = 'UNKNOWN',
}

interface HermesTraceEventArgs {
line: number | null
column: number | null
funcLine?: string | null
funcColumn?: string | null
name: string
category: string
parent?: number
url: string | null
params: string | null
allocatedCategory: string
allocatedName: string
}

const requiredHermesArguments: Array<keyof HermesTraceEventArgs> = [
'line',
'column',
'name',
'category',
'url',
'params',
'allocatedCategory',
'allocatedName',
]

type HermesTraceEvent = TraceEvent & {
args: HermesTraceEventArgs
}

interface BTraceEvent extends TraceEvent {
ph: 'B'
}
Expand Down Expand Up @@ -151,7 +185,7 @@ function selectQueueToTakeFromNext(
// to ensure it opens before we try to close it.
//
// Otherwise, process the 'E' queue first.
return frameInfoForEvent(bFront).key === frameInfoForEvent(eFront).key ? 'B' : 'E'
return keyForEvent(bFront) === keyForEvent(eFront) ? 'B' : 'E'
}

function convertToEventQueues(events: ImportableTraceEvent[]): [BTraceEvent[], ETraceEvent[]] {
Expand Down Expand Up @@ -271,16 +305,36 @@ function getThreadNamesByPidTid(events: TraceEvent[]): Map<string, string> {
return threadNameByPidTid
}

function getEventName(event: TraceEvent): string {
return `${event.name || '(unnamed)'}`
}

function keyForEvent(event: TraceEvent): string {
let name = `${event.name || '(unnamed)'}`
let key = getEventName(event)
if (event.args) {
name += ` ${JSON.stringify(event.args)}`
key += ` ${JSON.stringify(event.args)}`
}
return name
return key
}

function frameInfoForEvent(event: TraceEvent): FrameInfo {
function frameInfoForEvent(
event: TraceEvent,
exporterSource: ExporterSource = ExporterSource.UNKNOWN,
): FrameInfo {
const key = keyForEvent(event)

// In Hermes profiles we have additional guaranteed metadata we can use to
// more accurately populate profiles with info such as line + col number
if (exporterSource === ExporterSource.HERMES) {
return {
name: getEventName(event),
key: key,
file: event.args.url,
line: event.args.line,
col: event.args.column,
}
}

return {
name: key,
key: key,
Expand Down Expand Up @@ -329,7 +383,11 @@ function getProfileNameByPidTid(
return profileNamesByPidTid
}

function eventListToProfile(importableEvents: ImportableTraceEvent[], name: string): Profile {
function eventListToProfile(
importableEvents: ImportableTraceEvent[],
name: string,
exporterSource: ExporterSource = ExporterSource.UNKNOWN,
): 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
Expand All @@ -355,7 +413,7 @@ function eventListToProfile(importableEvents: ImportableTraceEvent[], name: stri
const frameStack: BTraceEvent[] = []
const enterFrame = (b: BTraceEvent) => {
frameStack.push(b)
profileBuilder.enterFrame(frameInfoForEvent(b), b.ts)
profileBuilder.enterFrame(frameInfoForEvent(b, exporterSource), b.ts)
}

const tryToLeaveFrame = (e: ETraceEvent) => {
Expand All @@ -364,14 +422,14 @@ function eventListToProfile(importableEvents: ImportableTraceEvent[], name: stri
if (b == null) {
console.warn(
`Tried to end frame "${
frameInfoForEvent(e).key
frameInfoForEvent(e, exporterSource).key
}", but the stack was empty. Doing nothing instead.`,
)
return
}

const eFrameInfo = frameInfoForEvent(e)
const bFrameInfo = frameInfoForEvent(b)
const eFrameInfo = frameInfoForEvent(e, exporterSource)
const bFrameInfo = frameInfoForEvent(b, exporterSource)

if (e.name !== b.name) {
console.warn(
Expand Down Expand Up @@ -404,7 +462,7 @@ function eventListToProfile(importableEvents: ImportableTraceEvent[], name: stri
// match.
const stackTop = lastOf(frameStack)
if (stackTop != null) {
const bFrameInfo = frameInfoForEvent(stackTop)
const bFrameInfo = frameInfoForEvent(stackTop, exporterSource)

let swapped: boolean = false

Expand All @@ -415,7 +473,7 @@ function eventListToProfile(importableEvents: ImportableTraceEvent[], name: stri
break
}

const eFrameInfo = frameInfoForEvent(eEvent)
const eFrameInfo = frameInfoForEvent(eEvent, exporterSource)
if (bFrameInfo.key === eFrameInfo.key) {
// We have a match! Process this one first.
const temp = eEventQueue[0]
Expand Down Expand Up @@ -463,7 +521,7 @@ function eventListToProfile(importableEvents: ImportableTraceEvent[], name: stri
}

for (let i = frameStack.length - 1; i >= 0; i--) {
const frame = frameInfoForEvent(frameStack[i])
const frame = frameInfoForEvent(frameStack[i], exporterSource)
console.warn(`Frame "${frame.key}" was still open at end of profile. Closing automatically.`)
profileBuilder.leaveFrame(frame, profileBuilder.getTotalWeight())
}
Expand Down Expand Up @@ -544,7 +602,10 @@ function sampleListToProfile(contents: TraceWithSamples, samples: Sample[], name
return profileBuilder.build()
}

function eventListToProfileGroup(events: TraceEvent[]): ProfileGroup {
function eventListToProfileGroup(
events: TraceEvent[],
exporterSource: ExporterSource = ExporterSource.UNKNOWN,
): ProfileGroup {
const importableEvents = filterIgnoredEventTypes(events)
const partitionedTraceEvents = partitionByPidTid(importableEvents)
const profileNamesByPidTid = getProfileNameByPidTid(events, partitionedTraceEvents)
Expand All @@ -558,7 +619,10 @@ function eventListToProfileGroup(events: TraceEvent[]): ProfileGroup {
throw new Error(`Could not find events for key: ${importableEventsForPidTid}`)
}

profilePairs.push([profileKey, eventListToProfile(importableEventsForPidTid, name)])
profilePairs.push([
profileKey,
eventListToProfile(importableEventsForPidTid, name, exporterSource),
])
})

// For now, we just sort processes by pid & tid.
Expand Down Expand Up @@ -641,6 +705,23 @@ function isTraceEventList(maybeEventList: any): maybeEventList is TraceEvent[] {
return true
}

function isHermesTraceEvent(traceEventArgs: any): traceEventArgs is HermesTraceEventArgs {
if (!traceEventArgs) {
return false
}

return requiredHermesArguments.every(prop => prop in traceEventArgs)
}

function isHermesTraceEventList(maybeEventList: any): maybeEventList is HermesTraceEvent[] {
if (!isTraceEventList(maybeEventList)) return false

// We just check the first element to avoid iterating over all trace events,
// and asumme that if the first one is formatted like a hermes profile then
// all events will be
return isHermesTraceEvent(maybeEventList[0].args)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[0]?.args please!

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind, this is guarded in isTraceEventList

}

function isTraceEventObject(maybeTraceEventObject: any): maybeTraceEventObject is TraceEventObject {
if (!('traceEvents' in maybeTraceEventObject)) return false
return isTraceEventList(maybeTraceEventObject['traceEvents'])
Expand Down Expand Up @@ -669,6 +750,8 @@ export function importTraceEvents(rawProfile: Trace): ProfileGroup {
return sampleListToProfileGroup(rawProfile)
} else if (isTraceEventObject(rawProfile)) {
return eventListToProfileGroup(rawProfile.traceEvents)
} else if (isHermesTraceEventList(rawProfile)) {
return eventListToProfileGroup(rawProfile, ExporterSource.HERMES)
} else if (isTraceEventList(rawProfile)) {
return eventListToProfileGroup(rawProfile)
} else {
Expand Down