Skip to content

Commit

Permalink
Add V8 .heapprofile importer
Browse files Browse the repository at this point in the history
  • Loading branch information
krsh732 committed Feb 10, 2023
1 parent 6e18590 commit 632d47f
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 0 deletions.
170 changes: 170 additions & 0 deletions src/profile-logic/import/v8-heap-profile.js
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;
}
6 changes: 6 additions & 0 deletions src/profile-logic/process-profile.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { attemptToConvertChromeProfile } from './import/chrome';
import { attemptToConvertDhat } from './import/dhat';
import { attemptToConvertV8HeapProfile } from './import/v8-heap-profile';
import { AddressLocator } from './address-locator';
import { UniqueStringArray } from '../utils/unique-string-array';
import {
Expand Down Expand Up @@ -1690,6 +1691,11 @@ export async function unserializeProfileOfArbitraryFormat(
return processedDhat;
}

const processedV8HeapProfile = attemptToConvertV8HeapProfile(json);
if (processedV8HeapProfile) {
return processedV8HeapProfile;
}

// Else: Treat it as a Gecko profile and just attempt to process it.
return processGeckoOrDevToolsProfile(json);
} catch (e) {
Expand Down

0 comments on commit 632d47f

Please sign in to comment.