mirror of
https://github.com/jlfwong/speedscope.git
synced 2024-12-04 16:43:54 +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)
142 lines
3.8 KiB
TypeScript
142 lines
3.8 KiB
TypeScript
// This file contains methods to import data from OS X Instruments.app
|
|
// https://developer.apple.com/library/content/documentation/DeveloperTools/Conceptual/InstrumentsUserGuide/index.html
|
|
|
|
import {Profile, FrameInfo, ByteFormatter, TimeFormatter} from '../profile'
|
|
|
|
function parseTSV<T>(contents: string): T[] {
|
|
const lines = contents.split('\n').map(l => l.split('\t'))
|
|
|
|
const headerLine = lines.shift()
|
|
if (!headerLine) return []
|
|
|
|
const indexToField = new Map<number, string>()
|
|
for (let i = 0; i < headerLine.length; i++) {
|
|
indexToField.set(i, headerLine[i])
|
|
}
|
|
|
|
const ret: T[] = []
|
|
for (let line of lines) {
|
|
const row = {} as T
|
|
for (let i = 0; i < line.length; i++) {
|
|
;(row as any)[indexToField.get(i)!] = line[i]
|
|
}
|
|
ret.push(row)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
interface PastedTimeProfileRow {
|
|
Weight?: string
|
|
'Source Path'?: string
|
|
'Symbol Name'?: string
|
|
}
|
|
|
|
interface PastedAllocationsProfileRow {
|
|
'Bytes Used'?: string
|
|
'Source Path'?: string
|
|
'Symbol Name'?: string
|
|
}
|
|
|
|
interface FrameInfoWithWeight extends FrameInfo {
|
|
endValue: number
|
|
}
|
|
|
|
function getWeight(deepCopyRow: any): number {
|
|
if ('Bytes Used' in deepCopyRow) {
|
|
const bytesUsedString = deepCopyRow['Bytes Used']
|
|
const parts = /\s*(\d+(?:[.]\d+)?) (\w+)\s+(?:\d+(?:[.]\d+))%/.exec(bytesUsedString)
|
|
if (!parts) return 0
|
|
const value = parseInt(parts[1], 10)
|
|
const units = parts[2]
|
|
|
|
switch (units) {
|
|
case 'Bytes':
|
|
return value
|
|
case 'KB':
|
|
return 1024 * value
|
|
case 'MB':
|
|
return 1024 * 1024 * value
|
|
case 'GB':
|
|
return 1024 * 1024 * 1024 * value
|
|
}
|
|
throw new Error(`Unrecognized units ${units}`)
|
|
}
|
|
|
|
if ('Weight' in deepCopyRow) {
|
|
const weightString = deepCopyRow['Weight']
|
|
const parts = /\s*(\d+(?:[.]\d+)?) (\w+)\s+(?:\d+(?:[.]\d+))%/.exec(weightString)
|
|
if (!parts) return 0
|
|
const value = parseInt(parts[1], 10)
|
|
const units = parts[2]
|
|
|
|
switch (units) {
|
|
case 'ms':
|
|
return value
|
|
case 's':
|
|
return 1000 * value
|
|
case 'min':
|
|
return 1000 * value
|
|
}
|
|
throw new Error(`Unrecognized units ${units}`)
|
|
}
|
|
|
|
return -1
|
|
}
|
|
|
|
// Import from a deep copy made of a profile
|
|
export function importFromInstrumentsDeepCopy(contents: string): Profile {
|
|
const profile = new Profile()
|
|
const rows = parseTSV<PastedTimeProfileRow | PastedAllocationsProfileRow>(contents)
|
|
|
|
const stack: FrameInfoWithWeight[] = []
|
|
let cumulativeValue: number = 0
|
|
|
|
for (let row of rows) {
|
|
const symbolName = row['Symbol Name']
|
|
if (!symbolName) continue
|
|
const trimmedSymbolName = symbolName.trim()
|
|
let stackDepth = symbolName.length - trimmedSymbolName.length
|
|
|
|
if (stack.length - stackDepth < 0) {
|
|
console.log(stack, symbolName)
|
|
throw new Error('Invalid format')
|
|
}
|
|
|
|
let framesToLeave: FrameInfoWithWeight[] = []
|
|
|
|
while (stackDepth < stack.length) {
|
|
const stackTop = stack.pop()!
|
|
framesToLeave.push(stackTop)
|
|
}
|
|
|
|
for (let frameToLeave of framesToLeave) {
|
|
cumulativeValue = Math.max(cumulativeValue, frameToLeave.endValue)
|
|
profile.leaveFrame(frameToLeave, cumulativeValue)
|
|
}
|
|
|
|
const newFrameInfo: FrameInfoWithWeight = {
|
|
key: `${row['Source Path'] || ''}:${trimmedSymbolName}`,
|
|
name: trimmedSymbolName,
|
|
file: row['Source Path'],
|
|
endValue: cumulativeValue + getWeight(row),
|
|
}
|
|
|
|
profile.enterFrame(newFrameInfo, cumulativeValue)
|
|
stack.push(newFrameInfo)
|
|
}
|
|
|
|
while (stack.length > 0) {
|
|
const frameToLeave = stack.pop()!
|
|
cumulativeValue = Math.max(cumulativeValue, frameToLeave.endValue)
|
|
profile.leaveFrame(frameToLeave, cumulativeValue)
|
|
}
|
|
|
|
if ('Bytes Used' in rows[0]) {
|
|
profile.setValueFormatter(new ByteFormatter())
|
|
} else if ('Weight' in rows[0]) {
|
|
profile.setValueFormatter(new TimeFormatter('milliseconds'))
|
|
}
|
|
|
|
return profile
|
|
}
|