Compare commits

...

5 Commits

Author SHA1 Message Date
Zachary Marion
2b04b6f9f5 Remove console log 2023-12-24 22:33:01 -05:00
Zachary Marion
d6639f42f3 Fix 2023-12-24 16:25:07 -05:00
Zachary Marion
0c900275f9 Working trace format with samples 2023-12-24 15:58:32 -05:00
Zachary Marion
357f4747db More scaffolding 2023-12-24 13:12:13 -05:00
Zachary Marion
c1e2271334 Scaffold out new approach 2023-12-24 13:05:57 -05:00
2 changed files with 172 additions and 287 deletions

View File

@ -1,229 +0,0 @@
import {FrameInfo} from '../lib/profile'
import {lastOf} from '../lib/utils'
import {ProfileBuilderInfo, Sample, StackFrame, TraceEventJsonObject} from './trace-event'
/**
* 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 frameInfoForEvent({name, category}: StackFrame): FrameInfo {
return {
key: `${name}:${category}`,
name: name,
}
}
/**
* Initialization function to enable O(1) access to the set of active nodes in the stack by node ID.
*/
function getActiveNodeArrays(profile: TraceEventJsonObject): Map<number, number[]> {
const map: Map<number, number[]> = new Map<number, number[]>()
// Given a nodeId, `getActiveNodes` gets all the parent nodes in reversed call order
const getActiveNodes = (id: number): number[] => {
if (map.has(id)) return map.get(id) || []
const node = profile.stackFrames[id]
if (!node) throw new Error(`No such node ${id}`)
if (node.parent) {
const array = getActiveNodes(node.parent).concat([id])
map.set(id, array)
return array
} else {
return [id]
}
}
Object.keys(profile.stackFrames).forEach(nodeId => {
const id = Number(nodeId)
map.set(id, getActiveNodes(id))
})
return map
}
/**
* Returns an array containing the time difference in microseconds between the previous
* sample and the current sample
*/
function getTimeDeltas(samples: Sample[]) {
const timeDeltas: number[] = []
let lastTimeStamp = Number(samples[0].ts)
samples.forEach((sample: Sample, idx: number) => {
if (idx === 0) {
timeDeltas.push(0)
} else {
const timeDiff = Number(sample.ts) - lastTimeStamp
lastTimeStamp = Number(sample.ts)
timeDeltas.push(timeDiff)
}
})
return timeDeltas
}
export function constructProfileFromJsonObject(
contents: TraceEventJsonObject,
samplesForPidTid: Sample[],
{profileBuilder}: ProfileBuilderInfo,
) {
const activeNodeArraysById = getActiveNodeArrays(contents)
/**
* The json object format maintains an object of stack frames where the
* key is the frame id and the value is the stack frame object.
*/
function getFrameById(frameId: string | number): StackFrame {
return contents.stackFrames[String(frameId)]
}
/**
* Wrapper function to get the active nodes for a given node id. We should
* always have active nodes for any given node.
*/
function getActiveNodeIds(nodeId: number): number[] {
const activeNodeIds = activeNodeArraysById.get(nodeId)
if (!activeNodeIds) throw new Error(`No such node ID ${nodeId}`)
return activeNodeIds
}
// We need to leave frames in the same order that we start them, so we keep a stack
// of frames that are currently open
const frameStack: StackFrame[] = []
/**
* Enter a frame, pushing it to the top of the stack so that we keep track of what functions
* are currently being executed
*/
function enterFrame(frame: StackFrame, timestamp: number) {
frameStack.push(frame)
profileBuilder.enterFrame(frameInfoForEvent(frame), timestamp)
}
/**
* Attempt to leave a frame. First we check if the frame matches what we expect to be the
* next thing to leave (top of the stack). If this is not the case we warn, and then leave
* the frame at the top of the stack
*/
function tryToLeaveFrame(frame: StackFrame, timestamp: number) {
const lastActiveFrame = lastOf(frameStack)
if (lastActiveFrame == null) {
console.warn(
`Tried to end frame "${
frameInfoForEvent(frame).key
}", but the stack was empty. Doing nothing instead.`,
)
return
}
const frameInfo = frameInfoForEvent(frame)
const lastActiveFrameInfo = frameInfoForEvent(lastActiveFrame)
if (frame.name !== lastActiveFrame.name) {
console.warn(
`ts=${timestamp}: Tried to end "${frameInfo.key}" when "${lastActiveFrameInfo.key}" was on the top of the stack. Doing nothing instead.`,
)
return
}
if (frameInfo.key !== lastActiveFrameInfo.key) {
console.warn(
`ts=${timestamp}: Tried to end "${frameInfo.key}" when "${lastActiveFrameInfo.key}" was on the top of the stack. Ending ${lastActiveFrameInfo.key} instead.`,
)
}
frameStack.pop()
profileBuilder.leaveFrame(lastActiveFrameInfo, timestamp)
}
/**
* Handle opening and closing the appropriate frames at a given timestamp
*
* @param activeNodeIds - The ids of the functions that are active at this timestamp
* @param lastActiveNodeIds - The ids of the functions active at the previous timestamp
* @param timestamp - The current timestamp (microseconds)
*/
function handleSample(activeNodeIds: number[], lastActiveNodeIds: number[], timestamp: number) {
// Frames which are present only in the currentNodeIds and not in lastActiveNodeIds
const startFrameIds = activeNodeIds.filter(id => !lastActiveNodeIds.includes(id))
// Frames which are present only in the PreviousNodeIds and not in activeNodeIds
const endFrameIds = lastActiveNodeIds.filter(id => !activeNodeIds.includes(id))
// Before we take the first event in the end ids, let's first see if there are any
// end 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.
while (endFrameIds.length > 0) {
const stackTop = lastOf(frameStack)
if (stackTop != null) {
const bFrameInfo = frameInfoForEvent(stackTop)
let swapped: boolean = false
for (let i = 1; i < endFrameIds.length; i++) {
const eEvent = getFrameById(endFrameIds[i])
const eFrameInfo = frameInfoForEvent(eEvent)
if (bFrameInfo.key === eFrameInfo.key) {
// We have a match! Process this one first.
const temp = endFrameIds[0]
endFrameIds[0] = endFrameIds[i]
endFrameIds[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 < endFrameIds.length; i++) {
const eEvent = getFrameById(endFrameIds[i])
if (eEvent.name === stackTop.name) {
// We have a match! Process this one first.
const temp = endFrameIds[0]
endFrameIds[0] = endFrameIds[i]
endFrameIds[i] = temp
swapped = true
break
}
}
}
}
const endFrameId = endFrameIds.shift()!
tryToLeaveFrame(getFrameById(endFrameId), timestamp)
}
startFrameIds.forEach(frameId => {
const frame = getFrameById(frameId)
enterFrame(frame, timestamp)
})
}
let currentTimestamp = 0
let lastActiveNodeIds: number[] = []
const timeDeltas = getTimeDeltas(samplesForPidTid)
for (let i = 0; i < samplesForPidTid.length; i++) {
const nodeId = samplesForPidTid[i].sf
const timeDelta = Math.max(timeDeltas[i], 0)
const node = getFrameById(nodeId)
if (!node) throw new Error(`Missing node ${nodeId}`)
currentTimestamp += timeDelta
const activeNodeIds = getActiveNodeIds(nodeId)
handleSample(activeNodeIds, lastActiveNodeIds, currentTimestamp)
lastActiveNodeIds = activeNodeIds
}
handleSample([], lastActiveNodeIds, currentTimestamp)
}

View File

@ -1,7 +1,12 @@
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'
import {constructProfileFromJsonObject} from './trace-event-json'
// This file concerns import from the "Trace Event Format", authored by Google
// and used for Google's own chrome://trace.
@ -66,7 +71,7 @@ interface XTraceEvent extends TraceEvent {
// The trace format supports a number of event types that we ignore.
type ImportableTraceEvent = BTraceEvent | ETraceEvent | XTraceEvent
export interface StackFrame {
interface StackFrame {
line: string
column: string
funcLine: string
@ -77,7 +82,7 @@ export interface StackFrame {
parent?: number
}
export interface Sample {
interface Sample {
cpu: string
name: string
ts: string
@ -89,12 +94,18 @@ export interface Sample {
stackFrameData?: StackFrame
}
export interface TraceEventJsonObject {
interface TraceWithSamples {
traceEvents: TraceEvent[]
samples: Sample[]
stackFrames: {[key in 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)}`
@ -290,44 +301,45 @@ export type ProfileBuilderInfo = {
*
* See https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview#heading=h.xqopa5m0e28f
*/
function partitionToProfileBuilderPairs(events: TraceEvent[]): [string, ProfileBuilderInfo][] {
const importableEvents = filterIgnoredEventTypes(events)
const partitionedTraceEvents = partitionByPidTid(importableEvents)
function getProfileNameByPidTid(
events: TraceEvent[],
partitionedTraceEvents: Map<string, TraceEvent[]>,
): Map<string, string> {
const processNamesByPid = getProcessNamesByPid(events)
const threadNamesByPidTid = getThreadNamesByPidTid(events)
const profilePairs: [string, ProfileBuilderInfo][] = []
const profileNamesByPidTid = new Map<string, string>()
partitionedTraceEvents.forEach(importableEvents => {
if (importableEvents.length === 0) return
const {pid, tid} = importableEvents[0]
const profileBuilder = new CallTreeProfileBuilder()
profileBuilder.setValueFormatter(new TimeFormatter('microseconds'))
const profileKey = pidTidKey(pid, tid)
const processName = processNamesByPid.get(pid)
const threadName = threadNamesByPidTid.get(profileKey)
if (processName != null && threadName != null) {
profileBuilder.setName(`${processName} (pid ${pid}), ${threadName} (tid ${tid})`)
profileNamesByPidTid.set(
profileKey,
`${processName} (pid ${pid}), ${threadName} (tid ${tid})`,
)
} else if (processName != null) {
profileBuilder.setName(`${processName} (pid ${pid}, tid ${tid})`)
profileNamesByPidTid.set(profileKey, `${processName} (pid ${pid}, tid ${tid})`)
} else if (threadName != null) {
profileBuilder.setName(`${threadName} (pid ${pid}, tid ${tid})`)
profileNamesByPidTid.set(profileKey, `${threadName} (pid ${pid}, tid ${tid})`)
} else {
profileBuilder.setName(`pid ${pid}, tid ${tid}`)
profileNamesByPidTid.set(profileKey, `pid ${pid}, tid ${tid}`)
}
profilePairs.push([profileKey, {pid, tid, profileBuilder, importableEvents}])
})
return profilePairs
return profileNamesByPidTid
}
function constructProfileFromTraceEvents({profileBuilder, importableEvents}: ProfileBuilderInfo) {
function constructProfileFromTraceEvents(
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
@ -346,6 +358,10 @@ function constructProfileFromTraceEvents({profileBuilder, importableEvents}: Pro
// 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)
@ -461,21 +477,137 @@ function constructProfileFromTraceEvents({profileBuilder, importableEvents}: Pro
console.warn(`Frame "${frame.key}" was still open at end of profile. Closing automatically.`)
profileBuilder.leaveFrame(frame, profileBuilder.getTotalWeight())
}
return profileBuilder.build()
}
/**
* Partition by thread and then build the profile appropriately based on the format
* Returns an array containing the time difference in microseconds between the previous
* sample and the current sample
*/
function constructProfileGroup(
events: TraceEvent[],
buildFunction: (info: ProfileBuilderInfo) => void,
): ProfileGroup {
const profileBuilderPairs = partitionToProfileBuilderPairs(events)
function getTimeDeltasForSamples(samples: Sample[]) {
const timeDeltas: number[] = []
let lastTimeStamp = Number(samples[0].ts)
const profilePairs = profileBuilderPairs.map(([key, info]): [string, Profile] => {
buildFunction(info)
samples.forEach((sample: Sample, idx: number) => {
if (idx === 0) {
timeDeltas.push(0)
} else {
const timeDiff = Number(sample.ts) - lastTimeStamp
lastTimeStamp = Number(sample.ts)
timeDeltas.push(timeDiff)
}
})
return [key, info.profileBuilder.build()]
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 getActiveFrames(
stackFrames: TraceWithSamples['stackFrames'],
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 constructProfileFromSampleList(
contents: TraceWithSamples,
samples: Sample[],
name: string,
) {
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 = getActiveFrames(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, pidTidKey) => {
const importableEventsForPidTid = partitionedTraceEvents.get(pidTidKey)
if (!importableEventsForPidTid) {
throw new Error(`Could not find events for key: ${importableEventsForPidTid}`)
}
profilePairs.push([pidTidKey, constructProfileFromTraceEvents(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, pidTidKey) => {
const samplesForPidTid = partitionedSamples.get(pidTidKey)
if (!samplesForPidTid) {
throw new Error(`Could not find samples for key: ${samplesForPidTid}`)
}
if (samplesForPidTid.length === 0) {
return
}
profilePairs.push([pidTidKey, constructProfileFromSampleList(contents, samplesForPidTid, name)])
})
// For now, we just sort processes by pid & tid.
@ -525,54 +657,36 @@ function isTraceEventList(maybeEventList: any): maybeEventList is TraceEvent[] {
function isTraceEventListObject(
maybeTraceEventObject: any,
): maybeTraceEventObject is {traceEvents: TraceEvent[]} {
): maybeTraceEventObject is TraceEventObject {
if (!('traceEvents' in maybeTraceEventObject)) return false
return isTraceEventList(maybeTraceEventObject['traceEvents'])
}
function isTraceEventJsonObject(
function isTraceEventWithSamples(
maybeTraceEventObject: any,
): maybeTraceEventObject is TraceEventJsonObject {
): maybeTraceEventObject is TraceWithSamples {
return (
'traceEvents' in maybeTraceEventObject &&
'stackFrames' in maybeTraceEventObject &&
'samples' in maybeTraceEventObject &&
isTraceEventFormatted(maybeTraceEventObject['traceEvents'])
isTraceEventList(maybeTraceEventObject['traceEvents'])
)
}
export function isTraceEventFormatted(
rawProfile: any,
): rawProfile is {traceEvents: TraceEvent[]} | TraceEvent[] {
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 isTraceEventListObject(rawProfile) || isTraceEventList(rawProfile)
}
export function importTraceEvents(
rawProfile: {traceEvents: TraceEvent[]} | TraceEvent[] | TraceEventJsonObject,
): ProfileGroup {
if (isTraceEventJsonObject(rawProfile)) {
const samplesByPidTid = partitionByPidTid(rawProfile.samples)
function jsonObjectTraceBuilder(info: ProfileBuilderInfo) {
const {pid, tid} = info
const key = pidTidKey(pid, tid)
const samples = samplesByPidTid.get(key)
if (!samples) {
throw new Error(`Could not find samples for key: ${key}`)
}
return constructProfileFromJsonObject(rawProfile as TraceEventJsonObject, samples, info)
}
return constructProfileGroup(rawProfile.traceEvents, jsonObjectTraceBuilder)
export function importTraceEvents(rawProfile: Trace): ProfileGroup {
if (isTraceEventWithSamples(rawProfile)) {
return sampleListToProfileGroup(rawProfile)
} else if (isTraceEventListObject(rawProfile)) {
return constructProfileGroup(rawProfile.traceEvents, constructProfileFromTraceEvents)
return eventListToProfileGroup(rawProfile.traceEvents)
} else if (isTraceEventList(rawProfile)) {
return constructProfileGroup(rawProfile, constructProfileFromTraceEvents)
return eventListToProfileGroup(rawProfile)
} else {
const _exhaustiveCheck: never = rawProfile
return _exhaustiveCheck