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 Mar 27, 2023
1 parent aea780c commit b235b2c
Show file tree
Hide file tree
Showing 2 changed files with 260 additions and 0 deletions.
254 changes: 254 additions & 0 deletions src/profile-logic/import/v8-heap-profile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
/* 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,
IndexIntoCategoryList,
IndexIntoSubcategoryListForCategory,
} from 'firefox-profiler/types';

import {
getEmptyProfile,
getEmptyThread,
getEmptyUnbalancedNativeAllocationsTable,
} from '../data-structures';

import { coerce, ensureExists } from 'firefox-profiler/utils/flow';

// V8 Types Begin
// References used for heapprofile format:
// https://source.chromium.org/chromium/chromium/src/+/main:v8/include/js_protocol.pdl;l=699-729;drc=7b19557f8cb73895bda339c7a98decfb1dc9c5c2
// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/profiler/sampling-heap-profiler.h;drc=76372353c17d017ad220c51f7514e3b87a9888bb
// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/profiler/sampling-heap-profiler.cc;drc=2caf2ed610aa758ad1dcca603b49082678329f5b
// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/inspector/v8-heap-profiler-agent-impl.h;drc=3d59a3c2c16405eea59263300c5591c3283a2a0e
// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/inspector/v8-heap-profiler-agent-impl.cc;drc=3d59a3c2c16405eea59263300c5591c3283a2a0e

// 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[],
|}>;

// V8 Types End

type FunctionInfo = {
category: IndexIntoCategoryList,
subcategory: IndexIntoSubcategoryListForCategory,
isJS: boolean,
relevantForJS: boolean,
};

const CATEGORIES = [
{ name: 'Other', color: 'grey', subcategories: ['Other'] },
{
name: 'JavaScript',
color: 'yellow',
subcategories: [
'Node Built-in',
'Browser Extension',
'Dependency',
'Other',
],
},
{ name: 'Native', color: 'blue', subcategories: ['V8', 'Other'] },
];

/**
* Nested map used for convenience to get indices into categories and the respective subcategories.
* Is of the form: [category name][subcategory name] -> {category: index, subcategory: index}.
*/
const CATEGORY_IDX_MAP = Object.fromEntries(
CATEGORIES.map(({ name, subcategories }, i) => [
name,
Object.fromEntries(
subcategories.map((subCat, j) => [
subCat,
{ category: i, subcategory: j },
])
),
])
);

function getFunctionInfo(callFrame: CallFrame): FunctionInfo {
const { functionName, scriptId, url } = callFrame;
// V8 categorization and isJS checks were made based on:
// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/profiler/sampling-heap-profiler.cc;l=175-204;drc=2caf2ed610aa758ad1dcca603b49082678329f5b
// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/profiler/sampling-heap-profiler.cc;l=59-60;drc=61bc5ca953c07dca60dd1e4de000da97e7bc4e3f
const isJS = scriptId !== '0' || functionName === '(JS)';
if (isJS) {
let subcategory;
if (url.startsWith('node:')) {
subcategory = 'Node Built-in';
} else if (url.startsWith('chrome-extension://')) {
subcategory = 'Browser Extension';
} else if (/(\/|\\)node_modules(\/|\\)/.test(url)) {
subcategory = 'Dependency';
} else {
subcategory = 'Other';
}
return {
...CATEGORY_IDX_MAP.JavaScript[subcategory],
isJS,
relevantForJS: false,
};
}

switch (functionName) {
case '(GC)':
case '(PARSER)':
case '(COMPILER)':
case '(BYTECODE_COMPILER)':
case '(V8 API)':
return { ...CATEGORY_IDX_MAP.Native.V8, isJS, relevantForJS: false };
case '(EXTERNAL)':
return { ...CATEGORY_IDX_MAP.Native.Other, isJS, relevantForJS: false };
case '(root)':
return { ...CATEGORY_IDX_MAP.Other.Other, isJS, relevantForJS: false };
default:
return { ...CATEGORY_IDX_MAP.Native.V8, isJS, relevantForJS: true };
}
}

/** 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 = ensureExists(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';
profile.meta.categories = CATEGORIES;

const thread = getEmptyThread();
// KTODO: If name is defaulted for heapprofile, it has this info?
thread.pid = '0';
thread.tid = 0;
thread.name = 'Total Allocated Bytes';

const funcKeyToFuncId = new Map<string, number>();
const allocationsTable = getEmptyUnbalancedNativeAllocationsTable();
const { funcTable, stringTable, frameTable, stackTable } = thread;

const { head } = coerce<mixed, SamplingHeapProfile>(json);
// Traverse the tree and populate the tables.
// Each entry of the traversal stack is a pair (heap node, stack table index of parent node).
const traversalStack = [[head, null]];
while (traversalStack.length) {
const [node, prefixStackIndex] = traversalStack.pop();

const { functionName, url, scriptId, lineNumber, columnNumber } =
node.callFrame;
// Line and column number are 1-based in the firefox profiler.
const line = lineNumber >= 0 ? lineNumber + 1 : null;
const column = columnNumber >= 0 ? columnNumber + 1 : null;
const funcKey = `${functionName}:${scriptId}:${line || 0}:${column || 0}`;
let funcId = funcKeyToFuncId.get(funcKey);
if (funcId === undefined) {
funcId = funcTable.length++;
funcKeyToFuncId.set(funcKey, funcId);

const funcInfo = getFunctionInfo(node.callFrame);
funcTable.isJS.push(funcInfo.isJS);
funcTable.relevantForJS.push(funcInfo.relevantForJS);
funcTable.name.push(
stringTable.indexForString(functionName || '(anonymous)')
);
funcTable.resource.push(-1);
funcTable.fileName.push(stringTable.indexForString(url));
funcTable.lineNumber.push(line);
funcTable.columnNumber.push(column);

// The frame table is being populated here too because we don't get any new information,
// so they can be deduplicated.
frameTable.address.push(-1);
frameTable.category.push(funcInfo.category);
frameTable.subcategory.push(funcInfo.subcategory);
frameTable.func.push(funcId);
frameTable.nativeSymbol.push(null);
frameTable.innerWindowID.push(0);
frameTable.implementation.push(null);
frameTable.line.push(line);
frameTable.column.push(column);
frameTable.length++;
}

allocationsTable.time.push(0);
allocationsTable.stack.push(stackTable.length);
allocationsTable.weight.push(node.selfSize);
allocationsTable.length++;

stackTable.frame.push(funcId);
stackTable.category.push(ensureExists(frameTable.category[funcId]));
stackTable.subcategory.push(ensureExists(frameTable.subcategory[funcId]));
stackTable.prefix.push(prefixStackIndex);
traversalStack.push(
...node.children.map((child) => [child, stackTable.length])
);
stackTable.length++;
}

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 @@ -1723,6 +1724,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 b235b2c

Please sign in to comment.