Chrome timeline import, optimization of mergeAdjacentFrames

This commit is contained in:
Jamie Wong 2017-12-30 22:48:30 -05:00
parent e5d7cecda6
commit c240ac8694
5 changed files with 26060 additions and 17 deletions

View File

@ -4,6 +4,7 @@ import {ReloadableComponent} from './reloadable'
import {importFromBGFlameGraph} from './import/bg-flamegraph'
import {importFromStackprof} from './import/stackprof'
import {importFromChrome} from './import/chrome'
import {Profile} from './profile'
import {Flamechart} from './flamechart'
@ -41,14 +42,25 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
}
}
importJSON(parsed: any): Profile {
if (Array.isArray(parsed) && parsed[parsed.length - 1].name === "CpuProfile") {
return importFromChrome(parsed)
} else {
return importFromStackprof(parsed)
}
}
onDrop = (ev: DragEvent) => {
const file = ev.dataTransfer.files.item(0)
const reader = new FileReader
reader.addEventListener('loadend', () => {
const profile = file.name.endsWith('json') ? importFromStackprof(reader.result) : importFromBGFlameGraph(reader.result)
console.time('import')
const profile = file.name.endsWith('json') ? this.importJSON(JSON.parse(reader.result)) : importFromBGFlameGraph(reader.result)
const flamechart = new Flamechart(profile)
const sortedFlamechart = new Flamechart(profile.sortedAlphabetically())
this.setState({profile, flamechart, sortedFlamechart})
this.setState({ profile, flamechart, sortedFlamechart }, () => {
console.timeEnd('import')
})
})
reader.readAsText(file)
ev.preventDefault()

View File

@ -54,13 +54,13 @@ export class Flamechart {
return true
}
private static mergeFrames(first: FlamechartFrame, second: FlamechartFrame): FlamechartFrame {
private static mergeFrames(frames: FlamechartFrame[]): FlamechartFrame {
const frame: FlamechartFrame = {
node: first.node,
start: first.start,
end: second.end,
parent: first.parent,
children: first.children.concat(second.children)
node: frames[0].node,
start: frames[0].start,
end: frames[frames.length-1].end,
parent: frames[0].parent,
children: ([] as FlamechartFrame[]).concat(...frames.map(f => f.children))
}
for (let child of frame.children) {
child.parent = frame
@ -70,14 +70,22 @@ export class Flamechart {
private static mergeAdjacentFrames(layer: StackLayer): StackLayer {
const ret: StackLayer = []
let partition: FlamechartFrame[] = []
for (let flamechartFrame of layer) {
const prev = ret.length > 0 ? ret[ret.length - 1] : null
if (prev && Flamechart.shouldMergeFrames(prev, flamechartFrame)) {
ret.pop()
ret.push(Flamechart.mergeFrames(prev, flamechartFrame))
} else {
ret.push(flamechartFrame)
const prev: FlamechartFrame | undefined = partition[partition.length-1]
if (prev && !Flamechart.shouldMergeFrames(prev, flamechartFrame)) {
if (partition.length > 0) {
ret.push(Flamechart.mergeFrames(partition))
}
partition = []
}
partition.push(flamechartFrame)
}
if (partition.length > 0) {
ret.push(Flamechart.mergeFrames(partition))
}
return ret
}

108
import/chrome.ts Normal file
View File

@ -0,0 +1,108 @@
import {Profile, FrameInfo} from '../profile'
interface TimelineEvent {
pid: number,
tid: number,
ts: number,
ph: string,
cat: string,
name: string,
dur: number,
tdur: number,
tts: number,
args: { [key: string]: any }
}
interface PositionTickInfo {
line: number,
ticks: number
}
interface CPUProfileNode {
callFrame: {
columnNumber: number,
functionName: string,
lineNumber: number,
scriptId: string,
url: string
},
hitCount: number
id: number
children?: number[]
positionTicks?: PositionTickInfo[]
parent?: CPUProfileNode
}
interface CPUProfile {
startTime: number,
endTime: number,
nodes: CPUProfileNode[],
samples: number[],
timeDeltas: number[]
}
export function importFromChrome(events: TimelineEvent[]) {
const profileEvent = events[events.length - 1]
const chromeProfile = profileEvent.args.data.cpuProfile as CPUProfile
const profile = new Profile(chromeProfile.endTime - chromeProfile.startTime)
const nodeById = new Map<number, CPUProfileNode>()
for (let node of chromeProfile.nodes) {
nodeById.set(node.id, node)
}
for (let node of chromeProfile.nodes) {
if (!node.children) continue
for (let childId of node.children) {
const child = nodeById.get(childId)
if (!child) continue
child.parent = node
}
}
const samples: number[] = []
const timeDeltas: number[] = []
let elapsed = 0
let lastNodeId = NaN
// The chrome CPU profile format doesn't collapse identical samples. We'll do that
// here to save a ton of work later doing mergers.
for (let i = 0; i < chromeProfile.samples.length; i++) {
const nodeId = chromeProfile.samples[i]
if (nodeId != lastNodeId) {
samples.push(nodeId)
timeDeltas.push(elapsed)
elapsed = 0
}
elapsed += chromeProfile.timeDeltas[i]
lastNodeId = nodeId
}
if (!isNaN(lastNodeId)) {
samples.push(lastNodeId)
timeDeltas.push(elapsed)
}
for (let i = 0; i < samples.length; i++) {
const timeDelta = timeDeltas[i]
const nodeId = samples[i]
let node = nodeById.get(nodeId)
if (!node) continue
// TODO(jlfwong): This is silly and slow, but good enough for now
const stack: FrameInfo[] = []
for (let node = nodeById.get(nodeId); node; node = node.parent) {
stack.push({
key: node.id,
name: node.callFrame.functionName,
file: node.callFrame.url,
line: node.callFrame.lineNumber,
col: node.callFrame.columnNumber
})
}
stack.reverse()
profile.appendSample(stack, timeDelta)
}
return profile
}

View File

@ -14,9 +14,7 @@ export interface StackprofProfile {
raw_timestamp_deltas: number[]
}
export function importFromStackprof(contents: string): Profile {
const stackprofProfile = JSON.parse(contents) as StackprofProfile
export function importFromStackprof(stackprofProfile: StackprofProfile): Profile {
const duration = stackprofProfile.raw_timestamp_deltas.reduce((a, b) => a + b, 0)
const profile = new Profile(duration)

25917
sample/chrome-timeline.json Normal file

File diff suppressed because one or more lines are too long