789 lines
23 KiB
JavaScript
789 lines
23 KiB
JavaScript
/* 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/. */
|
|
|
|
/**
|
|
* Parses a JSON string and creates a Firefox profiler profile describing
|
|
* which parts of the JSON use up how many bytes.
|
|
*/
|
|
|
|
// Categories for different JSON types
|
|
const JSON_CATEGORIES = {
|
|
OBJECT: { name: "Object", color: "grey" },
|
|
ARRAY: { name: "Array", color: "grey" },
|
|
NULL: { name: "Null", color: "yellow" },
|
|
BOOL: { name: "Bool", color: "brown" },
|
|
NUMBER: { name: "Number", color: "green" },
|
|
STRING: { name: "String", color: "blue" },
|
|
PROPERTY_KEY: { name: "Property Key", color: "lightblue" },
|
|
};
|
|
|
|
const MAX_SAMPLE_COUNT = 100000;
|
|
|
|
class JsonSizeProfiler {
|
|
/**
|
|
* @param {string} jsonString - The JSON string to profile.
|
|
* @param {string} [filename] - Optional filename for the profile metadata.
|
|
*/
|
|
constructor(jsonString, filename) {
|
|
this.jsonString = jsonString;
|
|
this.filename = filename;
|
|
this.pos = 0;
|
|
this.bytePos = 0;
|
|
this.lastAdvancedBytePos = 0;
|
|
this.scopeStack = [];
|
|
|
|
// Caching structures
|
|
this.stringTable = new Map();
|
|
this.stringTableArray = [];
|
|
this.stackCache = new Map();
|
|
this.frameCache = new Map();
|
|
this.nodeCache = new Map();
|
|
|
|
// Profile tables - stored in final array format
|
|
this.frameTable = {
|
|
func: [],
|
|
category: [],
|
|
};
|
|
this.stackTable = {
|
|
frame: [],
|
|
prefix: [],
|
|
};
|
|
|
|
// Aggregation state
|
|
this.topStackHandle = null;
|
|
const totalBytes = new TextEncoder().encode(jsonString).length;
|
|
this.bytesPerSample = Math.max(
|
|
1,
|
|
Math.min(1000000, Math.floor(totalBytes / MAX_SAMPLE_COUNT))
|
|
);
|
|
this.sampleCount = 0;
|
|
this.aggregationMap = new Map();
|
|
this.aggregationStartPos = 0;
|
|
this.samples = {
|
|
stack: [],
|
|
time: [],
|
|
cpuDelta: [],
|
|
weight: [],
|
|
};
|
|
|
|
// Categories initialization
|
|
this.categories = [];
|
|
this.categoryMap = new Map();
|
|
for (const [key, value] of Object.entries(JSON_CATEGORIES)) {
|
|
const catIndex = this.categories.length;
|
|
this.categories.push(value);
|
|
this.categoryMap.set(key, catIndex);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Interns a string into the string table, returning its index.
|
|
*
|
|
* @param {string} str - The string to intern.
|
|
* @returns {number} The index of the string in the string table.
|
|
*/
|
|
internString(str) {
|
|
if (!this.stringTable.has(str)) {
|
|
const index = this.stringTableArray.length;
|
|
this.stringTableArray.push(str);
|
|
this.stringTable.set(str, index);
|
|
}
|
|
return this.stringTable.get(str);
|
|
}
|
|
|
|
/**
|
|
* Gets or creates a frame in the frame table.
|
|
*
|
|
* @param {string} funcName - The function name for the frame.
|
|
* @param {number} category - The category index for the frame.
|
|
* @returns {number} The frame index.
|
|
*/
|
|
getOrCreateFrame(funcName, category) {
|
|
const funcIndex = this.internString(funcName);
|
|
const cacheKey = `${funcIndex}:${category}`;
|
|
if (this.frameCache.has(cacheKey)) {
|
|
return this.frameCache.get(cacheKey);
|
|
}
|
|
|
|
const frameIndex = this.frameTable.func.length;
|
|
this.frameTable.func.push(funcIndex);
|
|
this.frameTable.category.push(category);
|
|
this.frameCache.set(cacheKey, frameIndex);
|
|
return frameIndex;
|
|
}
|
|
|
|
/**
|
|
* Gets or creates a stack in the stack table.
|
|
*
|
|
* @param {number} frameIndex - The frame index for this stack entry.
|
|
* @param {number|null} prefix - The parent stack index, or null for root.
|
|
* @returns {number} The stack index.
|
|
*/
|
|
getOrCreateStack(frameIndex, prefix) {
|
|
const cacheKey = `${frameIndex}:${prefix === null ? "null" : prefix}`;
|
|
if (this.stackCache.has(cacheKey)) {
|
|
return this.stackCache.get(cacheKey);
|
|
}
|
|
|
|
const stackIndex = this.stackTable.frame.length;
|
|
this.stackTable.frame.push(frameIndex);
|
|
this.stackTable.prefix.push(prefix);
|
|
this.stackCache.set(cacheKey, stackIndex);
|
|
return stackIndex;
|
|
}
|
|
|
|
/**
|
|
* Gets a stack handle for a given path and JSON type.
|
|
*
|
|
* @param {number|null} parentStackHandle - The parent stack handle.
|
|
* @param {string} path - The path in the JSON structure.
|
|
* @param {string} jsonType - The JSON type (OBJECT, ARRAY, STRING, etc.).
|
|
* @returns {number} The stack handle.
|
|
*/
|
|
getStack(parentStackHandle, path, jsonType) {
|
|
const cacheKey = `${parentStackHandle === null ? "null" : parentStackHandle}:${path}:${jsonType}`;
|
|
if (this.nodeCache.has(cacheKey)) {
|
|
return this.nodeCache.get(cacheKey);
|
|
}
|
|
|
|
const category = this.categoryMap.get(jsonType);
|
|
const frameIndex = this.getOrCreateFrame(path, category);
|
|
const stackHandle = this.getOrCreateStack(frameIndex, parentStackHandle);
|
|
this.nodeCache.set(cacheKey, stackHandle);
|
|
return stackHandle;
|
|
}
|
|
|
|
/**
|
|
* Moves position forward, updating both character and byte positions.
|
|
*
|
|
* This function tracks both character positions (in the UTF-16 string) and
|
|
* byte positions (in the original UTF-8 file) because:
|
|
* 1. The original JSON file contained bytes which formed a UTF-8 string.
|
|
* 2. When the JSON viewer loaded the JSON, these bytes were parsed into a
|
|
* UTF-16 string (or "potentially ill-formed UTF-16" aka WTF-16), which
|
|
* means all characters now take 2 bytes in memory even though most
|
|
* originally only took 1 byte in the UTF-8 file.
|
|
* 3. this.jsonString.charCodeAt() indexes into the UTF-16 string.
|
|
* 4. By examining the UTF-16 code unit value, we "recover" how many bytes
|
|
* this character originally occupied in the UTF-8 file.
|
|
*
|
|
* Note: this.pos will never stop in the middle of a surrogate pair.
|
|
* When this function returns, this.pos >= newCharPos.
|
|
*
|
|
* @param {number} newCharPos - The new character position to move to.
|
|
*/
|
|
advanceToPos(newCharPos) {
|
|
while (this.pos < newCharPos) {
|
|
const code = this.jsonString.charCodeAt(this.pos);
|
|
if (code >= 0xd800 && code <= 0xdbff) {
|
|
// Surrogate pair - always 4 bytes in UTF-8
|
|
this.bytePos += 4;
|
|
this.pos += 2;
|
|
} else {
|
|
// Single UTF-16 code unit - calculate UTF-8 byte length
|
|
if (code <= 0x7f) {
|
|
this.bytePos += 1;
|
|
} else if (code <= 0x7ff) {
|
|
this.bytePos += 2;
|
|
} else {
|
|
// 3-byte UTF-8 characters (U+0800 to U+FFFF)
|
|
// Examples: CJK characters like "中", symbols like "€", etc.
|
|
this.bytePos += 3;
|
|
}
|
|
this.pos++;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Moves position forward by a number of ASCII characters (1 byte each).
|
|
*
|
|
* @param {number} count - The number of ASCII characters to advance.
|
|
*/
|
|
advanceByAsciiChars(count) {
|
|
this.pos += count;
|
|
this.bytePos += count;
|
|
}
|
|
|
|
/**
|
|
* Parses a primitive value with stack tracking.
|
|
*
|
|
* @param {string} path - The path to the value in the JSON structure.
|
|
* @param {string} typeName - The type name (STRING, NUMBER, BOOL, NULL).
|
|
* @param {Function} parseFunc - The function to call to parse the value.
|
|
*/
|
|
parsePrimitive(path, typeName, parseFunc) {
|
|
this.recordBytesConsumed();
|
|
|
|
const scope = this.getCurrentScope();
|
|
const stackHandle = this.getStack(
|
|
scope.stackHandle,
|
|
`${path} (${typeName.toLowerCase()})`,
|
|
typeName
|
|
);
|
|
this.topStackHandle = stackHandle;
|
|
|
|
parseFunc();
|
|
|
|
this.recordBytesConsumed();
|
|
this.topStackHandle = scope.stackHandle;
|
|
}
|
|
|
|
/**
|
|
* Exits the current scope (object or array).
|
|
*/
|
|
exitScope() {
|
|
this.recordBytesConsumed();
|
|
this.scopeStack.pop();
|
|
const prevScope = this.getCurrentScope();
|
|
this.topStackHandle = prevScope.stackHandle;
|
|
}
|
|
|
|
/**
|
|
* Records bytes consumed since the last call.
|
|
*
|
|
* This method accumulates byte counts in aggregationMap instead of immediately
|
|
* creating profile samples. This aggregation limits the total sample count to
|
|
* approximately MAX_SAMPLE_COUNT (100,000), which keeps the Firefox Profiler
|
|
* UI responsive even for very large JSON files.
|
|
*
|
|
* For small files (< 100KB), bytesPerSample = 1, so samples are created
|
|
* frequently. For large files, bytesPerSample scales proportionally
|
|
* (e.g., 100 for a 10MB file), so samples are batched more aggressively.
|
|
*
|
|
* Samples are flushed when we have accumulated multiple stacks and have
|
|
* consumed enough bytes to justify creating new samples.
|
|
*/
|
|
recordBytesConsumed() {
|
|
if (this.bytePos === 0 && this.lastAdvancedBytePos === 0) {
|
|
return;
|
|
}
|
|
if (this.bytePos < this.lastAdvancedBytePos) {
|
|
throw new Error(
|
|
`Cannot advance backwards: ${this.lastAdvancedBytePos} -> ${this.bytePos}`
|
|
);
|
|
}
|
|
if (this.bytePos === this.lastAdvancedBytePos) {
|
|
return;
|
|
}
|
|
|
|
const byteDelta = this.bytePos - this.lastAdvancedBytePos;
|
|
const stackHandle = this.topStackHandle;
|
|
if (stackHandle !== null) {
|
|
const current = this.aggregationMap.get(stackHandle) || 0;
|
|
this.aggregationMap.set(stackHandle, current + byteDelta);
|
|
}
|
|
|
|
this.lastAdvancedBytePos = this.bytePos;
|
|
|
|
// Flush accumulated samples when we have multiple stacks and enough bytes
|
|
const aggregatedStackCount = this.aggregationMap.size;
|
|
if (aggregatedStackCount > 1) {
|
|
const sampleCountIfWeFlush = this.sampleCount + aggregatedStackCount;
|
|
const allowedSampleCount = Math.floor(
|
|
this.lastAdvancedBytePos / this.bytesPerSample
|
|
);
|
|
if (sampleCountIfWeFlush <= allowedSampleCount) {
|
|
this.recordSamples();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Flushes accumulated byte counts to the samples table.
|
|
*/
|
|
recordSamples() {
|
|
let synthLastPos = this.aggregationStartPos;
|
|
|
|
for (const [stackHandle, accDelta] of this.aggregationMap.entries()) {
|
|
const synthPos = synthLastPos + accDelta;
|
|
|
|
// First sample at start position
|
|
this.samples.stack.push(stackHandle);
|
|
this.samples.time.push(synthLastPos);
|
|
this.samples.cpuDelta.push(0);
|
|
this.samples.weight.push(0);
|
|
|
|
// Second sample at end position with size
|
|
this.samples.stack.push(stackHandle);
|
|
this.samples.time.push(synthPos);
|
|
this.samples.cpuDelta.push(accDelta * 1000);
|
|
this.samples.weight.push(accDelta);
|
|
|
|
synthLastPos = synthPos;
|
|
this.sampleCount += 1;
|
|
}
|
|
|
|
this.aggregationStartPos = this.lastAdvancedBytePos;
|
|
this.aggregationMap.clear();
|
|
}
|
|
|
|
/**
|
|
* Gets the current scope from the scope stack.
|
|
*
|
|
* @returns {object} An object with stackHandle, path, and arrayDepth.
|
|
*/
|
|
getCurrentScope() {
|
|
if (this.scopeStack.length === 0) {
|
|
return {
|
|
stackHandle: null,
|
|
path: "json",
|
|
arrayDepth: 0,
|
|
};
|
|
}
|
|
|
|
const scope = this.scopeStack[this.scopeStack.length - 1];
|
|
return {
|
|
stackHandle: scope.stackHandle,
|
|
path: scope.pathForValue || scope.pathForElems || scope.path,
|
|
arrayDepth: scope.arrayDepth,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Skips whitespace characters in the JSON string.
|
|
*/
|
|
skipWhitespace() {
|
|
while (this.pos < this.jsonString.length) {
|
|
const ch = this.jsonString[this.pos];
|
|
if (ch !== " " && ch !== "\t" && ch !== "\n" && ch !== "\r") {
|
|
break;
|
|
}
|
|
this.advanceByAsciiChars(1); // Whitespace is always ASCII (1 byte each)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses a JSON value at the current position.
|
|
*
|
|
* @param {string} path - The path to this value in the JSON structure.
|
|
*/
|
|
parseValue(path) {
|
|
this.skipWhitespace();
|
|
|
|
if (this.pos >= this.jsonString.length) {
|
|
throw new Error("Unexpected end of JSON");
|
|
}
|
|
|
|
const ch = this.jsonString[this.pos];
|
|
|
|
if (ch === "{") {
|
|
this.parseObject(path);
|
|
} else if (ch === "[") {
|
|
this.parseArray(path);
|
|
} else if (ch === '"') {
|
|
this.parseString(path);
|
|
} else if (ch === "t" || ch === "f") {
|
|
this.parseBool(path);
|
|
} else if (ch === "n") {
|
|
this.parseNull(path);
|
|
} else {
|
|
this.parseNumber(path);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses a JSON object at the current position.
|
|
*
|
|
* @param {string} path - The path to this object in the JSON structure.
|
|
*/
|
|
parseObject(path) {
|
|
this.recordBytesConsumed();
|
|
|
|
const parentScope = this.getCurrentScope();
|
|
const stackHandle = this.getStack(
|
|
parentScope.stackHandle,
|
|
`${path} (object)`,
|
|
"OBJECT"
|
|
);
|
|
|
|
this.scopeStack.push({
|
|
type: "object",
|
|
stackHandle,
|
|
path,
|
|
pathForValue: null,
|
|
arrayDepth: parentScope.arrayDepth,
|
|
});
|
|
this.topStackHandle = stackHandle;
|
|
|
|
this.advanceByAsciiChars(1); // skip '{'
|
|
|
|
let first = true;
|
|
while (this.pos < this.jsonString.length) {
|
|
this.skipWhitespace();
|
|
|
|
if (this.jsonString[this.pos] === "}") {
|
|
this.advanceByAsciiChars(1); // skip '}'
|
|
break;
|
|
}
|
|
|
|
if (!first) {
|
|
if (this.jsonString[this.pos] !== ",") {
|
|
throw new Error(`Expected ',' at position ${this.pos}`);
|
|
}
|
|
this.advanceByAsciiChars(1); // skip ','
|
|
this.skipWhitespace();
|
|
}
|
|
first = false;
|
|
|
|
// Parse property key
|
|
if (this.jsonString[this.pos] !== '"') {
|
|
throw new Error(`Expected property key at position ${this.pos}`);
|
|
}
|
|
|
|
this.recordBytesConsumed();
|
|
|
|
const key = this.parseStringValue();
|
|
const propertyPath = `${path}.${key}`;
|
|
|
|
const propKeyStack = this.getStack(
|
|
stackHandle,
|
|
`${propertyPath} (property key)`,
|
|
"PROPERTY_KEY"
|
|
);
|
|
this.topStackHandle = propKeyStack;
|
|
|
|
// Update scope with current property path
|
|
this.scopeStack[this.scopeStack.length - 1].pathForValue = propertyPath;
|
|
|
|
this.skipWhitespace();
|
|
if (this.jsonString[this.pos] !== ":") {
|
|
throw new Error(`Expected ':' at position ${this.pos}`);
|
|
}
|
|
this.advanceByAsciiChars(1); // skip ':'
|
|
|
|
// Parse property value
|
|
this.parseValue(propertyPath);
|
|
}
|
|
|
|
this.exitScope();
|
|
}
|
|
|
|
/**
|
|
* Parses a JSON array at the current position.
|
|
*
|
|
* @param {string} path - The path to this array in the JSON structure.
|
|
*/
|
|
parseArray(path) {
|
|
this.recordBytesConsumed();
|
|
|
|
const parentScope = this.getCurrentScope();
|
|
|
|
const INDEXER_CHARS = "ijklmnopqrstuvwxyz";
|
|
const indexer =
|
|
INDEXER_CHARS[parentScope.arrayDepth % INDEXER_CHARS.length];
|
|
const pathForElems = `${path}[${indexer}]`;
|
|
|
|
const stackHandle = this.getStack(
|
|
parentScope.stackHandle,
|
|
`${path} (array)`,
|
|
"ARRAY"
|
|
);
|
|
|
|
this.topStackHandle = stackHandle;
|
|
this.scopeStack.push({
|
|
type: "array",
|
|
stackHandle,
|
|
pathForElems,
|
|
arrayDepth: parentScope.arrayDepth + 1,
|
|
});
|
|
|
|
this.advanceByAsciiChars(1); // skip '['
|
|
|
|
let first = true;
|
|
while (this.pos < this.jsonString.length) {
|
|
this.skipWhitespace();
|
|
|
|
if (this.jsonString[this.pos] === "]") {
|
|
this.advanceByAsciiChars(1); // skip ']'
|
|
break;
|
|
}
|
|
|
|
if (!first) {
|
|
if (this.jsonString[this.pos] !== ",") {
|
|
throw new Error(`Expected ',' at position ${this.pos}`);
|
|
}
|
|
this.advanceByAsciiChars(1); // skip ','
|
|
this.skipWhitespace();
|
|
}
|
|
first = false;
|
|
|
|
this.parseValue(pathForElems);
|
|
}
|
|
|
|
this.exitScope();
|
|
}
|
|
|
|
/**
|
|
* Parses a JSON string at the current position.
|
|
*
|
|
* @param {string} path - The path to this string in the JSON structure.
|
|
*/
|
|
parseString(path) {
|
|
this.parsePrimitive(path, "STRING", () => this.parseStringValue());
|
|
}
|
|
|
|
/**
|
|
* Parses a JSON string value and returns it.
|
|
*
|
|
* @returns {string} The parsed string value.
|
|
*/
|
|
parseStringValue() {
|
|
this.advanceByAsciiChars(1); // skip opening quote (ASCII)
|
|
let value = "";
|
|
|
|
while (this.pos < this.jsonString.length) {
|
|
const ch = this.jsonString[this.pos];
|
|
|
|
if (ch === '"') {
|
|
this.advanceByAsciiChars(1); // closing quote (ASCII)
|
|
break;
|
|
} else if (ch === "\\") {
|
|
this.advanceByAsciiChars(1); // backslash (ASCII)
|
|
if (this.pos >= this.jsonString.length) {
|
|
throw new Error("Unexpected end of JSON in string");
|
|
}
|
|
const escaped = this.jsonString[this.pos];
|
|
if (escaped === '"' || escaped === "\\" || escaped === "/") {
|
|
value += escaped;
|
|
} else if (escaped === "b") {
|
|
value += "\b";
|
|
} else if (escaped === "f") {
|
|
value += "\f";
|
|
} else if (escaped === "n") {
|
|
value += "\n";
|
|
} else if (escaped === "r") {
|
|
value += "\r";
|
|
} else if (escaped === "t") {
|
|
value += "\t";
|
|
} else if (escaped === "u") {
|
|
// Unicode escape - \uXXXX (all ASCII)
|
|
this.advanceByAsciiChars(1);
|
|
const hex = this.jsonString.slice(this.pos, this.pos + 4);
|
|
value += String.fromCharCode(parseInt(hex, 16));
|
|
this.advanceByAsciiChars(3); // skip the 4 hex digits (already moved 1)
|
|
}
|
|
this.advanceByAsciiChars(1); // escaped char (ASCII)
|
|
} else {
|
|
// Regular character - may be multi-byte UTF-8
|
|
value += ch;
|
|
this.advanceToPos(this.pos + 1);
|
|
}
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Parses a JSON number at the current position.
|
|
*
|
|
* @param {string} path - The path to this number in the JSON structure.
|
|
*/
|
|
parseNumber(path) {
|
|
this.parsePrimitive(path, "NUMBER", () => {
|
|
// Skip all number characters: digits, decimal point, exponent, signs
|
|
while (this.pos < this.jsonString.length) {
|
|
const ch = this.jsonString[this.pos];
|
|
if (
|
|
(ch >= "0" && ch <= "9") ||
|
|
ch === "." ||
|
|
ch === "e" ||
|
|
ch === "E" ||
|
|
ch === "+" ||
|
|
ch === "-"
|
|
) {
|
|
this.advanceByAsciiChars(1);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Parses a JSON boolean at the current position.
|
|
*
|
|
* @param {string} path - The path to this boolean in the JSON structure.
|
|
*/
|
|
parseBool(path) {
|
|
this.parsePrimitive(path, "BOOL", () => {
|
|
if (this.jsonString.slice(this.pos, this.pos + 4) === "true") {
|
|
this.advanceByAsciiChars(4);
|
|
} else if (this.jsonString.slice(this.pos, this.pos + 5) === "false") {
|
|
this.advanceByAsciiChars(5);
|
|
} else {
|
|
throw new Error(`Expected boolean at position ${this.pos}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Parses a JSON null at the current position.
|
|
*
|
|
* @param {string} path - The path to this null in the JSON structure.
|
|
*/
|
|
parseNull(path) {
|
|
this.parsePrimitive(path, "NULL", () => {
|
|
if (this.jsonString.slice(this.pos, this.pos + 4) === "null") {
|
|
this.advanceByAsciiChars(4);
|
|
} else {
|
|
throw new Error(`Expected null at position ${this.pos}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Parses the JSON string and generates a Firefox profiler profile.
|
|
*
|
|
* @returns {object} A Firefox profiler profile object.
|
|
*/
|
|
parse() {
|
|
this.parseValue("json");
|
|
|
|
// Move to end of string to account for any trailing content
|
|
const remaining = this.jsonString.length - this.pos;
|
|
if (remaining > 0) {
|
|
this.advanceByAsciiChars(remaining);
|
|
}
|
|
|
|
// Advance to final position
|
|
if (this.bytePos !== this.lastAdvancedBytePos) {
|
|
this.recordBytesConsumed();
|
|
}
|
|
|
|
this.recordSamples();
|
|
|
|
const frameCount = this.frameTable.func.length;
|
|
const funcCount = this.stringTableArray.length;
|
|
const sampleCount = this.samples.stack.length;
|
|
// Convert absolute times to deltas in place
|
|
for (let i = sampleCount - 1; i > 0; i--) {
|
|
this.samples.time[i] = this.samples.time[i] - this.samples.time[i - 1];
|
|
}
|
|
// First element stays as-is (it's already a delta from 0)
|
|
|
|
const meta = {
|
|
version: 56,
|
|
preprocessedProfileVersion: 56,
|
|
startTime: 0,
|
|
fileSize: this.lastAdvancedBytePos,
|
|
processType: 0,
|
|
product: "JSON Size Profile",
|
|
interval: this.bytesPerSample,
|
|
markerSchema: [],
|
|
symbolicationNotSupported: true,
|
|
usesOnlyOneStackType: true,
|
|
categories: this.categories.map(cat => ({
|
|
name: cat.name,
|
|
color: cat.color,
|
|
subcategories: ["Other"],
|
|
})),
|
|
sampleUnits: {
|
|
time: "bytes",
|
|
eventDelay: "ms",
|
|
threadCPUDelta: "µs",
|
|
},
|
|
};
|
|
|
|
if (this.filename) {
|
|
meta.fileName = this.filename;
|
|
}
|
|
|
|
const profile = {
|
|
meta,
|
|
libs: [],
|
|
threads: [
|
|
{
|
|
processType: "default",
|
|
processStartupTime: 0,
|
|
processShutdownTime: null,
|
|
registerTime: 0,
|
|
unregisterTime: null,
|
|
pausedRanges: [],
|
|
name: "Bytes",
|
|
isMainThread: true,
|
|
pid: "0",
|
|
tid: "0",
|
|
samples: {
|
|
length: sampleCount,
|
|
stack: this.samples.stack,
|
|
timeDeltas: this.samples.time,
|
|
weight: this.samples.weight,
|
|
weightType: "bytes",
|
|
threadCPUDelta: this.samples.cpuDelta,
|
|
},
|
|
markers: {
|
|
length: 0,
|
|
category: [],
|
|
data: [],
|
|
endTime: [],
|
|
name: [],
|
|
phase: [],
|
|
startTime: [],
|
|
},
|
|
stackTable: {
|
|
length: this.stackTable.frame.length,
|
|
prefix: this.stackTable.prefix,
|
|
frame: this.stackTable.frame,
|
|
},
|
|
frameTable: {
|
|
length: frameCount,
|
|
address: new Array(frameCount).fill(-1),
|
|
category: this.frameTable.category,
|
|
subcategory: new Array(frameCount).fill(0),
|
|
func: this.frameTable.func,
|
|
nativeSymbol: new Array(frameCount).fill(null),
|
|
innerWindowID: new Array(frameCount).fill(0),
|
|
line: new Array(frameCount).fill(null),
|
|
column: new Array(frameCount).fill(null),
|
|
inlineDepth: new Array(frameCount).fill(0),
|
|
},
|
|
funcTable: {
|
|
length: funcCount,
|
|
name: Array.from({ length: funcCount }, (_, i) => i),
|
|
isJS: new Array(funcCount).fill(false),
|
|
relevantForJS: new Array(funcCount).fill(false),
|
|
resource: new Array(funcCount).fill(-1),
|
|
fileName: new Array(funcCount).fill(null),
|
|
lineNumber: new Array(funcCount).fill(null),
|
|
columnNumber: new Array(funcCount).fill(null),
|
|
},
|
|
resourceTable: {
|
|
length: 0,
|
|
lib: [],
|
|
name: [],
|
|
host: [],
|
|
type: [],
|
|
},
|
|
nativeSymbols: {
|
|
length: 0,
|
|
address: [],
|
|
functionSize: [],
|
|
libIndex: [],
|
|
name: [],
|
|
},
|
|
},
|
|
],
|
|
profilingLog: [],
|
|
shared: {
|
|
stringArray: this.stringTableArray,
|
|
},
|
|
};
|
|
|
|
return profile;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a Firefox profiler profile from a JSON string.
|
|
*
|
|
* @param {string} jsonString - The JSON string to profile
|
|
* @param {string} filename - Optional filename to include in the profile
|
|
* @returns {object} A Firefox profiler profile object
|
|
*/
|
|
export function createSizeProfile(jsonString, filename) {
|
|
const profiler = new JsonSizeProfiler(jsonString, filename);
|
|
return profiler.parse();
|
|
}
|