Compare commits

..
6 Commits
Author SHA1 Message Date
Jamie Wong 2f95f77fcf 0.4.0 2018-07-21 19:44:57 -07:00
Jamie Wong fcc6808054 Optionally read from stdin via cli (#99)
Test Plan:
- `./cli.js` opens speedscope in browser with no file selected
- `./cli.js sample/profiles/Chrome/65/simple-timeline.json` opens profile
- `cat sample/profiles/Chrome/65/simple-timeline.json | ./cli.js -` opens profile
- `node --prof-process --preprocess -j sample/profiles/node/8.5.0/isolate-0x102802600-v8.log | ./cli.js -` opens profile

Fixes #95
2018-07-21 19:42:58 -07:00
Jamie Wong ea8f982c10 Import from node profiles via v8 logs (#98)
This is inspired by https://github.com/mapbox/flamebearer
2018-07-21 16:00:25 -07:00
Jamie Wong fb5d148780 Add tests ensuring that import still works even for unfamiliar filenames (#96)
When importing files, there are two different paths we use for determining the file format.

The first is to pattern match on the filename.

If that fails, we fall back to examining the structure of the file, which can be slower and therefore wasteful.

Before this PR, we only tests that the filename pattern matching was correctly identifying the format. This PR ensures that our file structure matching is working correctly too.
2018-07-18 23:44:31 -07:00
Jamie Wong d7969ac2b8 0.3.0 2018-07-18 08:54:53 -07:00
Evan Wallace 1b36a2e3f4 add support for "wasm-function" symbol maps (#93)
This is an improvement to #76, which added support for asm.js symbol maps. This PR expands this to also work for emscripten's WebAssembly symbol maps too. This currently only works in Firefox. Chrome would need to fix https://crbug.com/863205 for this to be useful in Chrome.
2018-07-18 08:47:58 -07:00
19 changed files with 3202 additions and 118 deletions
+14
View File
@@ -0,0 +1,14 @@
## [Unreleased]
## [0.4.0] - 2018-07-21
### Added
* Support for importing v8 logs from node [#98]
* Optionally read from stdin via cli [#99]
## [0.3.0] - 2018-07-18
### Added
* Support for remapping profiles using a wasm symbol file [#93]
+11
View File
@@ -31,6 +31,17 @@ https://medium.com/@paul_irish/debugging-node-js-nightlies-with-chrome-devtools-
The `profile.json` file format output by Firefox can be saved and import into speedscope: https://developer.mozilla.org/en-US/docs/Tools/Performance
### Node
If you record profiling information like so:
node --prof /path/to/my/script.js
Then this will generate one or more `isolate*.log` files. You can open
the resulting profile in speedscope by running the following command:
node --prof-process --preprocess -j isolate*.log | speedscope -
### Instruments.app
You can import call trees from OSX Instruments.app into speedscope by
+3 -3
View File
@@ -16,7 +16,7 @@ import {SortMethod, SortField, SortDirection} from './profile-table-view'
import {triangle} from './utils'
import {Color} from './color'
import {RowAtlas} from './row-atlas'
import {importAsmJsSymbolMap} from './asm-js'
import {importEmscriptenSymbolMap} from './emscripten'
import {SandwichView} from './sandwich-view'
import {saveToFile} from './file-format'
@@ -384,9 +384,9 @@ export class Application extends ReloadableComponent<{}, ApplicationState> {
// a symbol map. If that's the case, we want to parse it, and apply the symbol
// mapping to the already loaded profile. This can be use to take an opaque
// profile and make it readable.
const map = importAsmJsSymbolMap(reader.result)
const map = importEmscriptenSymbolMap(reader.result)
if (map) {
console.log('Importing as asm.js symbol map')
console.log('Importing as emscripten symbol map')
let profile = this.state.profile
profile.remapNames(name => map.get(name) || name)
return profile
-26
View File
@@ -1,26 +0,0 @@
type AsmJsSymbolMap = Map<string, string>
// This imports symbol maps generated by emscripten using the "--emit-symbol-map" flag.
// It allows you to visualize a profile captured in a release build as long as you also
// have the associated symbol map. To do this, first drop the profile into speedscope
// and then drop the symbol map. After the second drop, the symbols will be remapped to
// their original names.
export function importAsmJsSymbolMap(contents: string): AsmJsSymbolMap | null {
const lines = contents.split('\n')
if (!lines.length) return null
// Remove a trailing blank line if there is one
if (lines[lines.length - 1] === '') lines.pop()
if (!lines.length) return null
const map: AsmJsSymbolMap = new Map()
const regex = /^([\$\w]+):([\$\w]+)$/
for (const line of lines) {
const match = regex.exec(line)
if (!match) return null
map.set(match[1], match[2])
}
return map
}
+84 -55
View File
@@ -2,73 +2,102 @@
const path = require('path')
const fs = require('fs')
const os = require('os')
const stream = require('stream')
const opn = require('opn')
const helpString = `
Usage: speedscope [filepath]
const helpString = `Usage: speedscope [filepath]
If invoked with no arguments, will open a local copy of speedscope in your default browser.
Once open, you can browse for a profile to import.
If - is used as the filepath, will read from stdin instead.
cat /path/to/profile | speedscope -
`
if (process.argv.includes('--help') || process.argv.includes('-h')) {
console.log(helpString)
process.exit(0)
function getProfileStream(relPath) {
const absPath = path.resolve(process.cwd(), relPath)
if (relPath === '-') {
// Read from stdin
return process.stdin
} else {
return fs.createReadStream(absPath)
}
}
if (process.argv.includes('--version') || process.argv.includes('-v')) {
console.log('v' + require('./package.json').version)
process.exit(0)
function getProfileBuffer(relPath) {
const profileStream = getProfileStream(relPath)
const chunks = []
return new Promise((resolve, reject) => {
profileStream.pipe(
stream.Writable({
write(chunk, encoding, callback) {
chunks.push(chunk)
callback()
},
final() {
resolve(Buffer.concat(chunks))
},
}),
)
profileStream.on('error', ev => reject(ev))
})
}
if (process.argv.length > 3) {
console.log('At most one argument expected')
console.log(helpString)
process.exit(1)
async function main() {
if (process.argv.includes('--help') || process.argv.includes('-h')) {
console.log(helpString)
return
}
if (process.argv.includes('--version') || process.argv.includes('-v')) {
console.log('v' + require('./package.json').version)
return
}
if (process.argv.length > 3) {
throw new Error('At most one argument expected')
}
let urlToOpen = 'file://' + path.resolve(__dirname, './dist/release/index.html')
if (process.argv.length === 3) {
const relPath = process.argv[2]
const sourceBuffer = await getProfileBuffer(relPath)
const filename = path.basename(relPath)
const sourceBase64 = sourceBuffer.toString('base64')
const jsSource = `speedscope.loadFileFromBase64(${JSON.stringify(filename)}, ${JSON.stringify(
sourceBase64,
)})`
const filePrefix = `speedscope-${+new Date()}-${process.pid}`
const jsPath = path.join(os.tmpdir(), `${filePrefix}.js`)
console.log(`Creating temp file ${jsPath}`)
fs.writeFileSync(jsPath, jsSource)
urlToOpen += `#localProfilePath=${jsPath}`
// For some silly reason, the OS X open command ignores any query parameters or hash parameters
// passed as part of the URL. To get around this weird issue, we'll create a local HTML file
// that just redirects.
const htmlPath = path.join(os.tmpdir(), `${filePrefix}.html`)
console.log(`Creating temp file ${htmlPath}`)
fs.writeFileSync(htmlPath, `<script>window.location=${JSON.stringify(urlToOpen)}</script>`)
urlToOpen = `file://${htmlPath}`
}
console.log('Opening', urlToOpen, 'in your default browser')
await opn(urlToOpen, {wait: false})
}
let urlToOpen = 'file://' + path.resolve(__dirname, './dist/release/index.html')
if (process.argv.length === 3) {
const absPath = path.resolve(process.cwd(), process.argv[2])
let sourceBuffer
try {
sourceBuffer = fs.readFileSync(absPath)
} catch (e) {
console.log(e)
main()
.then(() => {
process.exit(0)
})
.catch(e => {
console.log(e.stack + '\n')
console.log(helpString)
process.exit(1)
}
const filename = path.basename(absPath)
const sourceBase64 = sourceBuffer.toString('base64')
const jsSource = `speedscope.loadFileFromBase64(${JSON.stringify(filename)}, ${JSON.stringify(
sourceBase64,
)})`
const filePrefix = `speedscope-${+new Date()}-${process.pid}`
const jsPath = path.join(os.tmpdir(), `${filePrefix}.js`)
console.log(`Creating temp file ${jsPath}`)
fs.writeFileSync(jsPath, jsSource)
urlToOpen += `#localProfilePath=${jsPath}`
// For some silly reason, the OS X open command ignores any query parameters or hash parameters
// passed as part of the URL. To get around this weird issue, we'll create a local HTML file
// that just redirects.
const htmlPath = path.join(os.tmpdir(), `${filePrefix}.html`)
console.log(`Creating temp file ${htmlPath}`)
fs.writeFileSync(htmlPath, `<script>window.location=${JSON.stringify(urlToOpen)}</script>`)
urlToOpen = `file://${htmlPath}`
}
console.log('Opening', urlToOpen, 'in your default browser')
opn(urlToOpen, {wait: false}).then(
() => {
process.exit(0)
},
err => {
console.error(err)
console.exit(1)
},
)
})
+23 -10
View File
@@ -1,9 +1,9 @@
import {importAsmJsSymbolMap} from './asm-js'
import {importEmscriptenSymbolMap} from './emscripten'
test('importAsmJSSymbolMap', () => {
test('importEmscriptenSymbolMap', () => {
// Valid symbol map
expect(
importAsmJsSymbolMap(
importEmscriptenSymbolMap(
[
/* prettier: ignore */
'a:A',
@@ -15,7 +15,7 @@ test('importAsmJSSymbolMap', () => {
// Valid symbol map with trailing newline
expect(
importAsmJsSymbolMap(
importEmscriptenSymbolMap(
[
/* prettier: ignore */
'a:A',
@@ -27,14 +27,27 @@ test('importAsmJSSymbolMap', () => {
).toEqual(new Map([['a', 'A'], ['b', 'B'], ['c', 'C']]))
// Valid symbol map with non-alpha characters
expect(importAsmJsSymbolMap('u6:__ZN8tinyxml210XMLCommentD0Ev\n')).toEqual(
expect(importEmscriptenSymbolMap('u6:__ZN8tinyxml210XMLCommentD0Ev\n')).toEqual(
new Map([['u6', '__ZN8tinyxml210XMLCommentD0Ev']]),
)
// WebAssembly symbol map
expect(
importEmscriptenSymbolMap(
[
/* prettier: ignore */
'0:A',
'1:B',
'2:C',
].join('\n'),
),
).toEqual(
new Map([['wasm-function[0]', 'A'], ['wasm-function[1]', 'B'], ['wasm-function[2]', 'C']]),
)
// Invalid symbol map
expect(
importAsmJsSymbolMap(
importEmscriptenSymbolMap(
[
/* prettier: ignore */
'a:A',
@@ -47,7 +60,7 @@ test('importAsmJSSymbolMap', () => {
// Collapsed stack format should not be imported as an asm.js symbol map
expect(
importAsmJsSymbolMap(
importEmscriptenSymbolMap(
[
/* prettier: ignore */
'a;b 1',
@@ -58,6 +71,6 @@ test('importAsmJSSymbolMap', () => {
).toEqual(null)
// Unrelated files
expect(importAsmJsSymbolMap('')).toEqual(null)
expect(importAsmJsSymbolMap('\n')).toEqual(null)
expect(importEmscriptenSymbolMap('')).toEqual(null)
expect(importEmscriptenSymbolMap('\n')).toEqual(null)
})
+39
View File
@@ -0,0 +1,39 @@
type EmscriptenSymbolMap = Map<string, string>
// This imports symbol maps generated by emscripten using the "--emit-symbol-map" flag.
// It allows you to visualize a profile captured in a release build as long as you also
// have the associated symbol map. To do this, first drop the profile into speedscope
// and then drop the symbol map. After the second drop, the symbols will be remapped to
// their original names.
export function importEmscriptenSymbolMap(contents: string): EmscriptenSymbolMap | null {
const lines = contents.split('\n')
if (!lines.length) return null
// Remove a trailing blank line if there is one
if (lines[lines.length - 1] === '') lines.pop()
if (!lines.length) return null
const map: EmscriptenSymbolMap = new Map()
const intRegex = /^(\d+):([\$\w]+)$/
const idRegex = /^([\$\w]+):([\$\w]+)$/
for (const line of lines) {
// Match lines like "103:__ZN8tinyxml210XMLCommentD0Ev"
const intMatch = intRegex.exec(line)
if (intMatch) {
map.set(`wasm-function[${intMatch[1]}]`, intMatch[2])
continue
}
// Match lines like "u6:__ZN8tinyxml210XMLCommentD0Ev"
const idMatch = idRegex.exec(line)
if (idMatch) {
map.set(idMatch[1], idMatch[2])
continue
}
return null
}
return map
}
+260
View File
@@ -0,0 +1,260 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`importFromV8ProfLog 1`] = `
Object {
"frames": Array [
Frame {
"col": 10,
"file": "bootstrap_node.js",
"key": " bootstrap_node.js:10:10",
"line": 10,
"name": "(anonymous)",
"selfWeight": 0,
"totalWeight": 116424,
},
Frame {
"col": 19,
"file": "bootstrap_node.js",
"key": "startup bootstrap_node.js:12:19",
"line": 12,
"name": "startup",
"selfWeight": 0,
"totalWeight": 116424,
},
Frame {
"col": 32,
"file": "bootstrap_node.js",
"key": "setupGlobalVariables bootstrap_node.js:265:32",
"line": 265,
"name": "setupGlobalVariables",
"selfWeight": 0,
"totalWeight": 29385,
},
Frame {
"col": 34,
"file": "bootstrap_node.js",
"key": "NativeModule.require bootstrap_node.js:534:34",
"line": 534,
"name": "NativeModule.require",
"selfWeight": 0,
"totalWeight": 52273,
},
Frame {
"col": 44,
"file": "bootstrap_node.js",
"key": "NativeModule.compile bootstrap_node.js:602:44",
"line": 602,
"name": "NativeModule.compile",
"selfWeight": 0,
"totalWeight": 52273,
},
Frame {
"col": 11,
"file": "util.js",
"key": " util.js:1:11",
"line": 1,
"name": "(anonymous)",
"selfWeight": 0,
"totalWeight": 29385,
},
Frame {
"col": 11,
"file": "internal/encoding.js",
"key": " internal/encoding.js:1:11",
"line": 1,
"name": "(anonymous)",
"selfWeight": 0,
"totalWeight": 29385,
},
Frame {
"col": undefined,
"file": undefined,
"key": "(c++) v8::internal::Runtime_CreateArrayLiteral",
"line": undefined,
"name": "(c++) v8::internal::Runtime_CreateArrayLiteral",
"selfWeight": 0,
"totalWeight": 29385,
},
Frame {
"col": undefined,
"file": undefined,
"key": "(c++) v8::internal::JSFunction::EnsureHasInitialMap",
"line": undefined,
"name": "(c++) v8::internal::JSFunction::EnsureHasInitialMap",
"selfWeight": 29385,
"totalWeight": 29385,
},
Frame {
"col": 30,
"file": "bootstrap_node.js",
"key": "setupGlobalConsole bootstrap_node.js:320:30",
"line": 320,
"name": "setupGlobalConsole",
"selfWeight": 0,
"totalWeight": 22888,
},
Frame {
"col": 40,
"file": "bootstrap_node.js",
"key": "setupInspectorCommandLineAPI bootstrap_node.js:364:40",
"line": 364,
"name": "setupInspectorCommandLineAPI",
"selfWeight": 0,
"totalWeight": 22888,
},
Frame {
"col": 11,
"file": "module.js",
"key": " module.js:1:11",
"line": 1,
"name": "(anonymous)",
"selfWeight": 0,
"totalWeight": 22888,
},
Frame {
"col": 11,
"file": "fs.js",
"key": " fs.js:1:11",
"line": 1,
"name": "(anonymous)",
"selfWeight": 0,
"totalWeight": 22888,
},
Frame {
"col": undefined,
"file": undefined,
"key": "(c++) v8::internal::Runtime_StoreIC_Miss",
"line": undefined,
"name": "(c++) v8::internal::Runtime_StoreIC_Miss",
"selfWeight": 0,
"totalWeight": 22888,
},
Frame {
"col": undefined,
"file": undefined,
"key": "(c++) v8::internal::Map::RawCopy",
"line": undefined,
"name": "(c++) v8::internal::Map::RawCopy",
"selfWeight": 22888,
"totalWeight": 22888,
},
Frame {
"col": 26,
"file": "module.js",
"key": "Module.runMain module.js:663:26",
"line": 663,
"name": "Module.runMain",
"selfWeight": 0,
"totalWeight": 64151,
},
Frame {
"col": 24,
"file": "module.js",
"key": "Module._load module.js:443:24",
"line": 443,
"name": "Module._load",
"selfWeight": 0,
"totalWeight": 64151,
},
Frame {
"col": 23,
"file": "module.js",
"key": "tryModuleLoad module.js:505:23",
"line": 505,
"name": "tryModuleLoad",
"selfWeight": 0,
"totalWeight": 64151,
},
Frame {
"col": 33,
"file": "module.js",
"key": "Module.load module.js:536:33",
"line": 536,
"name": "Module.load",
"selfWeight": 0,
"totalWeight": 64151,
},
Frame {
"col": 37,
"file": "module.js",
"key": "Module._extensions..js module.js:633:37",
"line": 633,
"name": "Module._extensions..js",
"selfWeight": 0,
"totalWeight": 64151,
},
Frame {
"col": 37,
"file": "module.js",
"key": "Module._compile module.js:581:37",
"line": 581,
"name": "Module._compile",
"selfWeight": 0,
"totalWeight": 64151,
},
Frame {
"col": 11,
"file": "/Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js",
"key": " /Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js:1:11",
"line": 1,
"name": "(anonymous)",
"selfWeight": 0,
"totalWeight": 64151,
},
Frame {
"col": 73,
"file": "/Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js",
"key": "a /Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js:1:73",
"line": 1,
"name": "a",
"selfWeight": 0,
"totalWeight": 64151,
},
Frame {
"col": 11,
"file": "/Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js",
"key": "b /Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js:8:11",
"line": 8,
"name": "b",
"selfWeight": 10044,
"totalWeight": 53997,
},
Frame {
"col": 11,
"file": "/Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js",
"key": "d /Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js:20:11",
"line": 20,
"name": "d",
"selfWeight": 51562,
"totalWeight": 51562,
},
Frame {
"col": 11,
"file": "/Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js",
"key": "c /Users/jlfwong/code/speedscope/sample/programs/javascript/simple.js:14:11",
"line": 14,
"name": "c",
"selfWeight": 2545,
"totalWeight": 10154,
},
],
"stacks": Array [
"(anonymous);startup;setupGlobalVariables;NativeModule.require;NativeModule.compile;(anonymous);NativeModule.require;NativeModule.compile;(anonymous);(c++) v8::internal::Runtime_CreateArrayLiteral;(c++) v8::internal::JSFunction::EnsureHasInitialMap 29.38ms",
"(anonymous);startup;setupGlobalConsole;setupInspectorCommandLineAPI;NativeModule.require;NativeModule.compile;(anonymous);NativeModule.require;NativeModule.compile;(anonymous);(c++) v8::internal::Runtime_StoreIC_Miss;(c++) v8::internal::Map::RawCopy 22.89ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;b;d 37.52ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;c;d 1.28ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;b;d 2.55ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;c;d 1.27ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;b;d 1.28ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;c;d 1.27ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;b;d 1.29ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;c;d 1.26ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;b;d 1.31ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;c;d 2.52ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;c 1.27ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;b 3.62ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;c 1.28ms",
"(anonymous);startup;Module.runMain;Module._load;tryModuleLoad;Module.load;Module._extensions..js;Module._compile;(anonymous);a;b 6.42ms",
],
}
`;
+7
View File
@@ -8,6 +8,7 @@ import {importFromBGFlameGraph} from './bg-flamegraph'
import {importFromFirefox} from './firefox'
import {importSpeedscopeProfiles} from '../file-format'
import {FileFormat} from '../file-format-spec'
import {importFromV8ProfLog} from './v8proflog'
export async function importProfile(fileName: string, contents: string): Promise<Profile | null> {
const profile = await _importProfile(fileName, contents)
@@ -45,6 +46,9 @@ async function _importProfile(fileName: string, contents: string): Promise<Profi
} else if (fileName.endsWith('.collapsedstack.txt')) {
console.log('Importing as collapsed stack format')
return importFromBGFlameGraph(contents)
} else if (fileName.endsWith('.v8log.json')) {
console.log('Importing as --prof-process v8 log')
return importFromV8ProfLog(JSON.parse(contents))
}
// Second pass: Try to guess what file format it is based on structure
@@ -68,6 +72,9 @@ async function _importProfile(fileName: string, contents: string): Promise<Profi
} else if ('mode' in parsed && 'frames' in parsed) {
console.log('Importing as stackprof profile')
return importFromStackprof(parsed)
} else if ('code' in parsed && 'functions' in parsed && 'ticks' in parsed) {
console.log('Importing as --prof-process v8 log')
return importFromV8ProfLog(parsed)
}
} else {
// Format is not JSON
+5
View File
@@ -0,0 +1,5 @@
import {checkProfileSnapshot} from '../test-utils'
test('importFromV8ProfLog', async () => {
await checkProfileSnapshot('./sample/profiles/node/8.5.0/simple.v8log.json')
})
+188
View File
@@ -0,0 +1,188 @@
import {Profile, FrameInfo, StackListProfileBuilder} from '../profile'
import {getOrInsert, sortBy} from '../utils'
import {TimeFormatter} from '../value-formatters'
// This imports profiles generated by a combination of the following commands:
//
// node --prof /path/to/my/script.js
// node --prof-process -preprocess -j isolate*.log > profile.v8log.json
// References:
// - https://github.com/nodejs/node/blob/7edd0a17af8d74dce7dd6c7554a8b8523f83efdc/lib/internal/v8_prof_processor.js#L5
// - https://github.com/nodejs/node/blob/7edd0a17af8d74dce7dd6c7554a8b8523f83efdc/deps/v8/tools/tickprocessor.js
// - https://github.com/nodejs/node/blob/2db2857c72c219e5ba1642a345e52cfdd8c44a66/deps/v8/tools/logreader.js#L147
// - https://github.com/mapbox/flamebearer/blob/a8d4d5c0061ed439660783c613c43ab28b751219/index.js#L53
interface Code {
name: string
type: 'CODE' | 'CPP' | 'JS' | 'SHARED_LIB'
timestamp?: number
kind?:
| 'Bultin'
| 'BytecodeHandler'
| 'Handler'
| 'KeyedLoadIC'
| 'KeyedStoreIC'
| 'LoadGlobalIC'
| 'LoadIC'
| 'Opt'
| 'StoreIC'
| 'Stub'
| 'Unopt'
| 'Builtin'
| 'RegExp'
func?: number
tm?: number
}
interface Function {
name: string
codes: number[]
}
interface Tick {
// Timestamp
tm: number
// Virtual machine state?
vm: number
// stack
s: number[]
}
interface V8LogProfile {
code: Code[]
functions: Function[]
ticks: Tick[]
}
function codeToFrameInfo(code: Code, v8log: V8LogProfile): FrameInfo {
if (!code || !code.type) {
return {
key: '(unknown type)',
name: '(unknown type)',
}
}
let name = code.name
switch (code.type) {
case 'CPP': {
const matches = name.match(/[tT] ([^(<]*)/)
if (matches) name = `(c++) ${matches[1]}`
break
}
case 'SHARED_LIB':
name = '(LIB) ' + name
break
case 'JS': {
const matches = name.match(/([a-zA-Z0-9\._\-$]*) ([a-zA-Z0-9\.\-_\/$]*):(\d+):(\d+)/)
if (matches) {
return {
key: name,
name: matches[1].length > 0 ? matches[1] : '(anonymous)',
file: matches[2].length > 0 ? matches[2] : '(unknown file)',
line: parseInt(matches[3], 10),
col: parseInt(matches[4], 10),
}
}
break
}
case 'CODE': {
switch (code.kind) {
case 'LoadIC':
case 'StoreIC':
case 'KeyedStoreIC':
case 'KeyedLoadIC':
case 'LoadGlobalIC':
case 'Handler':
name = '(IC) ' + name
break
case 'BytecodeHandler':
name = '(bytecode) ~' + name
break
case 'Stub':
name = '(stub) ' + name
break
case 'Builtin':
name = '(builtin) ' + name
break
case 'RegExp':
name = '(regexp) ' + name
break
}
break
}
default: {
name = `(${code.type}) ${name}`
break
}
}
return {key: name, name}
}
export function importFromV8ProfLog(v8log: V8LogProfile): Profile {
const profile = new StackListProfileBuilder()
const sToFrameInfo = new Map<number, FrameInfo>()
function getFrameInfo(t: number) {
return getOrInsert(sToFrameInfo, t, t => {
const code = v8log.code[t]
return codeToFrameInfo(code, v8log)
})
}
let lastTm = 0
sortBy(v8log.ticks, tick => tick.tm)
for (let tick of v8log.ticks) {
const stack: FrameInfo[] = []
// tick.s holds the call stack at the time the sample was taken. The
// structure is a little strange -- it seems to be capturing both the
// JavaScript stack & the parallel C++ stack by interleaving the two.
// Because the stacks might not be the same length, it looks like the
// shorter stack is padded with indices of -1, so we'll just ignore those
// stacks.
//
// If you change the start index to `let i = tick.s.length - 1` instead,
// you'll see the C++ stack instead.
//
// Mostly the numbers in the stack seem to be indices into the `v8log.code`
// array, but some of the numbers in the C++ stack seem to be raw memory
// addresses.
for (let i = tick.s.length - 2; i >= 0; i -= 2) {
const id = tick.s[i]
if (id === -1) continue
if (id > v8log.code.length) {
// Treat this like a memory address
stack.push({
key: id,
name: `0x${id.toString(16)}`,
})
continue
}
stack.push(getFrameInfo(id))
}
profile.appendSample(stack, tick.tm - lastTm)
lastTm = tick.tm
}
// Despite the code in the v8 processing library indicating that the
// timestamps come from a variable called "time_ns", from making empirical
// recordings, it really seems like these profiles are recording timestamps in
// microseconds, not nanoseconds.
// https://github.com/nodejs/node/blob/c39caa997c751473d0c8f50af8c6b14bcd389fa0/deps/v8/tools/profile.js#L1076
profile.setValueFormatter(new TimeFormatter('microseconds'))
return profile.build()
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "speedscope",
"version": "0.2.0",
"version": "0.3.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
+6 -22
View File
@@ -1,6 +1,6 @@
{
"name": "speedscope",
"version": "0.2.0",
"version": "0.4.0",
"description": "",
"main": "index.js",
"bin": {
@@ -11,20 +11,13 @@
"prepack": "./build-release.sh",
"prettier": "prettier --write './**/*.ts' './**/*.tsx'",
"lint": "eslint './**/*.ts' './**/*.tsx'",
"jest": "jest",
"jest": "./test-setup.sh && jest",
"coverage": "npm run jest -- --coverage && coveralls < coverage/lcov.info",
"test": "tsc --noEmit && npm run lint && npm run coverage",
"serve": "parcel index.html --open --no-autoinstall"
},
"files": [
"cli.js",
"dist/release/**",
"!*.map"
],
"browserslist": [
"last 2 Chrome versions",
"last 2 Firefox versions"
],
"files": ["cli.js", "dist/release/**", "!*.map"],
"browserslist": ["last 2 Chrome versions", "last 2 Firefox versions"],
"author": "",
"license": "MIT",
"devDependencies": {
@@ -55,17 +48,8 @@
"^.+\\.tsx?$": "ts-jest"
},
"testRegex": "\\.test\\.tsx?$",
"collectCoverageFrom": [
"**/*.{ts,tsx}",
"!**/*.d.{ts,tsx}"
],
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json"
]
"collectCoverageFrom": ["**/*.{ts,tsx}", "!**/*.d.{ts,tsx}"],
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json"]
},
"dependencies": {
"opn": "5.3.0"
+1
View File
@@ -0,0 +1 @@
*.v8log.json
File diff suppressed because it is too large Load Diff
Binary file not shown.
Executable
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
set -e
for f in `find sample/profiles -name '*.zip' | grep -v Instruments`; do
unzip -o $f -d $(dirname $f);
done
+9
View File
@@ -55,6 +55,15 @@ export async function checkProfileSnapshot(filepath: string) {
return
}
const profileWithoutFilename = await importProfile('unknown', input)
if (profileWithoutFilename) {
profileWithoutFilename.setName(profile.getName())
expect(exportProfile(profileWithoutFilename)).toEqual(exportProfile(profile))
} else {
fail('Failed to extract profile when filename was "unknown"')
return
}
const exported = exportProfile(profile)
const reimported = importSpeedscopeProfiles(exported)[0]
+5 -1
View File
@@ -22,7 +22,7 @@ export class TimeFormatter implements ValueFormatter {
else this.multiplier = 1
}
format(v: number) {
formatUnsigned(v: number) {
const s = v * this.multiplier
if (s / 60 >= 1) return `${(s / 60).toFixed(2)}min`
@@ -31,6 +31,10 @@ export class TimeFormatter implements ValueFormatter {
if (s / 1e-6 >= 1) return `${(s / 1e-6).toFixed(2)}µs`
else return `${(s / 1e-9).toFixed(2)}ns`
}
format(v: number) {
return `${v < 0 ? '-' : ''}${this.formatUnsigned(Math.abs(v))}`
}
}
export class ByteFormatter implements ValueFormatter {