Conversion to more efficient flamechart construction methodology

This commit is contained in:
Jamie Wong 2017-12-31 14:57:38 -05:00
parent 9fbef41305
commit 5932111819
4 changed files with 72 additions and 80 deletions

View File

@ -22,12 +22,13 @@ interface FlamechartFrameLabel {
}
function binarySearch(lo: number, hi: number, f: (val: number) => number, target: number, targetRangeSize = 1): [number, number] {
console.assert(!isNaN(targetRangeSize) && !isNaN(target))
while (true) {
if (hi - lo <= targetRangeSize) return [lo, hi]
const mid = (hi + lo) / 2
const val = f(mid)
if (val < target) lo = mid
if (val > target) hi = mid
else hi = mid
}
}

View File

@ -1,5 +1,7 @@
import {Profile, Frame, CallTreeNode} from './profile'
import { lastOf } from './utils'
interface FlamechartFrame {
node: CallTreeNode
start: number
@ -23,73 +25,6 @@ export class Flamechart {
getFrameColors() { return this.frameColors }
getMinFrameWidth() { return this.minFrameWidth }
private appendFrame(layerIndex: number, node: CallTreeNode, timeDelta: number, parent: FlamechartFrame | null) {
while (layerIndex >= this.layers.length) this.layers.push([])
const flamechartFrame: FlamechartFrame = {
node: node,
start: this.duration,
end: this.duration + timeDelta,
parent,
children: []
}
this.layers[layerIndex].push(flamechartFrame)
if (parent) {
parent.children.push(flamechartFrame)
}
return flamechartFrame
}
private appendSample(stack: CallTreeNode[], timeDelta: number) {
let parent: FlamechartFrame | null = null
for (let i = 0; i < stack.length; i++) {
parent = this.appendFrame(i, stack[i], timeDelta, parent)
}
this.duration += timeDelta
}
private static shouldMergeFrames(first: FlamechartFrame, second: FlamechartFrame): boolean {
if (first.node !== second.node) return false
if (first.parent !== second.parent) return false
if (first.end !== second.start) return false
return true
}
private static mergeFrames(frames: FlamechartFrame[]): FlamechartFrame {
const frame: FlamechartFrame = {
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
}
return frame
}
private static mergeAdjacentFrames(layer: StackLayer): StackLayer {
const ret: StackLayer = []
let partition: FlamechartFrame[] = []
for (let flamechartFrame of layer) {
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
}
private selectFrameColors(profile: Profile) {
const frames: Frame[] = []
@ -164,16 +99,35 @@ export class Flamechart {
}
constructor(private profile: Profile) {
profile.forEachSample(this.appendSample.bind(this))
this.layers = this.layers.map(Flamechart.mergeAdjacentFrames)
this.minFrameWidth = Infinity
for (let layer of this.layers) {
for (let frame of layer) {
const width = frame.end - frame.start
if (!width) continue
this.minFrameWidth = Math.min(this.minFrameWidth, width)
const stack: FlamechartFrame[] = []
const openFrame = (node: CallTreeNode, value: number) => {
const parent = lastOf(stack)
const frame: FlamechartFrame = {
node,
parent,
children: [],
start: value,
end: value,
}
if (parent) {
parent.children.push(frame)
}
stack.push(frame)
}
this.minFrameWidth = Infinity
const closeFrame = (node: CallTreeNode, value: number) => {
console.assert(stack.length > 0)
const stackTop = stack.pop()!
stackTop.end = value
const layerIndex = stack.length
while (this.layers.length <= layerIndex) this.layers.push([])
this.layers[layerIndex].push(stackTop)
this.minFrameWidth = Math.min(this.minFrameWidth, stackTop.end - stackTop.start)
}
this.duration = profile.getDuration()
profile.forEachCall(openFrame, closeFrame)
this.selectFrameColors(profile)
}
}

View File

@ -1,3 +1,5 @@
import { lastOf } from './utils'
export interface FrameInfo {
key: string | number
@ -84,10 +86,6 @@ function getOrInsert<K, V>(map: Map<K, V>, k: K, v: V): V {
return map.get(k)!
}
function lastOf<T>(ts: T[]): T | undefined {
return ts[ts.length-1]
}
export class Profile {
// Duration of the entire profile, in microseconds
private duration: number
@ -140,6 +138,41 @@ export class Profile {
}
}
forEachCall(
openFrame: (node: CallTreeNode, value: number) => void,
closeFrame: (node: CallTreeNode, value: number) => void
) {
let prevStack: CallTreeNode[] = []
let value = 0
let sampleIndex = 0
for (let stackTop of this.samples) {
// Close frames that are no longer open
while (prevStack.length > 0 && lastOf(prevStack) != stackTop) {
closeFrame(prevStack.pop()!, value)
}
// Open frames that are now becoming open
const toOpen: CallTreeNode[] = []
for (let node: CallTreeNode | null = stackTop; node && node != lastOf(prevStack); node = node.parent) {
toOpen.push(node)
}
for (let i = toOpen.length - 1; i >= 0; i--) {
const node = toOpen[i]
openFrame(node, value)
}
prevStack = prevStack.concat(toOpen)
value += this.timeDeltas[sampleIndex++]
}
// Close frames that are open at the end of the trace
for (let i = prevStack.length - 1; i >= 0; i--) {
closeFrame(prevStack[i], value)
}
}
forEachFrame(fn: (frame: Frame) => void) {
this.frames.forEach(fn)
}

View File

@ -10,3 +10,7 @@ export function atMostOnceAFrame<F extends Function>(fn: F) {
}
return ret as any as F
}
export function lastOf<T>(ts: T[]): T | null {
return ts[ts.length-1] || null
}