mirror of
https://github.com/jlfwong/speedscope.git
synced 2024-11-30 13:22:59 +03:00
2b9f7ffe1b
Instruments has a complex binary file format. If we're interested in just having a nice flamegraph display of the contents and don't care too much about time ordering or symbol file locations, then we can just grab the information we need from the clipboard rather than deal with the binary file format. This also avoids needing to deal with multiple processes or multiple threads. This PR contains 2 compressed `.trace` files. In each, if you select the top row in the call tree view and hit "Cmd+Shift+C" or go to "Edit -> Deep Copy", then paste into speedscope, you should get the corresponding flamechart. ## Allocations Profile ![image](https://user-images.githubusercontent.com/150329/39796943-5d900c88-530e-11e8-8dea-fa0a44888a64.png) ![image](https://user-images.githubusercontent.com/150329/39796949-65f6a9f4-530e-11e8-8509-64816cebe74c.png) ## Time Profile ![image](https://user-images.githubusercontent.com/150329/39796956-6fd88776-530e-11e8-9978-14aba8e883e1.png) ![image](https://user-images.githubusercontent.com/150329/39796973-8983189e-530e-11e8-8d82-92183c8590f6.png)
416 lines
11 KiB
TypeScript
416 lines
11 KiB
TypeScript
import {lastOf, getOrInsert} from './utils'
|
|
const demangleCppModule = import('./demangle-cpp')
|
|
|
|
// Force eager loading of the module
|
|
demangleCppModule.then(() => console.log('CPP demangler loaded'))
|
|
|
|
export interface FrameInfo {
|
|
key: string | number
|
|
|
|
// Name of the frame. May be a method name, e.g.
|
|
// "ActiveRecord##to_hash"
|
|
name: string
|
|
|
|
// File path of the code corresponding to this
|
|
// call stack frame.
|
|
file?: string
|
|
|
|
// Line in the given file where this frame occurs
|
|
line?: number
|
|
|
|
// Column in the file
|
|
col?: number
|
|
}
|
|
|
|
export class HasWeights {
|
|
private selfWeight = 0
|
|
private totalWeight = 0
|
|
getSelfWeight() {
|
|
return this.selfWeight
|
|
}
|
|
getTotalWeight() {
|
|
return this.totalWeight
|
|
}
|
|
addToTotalWeight(delta: number) {
|
|
this.totalWeight += delta
|
|
}
|
|
addToSelfWeight(delta: number) {
|
|
this.selfWeight += delta
|
|
}
|
|
}
|
|
|
|
export class Frame extends HasWeights {
|
|
key: string | number
|
|
|
|
// Name of the frame. May be a method name, e.g.
|
|
// "ActiveRecord##to_hash"
|
|
name: string
|
|
|
|
// File path of the code corresponding to this
|
|
// call stack frame.
|
|
file?: string
|
|
|
|
// Line in the given file where this frame occurs
|
|
line?: number
|
|
|
|
// Column in the file
|
|
col?: number
|
|
|
|
constructor(info: FrameInfo) {
|
|
super()
|
|
this.key = info.key
|
|
this.name = info.name
|
|
this.file = info.file
|
|
this.line = info.line
|
|
this.col = info.col
|
|
}
|
|
}
|
|
|
|
export class CallTreeNode extends HasWeights {
|
|
children: CallTreeNode[] = []
|
|
constructor(readonly frame: Frame, readonly parent: CallTreeNode | null) {
|
|
super()
|
|
}
|
|
}
|
|
|
|
const rootFrame = new Frame({
|
|
key: '(speedscope root)',
|
|
name: '(speedscope root)',
|
|
})
|
|
|
|
export interface ValueFormatter {
|
|
format(v: number): string
|
|
}
|
|
|
|
export class RawValueFormatter implements ValueFormatter {
|
|
format(v: number) {
|
|
return v.toLocaleString()
|
|
}
|
|
}
|
|
|
|
export class TimeFormatter implements ValueFormatter {
|
|
private multiplier: number
|
|
|
|
constructor(unit: 'nanoseconds' | 'microseconds' | 'milliseconds' | 'seconds') {
|
|
if (unit === 'nanoseconds') this.multiplier = 1e-9
|
|
else if (unit === 'microseconds') this.multiplier = 1e-6
|
|
else if (unit === 'milliseconds') this.multiplier = 1e-3
|
|
else this.multiplier = 1
|
|
}
|
|
|
|
format(v: number) {
|
|
const s = v * this.multiplier
|
|
|
|
if (s / 60 >= 1) return `${(s / 60).toFixed(2)}min`
|
|
if (s / 1 >= 1) return `${s.toFixed(2)}s`
|
|
if (s / 1e-3 >= 1) return `${(s / 1e-3).toFixed(2)}ms`
|
|
if (s / 1e-6 >= 1) return `${(s / 1e-6).toFixed(2)}µs`
|
|
else return `${(s / 1e-9).toFixed(2)}ms`
|
|
}
|
|
}
|
|
|
|
export class ByteFormatter implements ValueFormatter {
|
|
format(v: number) {
|
|
if (v < 1024) return `${v.toFixed(2)} B`
|
|
v /= 1024
|
|
if (v < 1024) return `${v.toFixed(2)} KB`
|
|
v /= 1024
|
|
if (v < 1024) return `${v.toFixed(2)} MB`
|
|
v /= 1024
|
|
return `${v.toFixed(2)} GB`
|
|
}
|
|
}
|
|
|
|
export class Profile {
|
|
private name: string = ''
|
|
|
|
private totalWeight: number
|
|
|
|
private frames = new Map<string | number, Frame>()
|
|
private appendOrderCalltreeRoot = new CallTreeNode(rootFrame, null)
|
|
private groupedCalltreeRoot = new CallTreeNode(rootFrame, null)
|
|
|
|
// List of references to CallTreeNodes at the top of the
|
|
// stack at the time of the sample.
|
|
private samples: CallTreeNode[] = []
|
|
private weights: number[] = []
|
|
|
|
private valueFormatter: ValueFormatter = new RawValueFormatter()
|
|
|
|
constructor(totalWeight: number = 0) {
|
|
this.totalWeight = totalWeight
|
|
}
|
|
|
|
formatValue(v: number) {
|
|
return this.valueFormatter.format(v)
|
|
}
|
|
setValueFormatter(f: ValueFormatter) {
|
|
this.valueFormatter = f
|
|
}
|
|
|
|
getName() {
|
|
return this.name
|
|
}
|
|
setName(name: string) {
|
|
this.name = name
|
|
}
|
|
|
|
getTotalWeight() {
|
|
return this.totalWeight
|
|
}
|
|
getTotalNonIdleWeight() {
|
|
return this.groupedCalltreeRoot.children.reduce((n, c) => n + c.getTotalWeight(), 0)
|
|
}
|
|
|
|
forEachCallGrouped(
|
|
openFrame: (node: CallTreeNode, value: number) => void,
|
|
closeFrame: (value: number) => void,
|
|
) {
|
|
function visit(node: CallTreeNode, start: number) {
|
|
if (node.frame !== rootFrame) {
|
|
openFrame(node, start)
|
|
}
|
|
|
|
let childTime = 0
|
|
|
|
const children = [...node.children]
|
|
children.sort((a, b) => (a.getTotalWeight() > b.getTotalWeight() ? -1 : 1))
|
|
|
|
children.forEach(function(child) {
|
|
visit(child, start + childTime)
|
|
childTime += child.getTotalWeight()
|
|
})
|
|
|
|
if (node.frame !== rootFrame) {
|
|
closeFrame(start + node.getTotalWeight())
|
|
}
|
|
}
|
|
visit(this.groupedCalltreeRoot, 0)
|
|
}
|
|
|
|
forEachCall(
|
|
openFrame: (node: CallTreeNode, value: number) => void,
|
|
closeFrame: (value: number) => void,
|
|
) {
|
|
let prevStack: CallTreeNode[] = []
|
|
let value = 0
|
|
|
|
let sampleIndex = 0
|
|
for (let stackTop of this.samples) {
|
|
// Find lowest common ancestor of the current stack and the previous one
|
|
let lca: CallTreeNode | null = null
|
|
|
|
// This is O(n^2), but n should be relatively small here (stack height),
|
|
// so hopefully this isn't much of a problem
|
|
for (
|
|
lca = stackTop;
|
|
lca && lca.frame != rootFrame && prevStack.indexOf(lca) === -1;
|
|
lca = lca.parent
|
|
) {}
|
|
|
|
// Close frames that are no longer open
|
|
while (prevStack.length > 0 && lastOf(prevStack) != lca) {
|
|
prevStack.pop()
|
|
closeFrame(value)
|
|
}
|
|
|
|
// Open frames that are now becoming open
|
|
const toOpen: CallTreeNode[] = []
|
|
for (
|
|
let node: CallTreeNode | null = stackTop;
|
|
node && node.frame != rootFrame && node != lca;
|
|
node = node.parent
|
|
) {
|
|
toOpen.push(node)
|
|
}
|
|
toOpen.reverse()
|
|
|
|
for (let node of toOpen) {
|
|
openFrame(node, value)
|
|
}
|
|
|
|
prevStack = prevStack.concat(toOpen)
|
|
value += this.weights[sampleIndex++]
|
|
}
|
|
|
|
// Close frames that are open at the end of the trace
|
|
for (let i = prevStack.length - 1; i >= 0; i--) {
|
|
closeFrame(value)
|
|
}
|
|
}
|
|
|
|
forEachFrame(fn: (frame: Frame) => void) {
|
|
this.frames.forEach(fn)
|
|
}
|
|
|
|
_appendSample(stack: FrameInfo[], weight: number, useAppendOrder: boolean) {
|
|
if (isNaN(weight)) throw new Error('invalid weight')
|
|
let node = useAppendOrder ? this.appendOrderCalltreeRoot : this.groupedCalltreeRoot
|
|
|
|
let framesInStack = new Set<Frame>()
|
|
|
|
for (let frameInfo of stack) {
|
|
const frame = getOrInsert(this.frames, frameInfo.key, () => new Frame(frameInfo))
|
|
const last = useAppendOrder
|
|
? lastOf(node.children)
|
|
: node.children.find(c => c.frame === frame)
|
|
if (last && last.frame == frame) {
|
|
node = last
|
|
} else {
|
|
const parent = node
|
|
node = new CallTreeNode(frame, node)
|
|
parent.children.push(node)
|
|
}
|
|
node.addToTotalWeight(weight)
|
|
|
|
// It's possible for the same frame to occur multiple
|
|
// times in the same call stack due to either direct
|
|
// or indirect recursion. We want to avoid counting that
|
|
// frame multiple times for a single sample, we so just
|
|
// track all of the unique frames that participated in
|
|
// this call stack, then add to their weight at the end.
|
|
framesInStack.add(node.frame)
|
|
}
|
|
node.addToSelfWeight(weight)
|
|
|
|
if (useAppendOrder) {
|
|
node.frame.addToSelfWeight(weight)
|
|
|
|
for (let frame of framesInStack) {
|
|
frame.addToTotalWeight(weight)
|
|
}
|
|
|
|
this.samples.push(node)
|
|
this.weights.push(weight)
|
|
}
|
|
}
|
|
|
|
appendSample(stack: FrameInfo[], weight: number) {
|
|
this._appendSample(stack, weight, true)
|
|
this._appendSample(stack, weight, false)
|
|
}
|
|
|
|
// As an alternative API for importing profiles more efficiently, provide a
|
|
// way to open & close frames directly without needing to construct tons of
|
|
// arrays as intermediaries.
|
|
private appendOrderStack: CallTreeNode[] = [this.appendOrderCalltreeRoot]
|
|
private groupedOrderStack: CallTreeNode[] = [this.groupedCalltreeRoot]
|
|
private framesInStack = new Map<Frame, number>()
|
|
private stack: Frame[] = []
|
|
|
|
private lastValue: number | null = null
|
|
private addWeightsToFrames(value: number) {
|
|
if (this.lastValue == null) this.lastValue = value
|
|
const delta = value - this.lastValue!
|
|
for (let frame of this.framesInStack.keys()) {
|
|
frame.addToTotalWeight(delta)
|
|
}
|
|
const stackTop = lastOf(this.stack)
|
|
if (stackTop) {
|
|
stackTop.addToSelfWeight(delta)
|
|
}
|
|
}
|
|
private addWeightsToNodes(value: number, stack: CallTreeNode[]) {
|
|
const delta = value - this.lastValue!
|
|
for (let node of stack) {
|
|
node.addToTotalWeight(delta)
|
|
}
|
|
const stackTop = lastOf(stack)
|
|
if (stackTop) {
|
|
stackTop.addToSelfWeight(delta)
|
|
}
|
|
}
|
|
|
|
private _enterFrame(frame: Frame, value: number, useAppendOrder: boolean) {
|
|
let stack = useAppendOrder ? this.appendOrderStack : this.groupedOrderStack
|
|
this.addWeightsToNodes(value, stack)
|
|
|
|
let prevTop = lastOf(stack)
|
|
|
|
if (prevTop) {
|
|
if (useAppendOrder) {
|
|
const delta = value - this.lastValue!
|
|
if (delta > 0) {
|
|
this.samples.push(prevTop)
|
|
this.weights.push(value - this.lastValue!)
|
|
}
|
|
}
|
|
|
|
const last = useAppendOrder
|
|
? lastOf(prevTop.children)
|
|
: prevTop.children.find(c => c.frame === frame)
|
|
let node: CallTreeNode
|
|
if (last && last.frame == frame) {
|
|
node = last
|
|
} else {
|
|
node = new CallTreeNode(frame, prevTop)
|
|
prevTop.children.push(node)
|
|
}
|
|
stack.push(node)
|
|
}
|
|
}
|
|
enterFrame(frameInfo: FrameInfo, value: number) {
|
|
const frame = getOrInsert(this.frames, frameInfo.key, () => new Frame(frameInfo))
|
|
this.addWeightsToFrames(value)
|
|
this._enterFrame(frame, value, true)
|
|
this._enterFrame(frame, value, false)
|
|
|
|
this.stack.push(frame)
|
|
const frameCount = this.framesInStack.get(frame) || 0
|
|
this.framesInStack.set(frame, frameCount + 1)
|
|
this.lastValue = value
|
|
}
|
|
|
|
private _leaveFrame(frame: Frame, value: number, useAppendOrder: boolean) {
|
|
let stack = useAppendOrder ? this.appendOrderStack : this.groupedOrderStack
|
|
this.addWeightsToNodes(value, stack)
|
|
|
|
if (useAppendOrder) {
|
|
const leavingStackTop = this.appendOrderStack.pop()
|
|
const delta = value - this.lastValue!
|
|
if (delta > 0) {
|
|
this.samples.push(leavingStackTop!)
|
|
this.weights.push(value - this.lastValue!)
|
|
}
|
|
} else {
|
|
this.groupedOrderStack.pop()
|
|
}
|
|
}
|
|
|
|
leaveFrame(frameInfo: FrameInfo, value: number) {
|
|
const frame = getOrInsert(this.frames, frameInfo.key, () => new Frame(frameInfo))
|
|
this.addWeightsToFrames(value)
|
|
|
|
this._leaveFrame(frame, value, true)
|
|
this._leaveFrame(frame, value, false)
|
|
|
|
this.stack.pop()
|
|
const frameCount = this.framesInStack.get(frame)
|
|
if (frameCount == null) return
|
|
if (frameCount === 1) {
|
|
this.framesInStack.delete(frame)
|
|
} else {
|
|
this.framesInStack.set(frame, frameCount - 1)
|
|
}
|
|
this.lastValue = value
|
|
|
|
this.totalWeight = Math.max(this.totalWeight, this.lastValue)
|
|
}
|
|
|
|
// Demangle symbols for readability
|
|
async demangle() {
|
|
let demangleCpp: ((name: string) => string) | null = null
|
|
|
|
for (let frame of this.frames.values()) {
|
|
// This function converts a mangled C++ name such as "__ZNK7Support6ColorFeqERKS0_"
|
|
// into a human-readable symbol (in this case "Support::ColorF::==(Support::ColorF&)")
|
|
if (frame.name.startsWith('__Z')) {
|
|
if (!demangleCpp) {
|
|
demangleCpp = (await demangleCppModule).demangleCpp
|
|
}
|
|
frame.name = demangleCpp(frame.name)
|
|
}
|
|
}
|
|
}
|
|
}
|