-
Notifications
You must be signed in to change notification settings - Fork 392
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
176 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
/* This Source Code Form is subject to the terms of the Mozilla Public | ||
* License, v. 2.0. If a copy of the MPL was not distributed with this | ||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
// @flow | ||
import type { | ||
Profile, | ||
Bytes, | ||
IndexIntoFuncTable, | ||
} from 'firefox-profiler/types'; | ||
|
||
import { | ||
getEmptyProfile, | ||
getEmptyThread, | ||
getEmptyUnbalancedNativeAllocationsTable, | ||
} from '../data-structures'; | ||
|
||
import { coerce } from 'firefox-profiler/utils/flow'; | ||
|
||
// Reference used for heapprofile format: | ||
// https://chromium.googlesource.com/v8/v8.git/+/a41a2e4571ec8847c7042d01f9d0afe2accb9730/include/js_protocol.pdl#696 | ||
|
||
// Unique script identifier. | ||
type ScriptId = string; | ||
|
||
// Unique node identifier. | ||
type NodeId = number; | ||
|
||
// Stack entry for runtime errors and assertions. | ||
type CallFrame = $ReadOnly<{| | ||
// JavaScript function name. | ||
functionName: string, | ||
// JavaScript script id. | ||
scriptId: ScriptId, | ||
// JavaScript script name or url. | ||
url: string, | ||
// JavaScript script line number (0-based). | ||
lineNumber: number, | ||
// JavaScript script column number (0-based). | ||
columnNumber: number, | ||
|}>; | ||
|
||
// Sampling Heap Profile node. Holds callsite information, allocation statistics and child nodes. | ||
type SamplingHeapProfileNode = $ReadOnly<{| | ||
// Function location. | ||
callFrame: CallFrame, | ||
// Allocations size in bytes for the node excluding children. | ||
selfSize: Bytes, | ||
// Node id. Ids are unique across all profiles collected between startSampling and stopSampling. | ||
id: NodeId, | ||
// Child nodes. | ||
children: SamplingHeapProfileNode[], | ||
|}>; | ||
|
||
// A single sample from a sampling profile. | ||
type SamplingHeapProfileSample = $ReadOnly<{| | ||
// Allocation size in bytes attributed to the sample. | ||
size: Bytes, | ||
// Id of the corresponding profile tree node. | ||
nodeId: NodeId, | ||
// Time-ordered sample ordinal number. It is unique across all profiles retrieved | ||
// between startSampling and stopSampling. | ||
ordinal: number, | ||
|}>; | ||
|
||
// Sampling profile. | ||
type SamplingHeapProfile = $ReadOnly<{| | ||
head: SamplingHeapProfileNode, | ||
samples: SamplingHeapProfileSample[], | ||
|}>; | ||
|
||
/** Lightly checks that the properties in SamplingHeapProfile are present. */ | ||
function isV8HeapProfile(json: mixed): boolean { | ||
if (!json || typeof json !== 'object') { | ||
return false; | ||
} | ||
|
||
if (typeof json.head !== 'object' || !Array.isArray(json.samples)) { | ||
return false; | ||
} | ||
|
||
const head: any = json.head; | ||
return ['callFrame', 'selfSize', 'children', 'id'].every( | ||
(prop) => prop in head | ||
); | ||
} | ||
|
||
export function attemptToConvertV8HeapProfile(json: mixed): Profile | null { | ||
if (!isV8HeapProfile(json)) { | ||
return null; | ||
} | ||
|
||
const profile = getEmptyProfile(); | ||
profile.meta.product = 'V8 Heap Profile'; | ||
profile.meta.importedFrom = 'V8 Heap Profile'; | ||
|
||
const thread = getEmptyThread(); | ||
// KTODO: If name is defaulted for heapprofile, it has this info? | ||
thread.pid = 0; | ||
thread.tid = 0; | ||
thread.name = 'Memory'; // KTODO: ??? | ||
|
||
const funcKeyToFuncId = new Map<string, IndexIntoFuncTable>(); | ||
const allocationsTable = getEmptyUnbalancedNativeAllocationsTable(); | ||
const { funcTable, stringTable, frameTable, stackTable } = thread; | ||
|
||
// Traverse the tree and populate the tables. | ||
const heapProfile = coerce<mixed, SamplingHeapProfile>(json); | ||
const nodeStack = [[heapProfile.head, null]]; | ||
while (nodeStack.length) { | ||
const [node, prefixStackIndex] = nodeStack.pop(); | ||
|
||
const { functionName, url, lineNumber, columnNumber, scriptId } = | ||
node.callFrame; | ||
const funcKey = `${functionName}:${scriptId}:${lineNumber}:${columnNumber}`; | ||
let funcId = funcKeyToFuncId.get(funcKey); | ||
if (funcId === undefined) { | ||
funcId = funcTable.length++; | ||
funcTable.isJS.push(true); | ||
funcTable.relevantForJS.push(false); | ||
const name = functionName !== '' ? functionName : '(anonymous)'; | ||
funcTable.name.push(stringTable.indexForString(name)); | ||
funcTable.resource.push(-1); | ||
funcTable.fileName.push(stringTable.indexForString(url)); | ||
funcTable.lineNumber.push(lineNumber === -1 ? null : lineNumber); | ||
funcTable.columnNumber.push(columnNumber === -1 ? null : columnNumber); | ||
funcKeyToFuncId.set(funcKey, funcId); | ||
} | ||
|
||
// Similar to the chrome.js importer, we use the fact that node id is a 1-based index: | ||
// https://chromium.googlesource.com/v8/v8.git/+/70de68560c496747ca51849c809a69e329e72bf9/src/profiler/sampling-heap-profiler.h#169 | ||
const nodeIndex = node.id; | ||
const frameIndex = nodeIndex - 1; | ||
if (prefixStackIndex === undefined) { | ||
throw new Error( | ||
'Unable to find the prefix stack index from a node index.' | ||
); | ||
} | ||
|
||
frameTable.address[frameIndex] = -1; | ||
frameTable.category[frameIndex] = 0; | ||
frameTable.subcategory[frameIndex] = 0; | ||
frameTable.func[frameIndex] = funcId; | ||
frameTable.nativeSymbol[frameIndex] = null; | ||
frameTable.innerWindowID[frameIndex] = 0; | ||
frameTable.implementation[frameIndex] = null; | ||
frameTable.line[frameIndex] = lineNumber === -1 ? null : lineNumber; | ||
frameTable.column[frameIndex] = columnNumber === -1 ? null : columnNumber; | ||
frameTable.length = Math.max(frameTable.length, frameIndex + 1); | ||
|
||
stackTable.frame.push(frameIndex); | ||
// KTODO: How does chrome inspector color the flame graph? Categories, heat or random? | ||
stackTable.category.push(0); | ||
stackTable.subcategory.push(0); | ||
stackTable.prefix.push(prefixStackIndex); | ||
stackTable.length++; | ||
|
||
allocationsTable.time.push(0); | ||
allocationsTable.stack.push(stackTable.length - 1); | ||
allocationsTable.weight.push(node.selfSize); | ||
allocationsTable.length++; | ||
|
||
nodeStack.push( | ||
...node.children.map((child) => [child, stackTable.length - 1]) | ||
); | ||
} | ||
|
||
thread.nativeAllocations = allocationsTable; | ||
profile.threads = [thread]; | ||
return profile; | ||
} |
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