chrome-devtools-frontend
Version:
Chrome DevTools UI
265 lines (238 loc) • 9.73 kB
text/typescript
/*
* Copyright (C) 2012 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import * as Platform from '../../core/platform/platform.js';
import * as TextUtils from '../../models/text_utils/text_utils.js';
import {type HeapSnapshotHeader, HeapSnapshotProgress, JSHeapSnapshot, type Profile} from './HeapSnapshot.js';
import type {HeapSnapshotWorkerDispatcher} from './HeapSnapshotWorkerDispatcher.js';
export class HeapSnapshotLoader {
readonly #progress: HeapSnapshotProgress;
#buffer: string[];
#dataCallback: ((value: string|PromiseLike<string>) => void)|null;
#done: boolean;
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
#snapshot?: Record<string, any>;
#array!: Platform.TypedArrayUtilities.BigUint32Array|null;
#arrayIndex!: number;
#json = '';
parsingComplete: Promise<void>;
constructor(dispatcher: HeapSnapshotWorkerDispatcher) {
this.#reset();
this.#progress = new HeapSnapshotProgress(dispatcher);
this.#buffer = [];
this.#dataCallback = null;
this.#done = false;
this.parsingComplete = this.#parseInput();
}
dispose(): void {
this.#reset();
}
#reset(): void {
this.#json = '';
this.#snapshot = undefined;
}
close(): void {
this.#done = true;
if (this.#dataCallback) {
this.#dataCallback('');
}
}
async buildSnapshot(secondWorker: MessagePort): Promise<JSHeapSnapshot> {
this.#snapshot = this.#snapshot || {};
this.#progress.updateStatus('Processing snapshot…');
const result = new JSHeapSnapshot((this.#snapshot as Profile), this.#progress);
await result.initialize(secondWorker);
this.#reset();
return result;
}
#parseUintArray(): boolean {
let index = 0;
const char0 = '0'.charCodeAt(0);
const char9 = '9'.charCodeAt(0);
const closingBracket = ']'.charCodeAt(0);
const length = this.#json.length;
while (true) {
while (index < length) {
const code = this.#json.charCodeAt(index);
if (char0 <= code && code <= char9) {
break;
} else if (code === closingBracket) {
this.#json = this.#json.slice(index + 1);
return false;
}
++index;
}
if (index === length) {
this.#json = '';
return true;
}
let nextNumber = 0;
const startIndex = index;
while (index < length) {
const code = this.#json.charCodeAt(index);
if (char0 > code || code > char9) {
break;
}
nextNumber *= 10;
nextNumber += (code - char0);
++index;
}
if (index === length) {
this.#json = this.#json.slice(startIndex);
return true;
}
if (!this.#array) {
throw new Error('Array not instantiated');
}
this.#array.setValue(this.#arrayIndex++, nextNumber);
}
}
#parseStringsArray(): void {
this.#progress.updateStatus('Parsing strings…');
const closingBracketIndex = this.#json.lastIndexOf(']');
if (closingBracketIndex === -1) {
throw new Error('Incomplete JSON');
}
this.#json = this.#json.slice(0, closingBracketIndex + 1);
if (!this.#snapshot) {
throw new Error('No snapshot in parseStringsArray');
}
this.#snapshot.strings = JSON.parse(this.#json);
}
write(chunk: string): void {
this.#buffer.push(chunk);
if (!this.#dataCallback) {
return;
}
this.#dataCallback(this.#buffer.shift() as string);
this.#dataCallback = null;
}
#fetchChunk(): Promise<string> {
// This method shoudln't be entered more than once since parsing happens
// sequentially. This means it's fine to stash away a single #dataCallback
// instead of an array of them.
if (this.#buffer.length > 0) {
return Promise.resolve(this.#buffer.shift() as string);
}
const {promise, resolve} = Promise.withResolvers<string>();
this.#dataCallback = resolve;
return promise;
}
async #findToken(token: string, startIndex?: number): Promise<number> {
while (true) {
const pos = this.#json.indexOf(token, startIndex || 0);
if (pos !== -1) {
return pos;
}
startIndex = this.#json.length - token.length + 1;
this.#json += await this.#fetchChunk();
}
}
async #parseArray(name: string, title: string, length?: number):
Promise<Platform.TypedArrayUtilities.BigUint32Array> {
const nameIndex = await this.#findToken(name);
const bracketIndex = await this.#findToken('[', nameIndex);
this.#json = this.#json.slice(bracketIndex + 1);
this.#array = length === undefined ? Platform.TypedArrayUtilities.createExpandableBigUint32Array() :
Platform.TypedArrayUtilities.createFixedBigUint32Array(length);
this.#arrayIndex = 0;
while (this.#parseUintArray()) {
if (length) {
this.#progress.updateProgress(title, this.#arrayIndex, this.#array.length);
} else {
this.#progress.updateStatus(title);
}
this.#json += await this.#fetchChunk();
}
const result = this.#array;
this.#array = null;
return result;
}
async #parseInput(): Promise<void> {
const snapshotToken = '"snapshot"';
const snapshotTokenIndex = await this.#findToken(snapshotToken);
if (snapshotTokenIndex === -1) {
throw new Error('Snapshot token not found');
}
this.#progress.updateStatus('Loading snapshot info…');
const json = this.#json.slice(snapshotTokenIndex + snapshotToken.length + 1);
let jsonTokenizerDone = false;
const jsonTokenizer = new TextUtils.TextUtils.BalancedJSONTokenizer(metaJSON => {
this.#json = jsonTokenizer.remainder();
jsonTokenizerDone = true;
this.#snapshot = this.#snapshot || {};
this.#snapshot.snapshot = (JSON.parse(metaJSON) as HeapSnapshotHeader);
});
jsonTokenizer.write(json);
while (!jsonTokenizerDone) {
jsonTokenizer.write(await this.#fetchChunk());
}
this.#snapshot = this.#snapshot || {};
const nodes = await this.#parseArray(
'"nodes"', 'Loading nodes… {PH1}%',
this.#snapshot.snapshot.meta.node_fields.length * this.#snapshot.snapshot.node_count);
this.#snapshot.nodes = nodes;
const edges = await this.#parseArray(
'"edges"', 'Loading edges… {PH1}%',
this.#snapshot.snapshot.meta.edge_fields.length * this.#snapshot.snapshot.edge_count);
this.#snapshot.edges = edges;
if (this.#snapshot.snapshot.trace_function_count) {
const traceFunctionInfos = await this.#parseArray(
'"trace_function_infos"', 'Loading allocation traces… {PH1}%',
this.#snapshot.snapshot.meta.trace_function_info_fields.length *
this.#snapshot.snapshot.trace_function_count);
this.#snapshot.trace_function_infos = traceFunctionInfos.asUint32ArrayOrFail();
const thisTokenEndIndex = await this.#findToken(':');
const nextTokenIndex = await this.#findToken('"', thisTokenEndIndex);
const openBracketIndex = this.#json.indexOf('[');
const closeBracketIndex = this.#json.lastIndexOf(']', nextTokenIndex);
this.#snapshot.trace_tree = JSON.parse(this.#json.substring(openBracketIndex, closeBracketIndex + 1));
this.#json = this.#json.slice(closeBracketIndex + 1);
}
if (this.#snapshot.snapshot.meta.sample_fields) {
const samples = await this.#parseArray('"samples"', 'Loading samples…');
this.#snapshot.samples = samples.asArrayOrFail();
}
if (this.#snapshot.snapshot.meta['location_fields']) {
const locations = await this.#parseArray('"locations"', 'Loading locations…');
this.#snapshot.locations = locations.asArrayOrFail();
} else {
this.#snapshot.locations = [];
}
this.#progress.updateStatus('Loading strings…');
const stringsTokenIndex = await this.#findToken('"strings"');
const bracketIndex = await this.#findToken('[', stringsTokenIndex);
this.#json = this.#json.slice(bracketIndex);
while (this.#buffer.length > 0 || !this.#done) {
this.#json += await this.#fetchChunk();
}
this.#parseStringsArray();
}
}