-
Notifications
You must be signed in to change notification settings - Fork 620
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support symbolicating allocation stacks in Chrome heap snapshots
Summary: Adds support for symbolicating allocation stacks in Chrome-formatted heap snapshot (specifically, heap timeline) files. Other data in the heap snapshot remains untouched, in particular the `locations` array - since `locations` doesn't expose the unsymbolicated file names (which we need to e.g. extract segment IDs) and at any rate isn't exposed in the Chrome DevTools UI when loading a snapshot from a file. Reviewed By: MichaReiser Differential Revision: D26022945 fbshipit-source-id: ceff6ee875d170e3af524bf3032d7dac5db2ff52
- Loading branch information
1 parent
cb542c0
commit 6b0a0cb
Showing
7 changed files
with
252 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletions
1
.../metro-symbolicate/src/__tests__/__fixtures__/GenSampleHeapSnapshotBundle.js.heaptimeline
Large diffs are not rendered by default.
Oops, something went wrong.
1 change: 1 addition & 0 deletions
1
packages/metro-symbolicate/src/__tests__/__fixtures__/GenSampleHeapSnapshotBundle.js.map
Large diffs are not rendered by default.
Oops, something went wrong.
31 changes: 31 additions & 0 deletions
31
...ages/metro-symbolicate/src/__tests__/__snapshots__/symbolicate-heap-snapshot-test.js.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`heap snapshots/timelines symbolicating allocation stacks: symbolicated 1`] = ` | ||
"<global> @ /js/RKJSModules/Apps/GenSampleHeapSnapshot/GenSampleHeapSnapshot.js:10:20 | ||
loadModuleImplementation @ /js/node_modules/metro-runtime/src/polyfills/require.js:409:12 | ||
metroRequire @ /js/node_modules/metro-runtime/src/polyfills/require.js:193:20 | ||
metroRequire @ /js/node_modules/metro-runtime/src/polyfills/require.js:193:20 | ||
metroImportDefault @ /js/node_modules/metro-runtime/src/polyfills/require.js:212:31 | ||
<global> @ /js/RKJSModules/EntryPoints/GenSampleHeapSnapshotBundle.js:12:1 | ||
loadModuleImplementation @ /js/node_modules/metro-runtime/src/polyfills/require.js:409:12 | ||
guardedLoadModule @ /js/node_modules/metro-runtime/src/polyfills/require.js:271:45 | ||
metroRequire @ /js/node_modules/metro-runtime/src/polyfills/require.js:193:20 | ||
global @ ./GenSampleHeapSnapshotBundle.js:26:4 | ||
global @ ./GenSampleHeapSnapshotBundle.js:1:1 | ||
(root) @ :0:0" | ||
`; | ||
exports[`heap snapshots/timelines symbolicating allocation stacks: unsymbolicated 1`] = ` | ||
"(anonymous) @ ./GenSampleHeapSnapshotBundle.js:25:231 | ||
loadModuleImplementation @ ./GenSampleHeapSnapshotBundle.js:10:6144 | ||
metroRequire @ ./GenSampleHeapSnapshotBundle.js:10:2060 | ||
metroRequire @ ./GenSampleHeapSnapshotBundle.js:10:2060 | ||
metroImportDefault @ ./GenSampleHeapSnapshotBundle.js:10:2452 | ||
(anonymous) @ ./GenSampleHeapSnapshotBundle.js:15:139 | ||
loadModuleImplementation @ ./GenSampleHeapSnapshotBundle.js:10:6144 | ||
guardedLoadModule @ ./GenSampleHeapSnapshotBundle.js:10:3528 | ||
metroRequire @ ./GenSampleHeapSnapshotBundle.js:10:2060 | ||
global @ ./GenSampleHeapSnapshotBundle.js:26:4 | ||
global @ ./GenSampleHeapSnapshotBundle.js:1:1 | ||
(root) @ :0:0" | ||
`; |
140 changes: 140 additions & 0 deletions
140
packages/metro-symbolicate/src/__tests__/symbolicate-heap-snapshot-test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
/** | ||
* Copyright (c) Facebook, Inc. and its affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
* | ||
* @emails oncall+js_symbolication | ||
* @format | ||
* @flow strict-local | ||
*/ | ||
|
||
'use strict'; | ||
|
||
const fs = require('fs'); | ||
const path = require('path'); | ||
const symbolicate = require('../symbolicate'); | ||
|
||
const {ChromeHeapSnapshotProcessor} = require('../ChromeHeapSnapshot'); | ||
const {PassThrough} = require('stream'); | ||
const resolve = fileName => path.resolve(__dirname, '__fixtures__', fileName); | ||
const read = fileName => fs.readFileSync(resolve(fileName), 'utf8'); | ||
|
||
const execute = async ( | ||
args: Array<string>, | ||
stdin?: string, | ||
): Promise<string> => { | ||
const streams = { | ||
stdin: new PassThrough(), | ||
stdout: new PassThrough(), | ||
stderr: new PassThrough(), | ||
}; | ||
const stdout = []; | ||
const errorMessage = ['Process failed with the following output:\n======\n']; | ||
streams.stdout.on('data', data => { | ||
errorMessage.push(data); | ||
stdout.push(data); | ||
}); | ||
streams.stderr.on('data', data => { | ||
errorMessage.push(data); | ||
}); | ||
if (stdin != null) { | ||
streams.stdin.write(stdin); | ||
streams.stdin.end(); | ||
} | ||
const code = await symbolicate(args, streams); | ||
|
||
if (code !== 0) { | ||
errorMessage.push('======\n'); | ||
throw new Error(errorMessage.join('')); | ||
} | ||
return stdout.join(''); | ||
}; | ||
|
||
describe('heap snapshots/timelines', () => { | ||
test('symbolicating allocation stacks', async () => { | ||
function findKnownAllocationStack(heapSnapshotStr) { | ||
const rawData = JSON.parse(heapSnapshotStr); | ||
const data = new ChromeHeapSnapshotProcessor(rawData); | ||
const node = findObjectByInboundProperty('RETAIN_ME', data, rawData); | ||
return getStackTrace(node.getNumber('trace_node_id'), data); | ||
} | ||
|
||
const symbolicated = await execute([ | ||
resolve('GenSampleHeapSnapshotBundle.js.map'), | ||
resolve('GenSampleHeapSnapshotBundle.js.heaptimeline'), | ||
]); | ||
|
||
// Snapshot the original unsymbolicated trace for easy comparison | ||
const unsymbolicated = read('GenSampleHeapSnapshotBundle.js.heaptimeline'); | ||
expect(findKnownAllocationStack(unsymbolicated)).toMatchSnapshot( | ||
'unsymbolicated', | ||
); | ||
expect(findKnownAllocationStack(symbolicated)).toMatchSnapshot( | ||
'symbolicated', | ||
); | ||
}); | ||
}); | ||
|
||
// Returns a node in the heap snapshot that has an incoming property edge with | ||
// the name passed as `propertyName`. | ||
function findObjectByInboundProperty(propertyName, data, rawData) { | ||
const sigilStrIndex = rawData.strings.indexOf(propertyName); | ||
for (const edge of data.edges()) { | ||
if ( | ||
edge.getNumber('name_or_index') === sigilStrIndex && | ||
edge.getString('type') === 'property' | ||
) { | ||
const nodeIt = data.nodes(); | ||
nodeIt.moveToRecord( | ||
edge.getNumber('to_node') / rawData.snapshot.meta.node_fields.length, | ||
); | ||
return nodeIt; | ||
} | ||
} | ||
throw new Error( | ||
`Could not find an object with an inbound property edge '${propertyName}'`, | ||
); | ||
} | ||
|
||
// Find a given trace node in the trace tree and record the path from the root | ||
// (reversed and translated into readable stack frames). | ||
function getStackTrace(traceNodeId, data) { | ||
const functionInfoStack = []; | ||
const FOUND = Symbol('FOUND'); | ||
|
||
function visit(traceNode) { | ||
functionInfoStack.push(traceNode.getNumber('function_info_index')); | ||
if (traceNode.getNumber('id') === traceNodeId) { | ||
throw FOUND; | ||
} | ||
for (const child of traceNode.getChildren('children')) { | ||
visit(child); | ||
} | ||
functionInfoStack.pop(); | ||
} | ||
|
||
for (const traceRoot of data.traceTree()) { | ||
try { | ||
visit(traceRoot); | ||
} catch (e) { | ||
if (e === FOUND) { | ||
break; | ||
} | ||
throw e; | ||
} | ||
} | ||
|
||
const frameIt = data.traceFunctionInfos(); | ||
return functionInfoStack | ||
.reverse() | ||
.map(index => { | ||
frameIt.moveToRecord(index); | ||
const name = frameIt.getString('name'); | ||
const scriptName = frameIt.getString('script_name'); | ||
const line = frameIt.getNumber('line'); | ||
const column = frameIt.getNumber('column'); | ||
return `${name} @ ${scriptName}:${line}:${column}`; | ||
}) | ||
.join('\n'); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters