tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

json-size-profiler.mjs (23390B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /**
      6 * Parses a JSON string and creates a Firefox profiler profile describing
      7 * which parts of the JSON use up how many bytes.
      8 */
      9 
     10 // Categories for different JSON types
     11 const JSON_CATEGORIES = {
     12  OBJECT: { name: "Object", color: "grey" },
     13  ARRAY: { name: "Array", color: "grey" },
     14  NULL: { name: "Null", color: "yellow" },
     15  BOOL: { name: "Bool", color: "brown" },
     16  NUMBER: { name: "Number", color: "green" },
     17  STRING: { name: "String", color: "blue" },
     18  PROPERTY_KEY: { name: "Property Key", color: "lightblue" },
     19 };
     20 
     21 const MAX_SAMPLE_COUNT = 100000;
     22 
     23 class JsonSizeProfiler {
     24  /**
     25   * @param {string} jsonString - The JSON string to profile.
     26   * @param {string} [filename] - Optional filename for the profile metadata.
     27   */
     28  constructor(jsonString, filename) {
     29    this.jsonString = jsonString;
     30    this.filename = filename;
     31    this.pos = 0;
     32    this.bytePos = 0;
     33    this.lastAdvancedBytePos = 0;
     34    this.scopeStack = [];
     35 
     36    // Caching structures
     37    this.stringTable = new Map();
     38    this.stringTableArray = [];
     39    this.stackCache = new Map();
     40    this.frameCache = new Map();
     41    this.nodeCache = new Map();
     42 
     43    // Profile tables - stored in final array format
     44    this.frameTable = {
     45      func: [],
     46      category: [],
     47    };
     48    this.stackTable = {
     49      frame: [],
     50      prefix: [],
     51    };
     52 
     53    // Aggregation state
     54    this.topStackHandle = null;
     55    const totalBytes = new TextEncoder().encode(jsonString).length;
     56    this.bytesPerSample = Math.max(
     57      1,
     58      Math.min(1000000, Math.floor(totalBytes / MAX_SAMPLE_COUNT))
     59    );
     60    this.sampleCount = 0;
     61    this.aggregationMap = new Map();
     62    this.aggregationStartPos = 0;
     63    this.samples = {
     64      stack: [],
     65      time: [],
     66      cpuDelta: [],
     67      weight: [],
     68    };
     69 
     70    // Categories initialization
     71    this.categories = [];
     72    this.categoryMap = new Map();
     73    for (const [key, value] of Object.entries(JSON_CATEGORIES)) {
     74      const catIndex = this.categories.length;
     75      this.categories.push(value);
     76      this.categoryMap.set(key, catIndex);
     77    }
     78  }
     79 
     80  /**
     81   * Interns a string into the string table, returning its index.
     82   *
     83   * @param {string} str - The string to intern.
     84   * @returns {number} The index of the string in the string table.
     85   */
     86  internString(str) {
     87    if (!this.stringTable.has(str)) {
     88      const index = this.stringTableArray.length;
     89      this.stringTableArray.push(str);
     90      this.stringTable.set(str, index);
     91    }
     92    return this.stringTable.get(str);
     93  }
     94 
     95  /**
     96   * Gets or creates a frame in the frame table.
     97   *
     98   * @param {string} funcName - The function name for the frame.
     99   * @param {number} category - The category index for the frame.
    100   * @returns {number} The frame index.
    101   */
    102  getOrCreateFrame(funcName, category) {
    103    const funcIndex = this.internString(funcName);
    104    const cacheKey = `${funcIndex}:${category}`;
    105    if (this.frameCache.has(cacheKey)) {
    106      return this.frameCache.get(cacheKey);
    107    }
    108 
    109    const frameIndex = this.frameTable.func.length;
    110    this.frameTable.func.push(funcIndex);
    111    this.frameTable.category.push(category);
    112    this.frameCache.set(cacheKey, frameIndex);
    113    return frameIndex;
    114  }
    115 
    116  /**
    117   * Gets or creates a stack in the stack table.
    118   *
    119   * @param {number} frameIndex - The frame index for this stack entry.
    120   * @param {number|null} prefix - The parent stack index, or null for root.
    121   * @returns {number} The stack index.
    122   */
    123  getOrCreateStack(frameIndex, prefix) {
    124    const cacheKey = `${frameIndex}:${prefix === null ? "null" : prefix}`;
    125    if (this.stackCache.has(cacheKey)) {
    126      return this.stackCache.get(cacheKey);
    127    }
    128 
    129    const stackIndex = this.stackTable.frame.length;
    130    this.stackTable.frame.push(frameIndex);
    131    this.stackTable.prefix.push(prefix);
    132    this.stackCache.set(cacheKey, stackIndex);
    133    return stackIndex;
    134  }
    135 
    136  /**
    137   * Gets a stack handle for a given path and JSON type.
    138   *
    139   * @param {number|null} parentStackHandle - The parent stack handle.
    140   * @param {string} path - The path in the JSON structure.
    141   * @param {string} jsonType - The JSON type (OBJECT, ARRAY, STRING, etc.).
    142   * @returns {number} The stack handle.
    143   */
    144  getStack(parentStackHandle, path, jsonType) {
    145    const cacheKey = `${parentStackHandle === null ? "null" : parentStackHandle}:${path}:${jsonType}`;
    146    if (this.nodeCache.has(cacheKey)) {
    147      return this.nodeCache.get(cacheKey);
    148    }
    149 
    150    const category = this.categoryMap.get(jsonType);
    151    const frameIndex = this.getOrCreateFrame(path, category);
    152    const stackHandle = this.getOrCreateStack(frameIndex, parentStackHandle);
    153    this.nodeCache.set(cacheKey, stackHandle);
    154    return stackHandle;
    155  }
    156 
    157  /**
    158   * Moves position forward, updating both character and byte positions.
    159   *
    160   * This function tracks both character positions (in the UTF-16 string) and
    161   * byte positions (in the original UTF-8 file) because:
    162   * 1. The original JSON file contained bytes which formed a UTF-8 string.
    163   * 2. When the JSON viewer loaded the JSON, these bytes were parsed into a
    164   *    UTF-16 string (or "potentially ill-formed UTF-16" aka WTF-16), which
    165   *    means all characters now take 2 bytes in memory even though most
    166   *    originally only took 1 byte in the UTF-8 file.
    167   * 3. this.jsonString.charCodeAt() indexes into the UTF-16 string.
    168   * 4. By examining the UTF-16 code unit value, we "recover" how many bytes
    169   *    this character originally occupied in the UTF-8 file.
    170   *
    171   * Note: this.pos will never stop in the middle of a surrogate pair.
    172   * When this function returns, this.pos >= newCharPos.
    173   *
    174   * @param {number} newCharPos - The new character position to move to.
    175   */
    176  advanceToPos(newCharPos) {
    177    while (this.pos < newCharPos) {
    178      const code = this.jsonString.charCodeAt(this.pos);
    179      if (code >= 0xd800 && code <= 0xdbff) {
    180        // Surrogate pair - always 4 bytes in UTF-8
    181        this.bytePos += 4;
    182        this.pos += 2;
    183      } else {
    184        // Single UTF-16 code unit - calculate UTF-8 byte length
    185        if (code <= 0x7f) {
    186          this.bytePos += 1;
    187        } else if (code <= 0x7ff) {
    188          this.bytePos += 2;
    189        } else {
    190          // 3-byte UTF-8 characters (U+0800 to U+FFFF)
    191          // Examples: CJK characters like "中", symbols like "€", etc.
    192          this.bytePos += 3;
    193        }
    194        this.pos++;
    195      }
    196    }
    197  }
    198 
    199  /**
    200   * Moves position forward by a number of ASCII characters (1 byte each).
    201   *
    202   * @param {number} count - The number of ASCII characters to advance.
    203   */
    204  advanceByAsciiChars(count) {
    205    this.pos += count;
    206    this.bytePos += count;
    207  }
    208 
    209  /**
    210   * Parses a primitive value with stack tracking.
    211   *
    212   * @param {string} path - The path to the value in the JSON structure.
    213   * @param {string} typeName - The type name (STRING, NUMBER, BOOL, NULL).
    214   * @param {Function} parseFunc - The function to call to parse the value.
    215   */
    216  parsePrimitive(path, typeName, parseFunc) {
    217    this.recordBytesConsumed();
    218 
    219    const scope = this.getCurrentScope();
    220    const stackHandle = this.getStack(
    221      scope.stackHandle,
    222      `${path} (${typeName.toLowerCase()})`,
    223      typeName
    224    );
    225    this.topStackHandle = stackHandle;
    226 
    227    parseFunc();
    228 
    229    this.recordBytesConsumed();
    230    this.topStackHandle = scope.stackHandle;
    231  }
    232 
    233  /**
    234   * Exits the current scope (object or array).
    235   */
    236  exitScope() {
    237    this.recordBytesConsumed();
    238    this.scopeStack.pop();
    239    const prevScope = this.getCurrentScope();
    240    this.topStackHandle = prevScope.stackHandle;
    241  }
    242 
    243  /**
    244   * Records bytes consumed since the last call.
    245   *
    246   * This method accumulates byte counts in aggregationMap instead of immediately
    247   * creating profile samples. This aggregation limits the total sample count to
    248   * approximately MAX_SAMPLE_COUNT (100,000), which keeps the Firefox Profiler
    249   * UI responsive even for very large JSON files.
    250   *
    251   * For small files (< 100KB), bytesPerSample = 1, so samples are created
    252   * frequently. For large files, bytesPerSample scales proportionally
    253   * (e.g., 100 for a 10MB file), so samples are batched more aggressively.
    254   *
    255   * Samples are flushed when we have accumulated multiple stacks and have
    256   * consumed enough bytes to justify creating new samples.
    257   */
    258  recordBytesConsumed() {
    259    if (this.bytePos === 0 && this.lastAdvancedBytePos === 0) {
    260      return;
    261    }
    262    if (this.bytePos < this.lastAdvancedBytePos) {
    263      throw new Error(
    264        `Cannot advance backwards: ${this.lastAdvancedBytePos} -> ${this.bytePos}`
    265      );
    266    }
    267    if (this.bytePos === this.lastAdvancedBytePos) {
    268      return;
    269    }
    270 
    271    const byteDelta = this.bytePos - this.lastAdvancedBytePos;
    272    const stackHandle = this.topStackHandle;
    273    if (stackHandle !== null) {
    274      const current = this.aggregationMap.get(stackHandle) || 0;
    275      this.aggregationMap.set(stackHandle, current + byteDelta);
    276    }
    277 
    278    this.lastAdvancedBytePos = this.bytePos;
    279 
    280    // Flush accumulated samples when we have multiple stacks and enough bytes
    281    const aggregatedStackCount = this.aggregationMap.size;
    282    if (aggregatedStackCount > 1) {
    283      const sampleCountIfWeFlush = this.sampleCount + aggregatedStackCount;
    284      const allowedSampleCount = Math.floor(
    285        this.lastAdvancedBytePos / this.bytesPerSample
    286      );
    287      if (sampleCountIfWeFlush <= allowedSampleCount) {
    288        this.recordSamples();
    289      }
    290    }
    291  }
    292 
    293  /**
    294   * Flushes accumulated byte counts to the samples table.
    295   */
    296  recordSamples() {
    297    let synthLastPos = this.aggregationStartPos;
    298 
    299    for (const [stackHandle, accDelta] of this.aggregationMap.entries()) {
    300      const synthPos = synthLastPos + accDelta;
    301 
    302      // First sample at start position
    303      this.samples.stack.push(stackHandle);
    304      this.samples.time.push(synthLastPos);
    305      this.samples.cpuDelta.push(0);
    306      this.samples.weight.push(0);
    307 
    308      // Second sample at end position with size
    309      this.samples.stack.push(stackHandle);
    310      this.samples.time.push(synthPos);
    311      this.samples.cpuDelta.push(accDelta * 1000);
    312      this.samples.weight.push(accDelta);
    313 
    314      synthLastPos = synthPos;
    315      this.sampleCount += 1;
    316    }
    317 
    318    this.aggregationStartPos = this.lastAdvancedBytePos;
    319    this.aggregationMap.clear();
    320  }
    321 
    322  /**
    323   * Gets the current scope from the scope stack.
    324   *
    325   * @returns {object} An object with stackHandle, path, and arrayDepth.
    326   */
    327  getCurrentScope() {
    328    if (this.scopeStack.length === 0) {
    329      return {
    330        stackHandle: null,
    331        path: "json",
    332        arrayDepth: 0,
    333      };
    334    }
    335 
    336    const scope = this.scopeStack[this.scopeStack.length - 1];
    337    return {
    338      stackHandle: scope.stackHandle,
    339      path: scope.pathForValue || scope.pathForElems || scope.path,
    340      arrayDepth: scope.arrayDepth,
    341    };
    342  }
    343 
    344  /**
    345   * Skips whitespace characters in the JSON string.
    346   */
    347  skipWhitespace() {
    348    while (this.pos < this.jsonString.length) {
    349      const ch = this.jsonString[this.pos];
    350      if (ch !== " " && ch !== "\t" && ch !== "\n" && ch !== "\r") {
    351        break;
    352      }
    353      this.advanceByAsciiChars(1); // Whitespace is always ASCII (1 byte each)
    354    }
    355  }
    356 
    357  /**
    358   * Parses a JSON value at the current position.
    359   *
    360   * @param {string} path - The path to this value in the JSON structure.
    361   */
    362  parseValue(path) {
    363    this.skipWhitespace();
    364 
    365    if (this.pos >= this.jsonString.length) {
    366      throw new Error("Unexpected end of JSON");
    367    }
    368 
    369    const ch = this.jsonString[this.pos];
    370 
    371    if (ch === "{") {
    372      this.parseObject(path);
    373    } else if (ch === "[") {
    374      this.parseArray(path);
    375    } else if (ch === '"') {
    376      this.parseString(path);
    377    } else if (ch === "t" || ch === "f") {
    378      this.parseBool(path);
    379    } else if (ch === "n") {
    380      this.parseNull(path);
    381    } else {
    382      this.parseNumber(path);
    383    }
    384  }
    385 
    386  /**
    387   * Parses a JSON object at the current position.
    388   *
    389   * @param {string} path - The path to this object in the JSON structure.
    390   */
    391  parseObject(path) {
    392    this.recordBytesConsumed();
    393 
    394    const parentScope = this.getCurrentScope();
    395    const stackHandle = this.getStack(
    396      parentScope.stackHandle,
    397      `${path} (object)`,
    398      "OBJECT"
    399    );
    400 
    401    this.scopeStack.push({
    402      type: "object",
    403      stackHandle,
    404      path,
    405      pathForValue: null,
    406      arrayDepth: parentScope.arrayDepth,
    407    });
    408    this.topStackHandle = stackHandle;
    409 
    410    this.advanceByAsciiChars(1); // skip '{'
    411 
    412    let first = true;
    413    while (this.pos < this.jsonString.length) {
    414      this.skipWhitespace();
    415 
    416      if (this.jsonString[this.pos] === "}") {
    417        this.advanceByAsciiChars(1); // skip '}'
    418        break;
    419      }
    420 
    421      if (!first) {
    422        if (this.jsonString[this.pos] !== ",") {
    423          throw new Error(`Expected ',' at position ${this.pos}`);
    424        }
    425        this.advanceByAsciiChars(1); // skip ','
    426        this.skipWhitespace();
    427      }
    428      first = false;
    429 
    430      // Parse property key
    431      if (this.jsonString[this.pos] !== '"') {
    432        throw new Error(`Expected property key at position ${this.pos}`);
    433      }
    434 
    435      this.recordBytesConsumed();
    436 
    437      const key = this.parseStringValue();
    438      const propertyPath = `${path}.${key}`;
    439 
    440      const propKeyStack = this.getStack(
    441        stackHandle,
    442        `${propertyPath} (property key)`,
    443        "PROPERTY_KEY"
    444      );
    445      this.topStackHandle = propKeyStack;
    446 
    447      // Update scope with current property path
    448      this.scopeStack[this.scopeStack.length - 1].pathForValue = propertyPath;
    449 
    450      this.skipWhitespace();
    451      if (this.jsonString[this.pos] !== ":") {
    452        throw new Error(`Expected ':' at position ${this.pos}`);
    453      }
    454      this.advanceByAsciiChars(1); // skip ':'
    455 
    456      // Parse property value
    457      this.parseValue(propertyPath);
    458    }
    459 
    460    this.exitScope();
    461  }
    462 
    463  /**
    464   * Parses a JSON array at the current position.
    465   *
    466   * @param {string} path - The path to this array in the JSON structure.
    467   */
    468  parseArray(path) {
    469    this.recordBytesConsumed();
    470 
    471    const parentScope = this.getCurrentScope();
    472 
    473    const INDEXER_CHARS = "ijklmnopqrstuvwxyz";
    474    const indexer =
    475      INDEXER_CHARS[parentScope.arrayDepth % INDEXER_CHARS.length];
    476    const pathForElems = `${path}[${indexer}]`;
    477 
    478    const stackHandle = this.getStack(
    479      parentScope.stackHandle,
    480      `${path} (array)`,
    481      "ARRAY"
    482    );
    483 
    484    this.topStackHandle = stackHandle;
    485    this.scopeStack.push({
    486      type: "array",
    487      stackHandle,
    488      pathForElems,
    489      arrayDepth: parentScope.arrayDepth + 1,
    490    });
    491 
    492    this.advanceByAsciiChars(1); // skip '['
    493 
    494    let first = true;
    495    while (this.pos < this.jsonString.length) {
    496      this.skipWhitespace();
    497 
    498      if (this.jsonString[this.pos] === "]") {
    499        this.advanceByAsciiChars(1); // skip ']'
    500        break;
    501      }
    502 
    503      if (!first) {
    504        if (this.jsonString[this.pos] !== ",") {
    505          throw new Error(`Expected ',' at position ${this.pos}`);
    506        }
    507        this.advanceByAsciiChars(1); // skip ','
    508        this.skipWhitespace();
    509      }
    510      first = false;
    511 
    512      this.parseValue(pathForElems);
    513    }
    514 
    515    this.exitScope();
    516  }
    517 
    518  /**
    519   * Parses a JSON string at the current position.
    520   *
    521   * @param {string} path - The path to this string in the JSON structure.
    522   */
    523  parseString(path) {
    524    this.parsePrimitive(path, "STRING", () => this.parseStringValue());
    525  }
    526 
    527  /**
    528   * Parses a JSON string value and returns it.
    529   *
    530   * @returns {string} The parsed string value.
    531   */
    532  parseStringValue() {
    533    this.advanceByAsciiChars(1); // skip opening quote (ASCII)
    534    let value = "";
    535 
    536    while (this.pos < this.jsonString.length) {
    537      const ch = this.jsonString[this.pos];
    538 
    539      if (ch === '"') {
    540        this.advanceByAsciiChars(1); // closing quote (ASCII)
    541        break;
    542      } else if (ch === "\\") {
    543        this.advanceByAsciiChars(1); // backslash (ASCII)
    544        if (this.pos >= this.jsonString.length) {
    545          throw new Error("Unexpected end of JSON in string");
    546        }
    547        const escaped = this.jsonString[this.pos];
    548        if (escaped === '"' || escaped === "\\" || escaped === "/") {
    549          value += escaped;
    550        } else if (escaped === "b") {
    551          value += "\b";
    552        } else if (escaped === "f") {
    553          value += "\f";
    554        } else if (escaped === "n") {
    555          value += "\n";
    556        } else if (escaped === "r") {
    557          value += "\r";
    558        } else if (escaped === "t") {
    559          value += "\t";
    560        } else if (escaped === "u") {
    561          // Unicode escape - \uXXXX (all ASCII)
    562          this.advanceByAsciiChars(1);
    563          const hex = this.jsonString.slice(this.pos, this.pos + 4);
    564          value += String.fromCharCode(parseInt(hex, 16));
    565          this.advanceByAsciiChars(3); // skip the 4 hex digits (already moved 1)
    566        }
    567        this.advanceByAsciiChars(1); // escaped char (ASCII)
    568      } else {
    569        // Regular character - may be multi-byte UTF-8
    570        value += ch;
    571        this.advanceToPos(this.pos + 1);
    572      }
    573    }
    574 
    575    return value;
    576  }
    577 
    578  /**
    579   * Parses a JSON number at the current position.
    580   *
    581   * @param {string} path - The path to this number in the JSON structure.
    582   */
    583  parseNumber(path) {
    584    this.parsePrimitive(path, "NUMBER", () => {
    585      // Skip all number characters: digits, decimal point, exponent, signs
    586      while (this.pos < this.jsonString.length) {
    587        const ch = this.jsonString[this.pos];
    588        if (
    589          (ch >= "0" && ch <= "9") ||
    590          ch === "." ||
    591          ch === "e" ||
    592          ch === "E" ||
    593          ch === "+" ||
    594          ch === "-"
    595        ) {
    596          this.advanceByAsciiChars(1);
    597        } else {
    598          break;
    599        }
    600      }
    601    });
    602  }
    603 
    604  /**
    605   * Parses a JSON boolean at the current position.
    606   *
    607   * @param {string} path - The path to this boolean in the JSON structure.
    608   */
    609  parseBool(path) {
    610    this.parsePrimitive(path, "BOOL", () => {
    611      if (this.jsonString.slice(this.pos, this.pos + 4) === "true") {
    612        this.advanceByAsciiChars(4);
    613      } else if (this.jsonString.slice(this.pos, this.pos + 5) === "false") {
    614        this.advanceByAsciiChars(5);
    615      } else {
    616        throw new Error(`Expected boolean at position ${this.pos}`);
    617      }
    618    });
    619  }
    620 
    621  /**
    622   * Parses a JSON null at the current position.
    623   *
    624   * @param {string} path - The path to this null in the JSON structure.
    625   */
    626  parseNull(path) {
    627    this.parsePrimitive(path, "NULL", () => {
    628      if (this.jsonString.slice(this.pos, this.pos + 4) === "null") {
    629        this.advanceByAsciiChars(4);
    630      } else {
    631        throw new Error(`Expected null at position ${this.pos}`);
    632      }
    633    });
    634  }
    635 
    636  /**
    637   * Parses the JSON string and generates a Firefox profiler profile.
    638   *
    639   * @returns {object} A Firefox profiler profile object.
    640   */
    641  parse() {
    642    this.parseValue("json");
    643 
    644    // Move to end of string to account for any trailing content
    645    const remaining = this.jsonString.length - this.pos;
    646    if (remaining > 0) {
    647      this.advanceByAsciiChars(remaining);
    648    }
    649 
    650    // Advance to final position
    651    if (this.bytePos !== this.lastAdvancedBytePos) {
    652      this.recordBytesConsumed();
    653    }
    654 
    655    this.recordSamples();
    656 
    657    const frameCount = this.frameTable.func.length;
    658    const funcCount = this.stringTableArray.length;
    659    const sampleCount = this.samples.stack.length;
    660    // Convert absolute times to deltas in place
    661    for (let i = sampleCount - 1; i > 0; i--) {
    662      this.samples.time[i] = this.samples.time[i] - this.samples.time[i - 1];
    663    }
    664    // First element stays as-is (it's already a delta from 0)
    665 
    666    const meta = {
    667      version: 56,
    668      preprocessedProfileVersion: 56,
    669      startTime: 0,
    670      fileSize: this.lastAdvancedBytePos,
    671      processType: 0,
    672      product: "JSON Size Profile",
    673      interval: this.bytesPerSample,
    674      markerSchema: [],
    675      symbolicationNotSupported: true,
    676      usesOnlyOneStackType: true,
    677      categories: this.categories.map(cat => ({
    678        name: cat.name,
    679        color: cat.color,
    680        subcategories: ["Other"],
    681      })),
    682      sampleUnits: {
    683        time: "bytes",
    684        eventDelay: "ms",
    685        threadCPUDelta: "µs",
    686      },
    687    };
    688 
    689    if (this.filename) {
    690      meta.fileName = this.filename;
    691    }
    692 
    693    const profile = {
    694      meta,
    695      libs: [],
    696      threads: [
    697        {
    698          processType: "default",
    699          processStartupTime: 0,
    700          processShutdownTime: null,
    701          registerTime: 0,
    702          unregisterTime: null,
    703          pausedRanges: [],
    704          name: "Bytes",
    705          isMainThread: true,
    706          pid: "0",
    707          tid: "0",
    708          samples: {
    709            length: sampleCount,
    710            stack: this.samples.stack,
    711            timeDeltas: this.samples.time,
    712            weight: this.samples.weight,
    713            weightType: "bytes",
    714            threadCPUDelta: this.samples.cpuDelta,
    715          },
    716          markers: {
    717            length: 0,
    718            category: [],
    719            data: [],
    720            endTime: [],
    721            name: [],
    722            phase: [],
    723            startTime: [],
    724          },
    725          stackTable: {
    726            length: this.stackTable.frame.length,
    727            prefix: this.stackTable.prefix,
    728            frame: this.stackTable.frame,
    729          },
    730          frameTable: {
    731            length: frameCount,
    732            address: new Array(frameCount).fill(-1),
    733            category: this.frameTable.category,
    734            subcategory: new Array(frameCount).fill(0),
    735            func: this.frameTable.func,
    736            nativeSymbol: new Array(frameCount).fill(null),
    737            innerWindowID: new Array(frameCount).fill(0),
    738            line: new Array(frameCount).fill(null),
    739            column: new Array(frameCount).fill(null),
    740            inlineDepth: new Array(frameCount).fill(0),
    741          },
    742          funcTable: {
    743            length: funcCount,
    744            name: Array.from({ length: funcCount }, (_, i) => i),
    745            isJS: new Array(funcCount).fill(false),
    746            relevantForJS: new Array(funcCount).fill(false),
    747            resource: new Array(funcCount).fill(-1),
    748            fileName: new Array(funcCount).fill(null),
    749            lineNumber: new Array(funcCount).fill(null),
    750            columnNumber: new Array(funcCount).fill(null),
    751          },
    752          resourceTable: {
    753            length: 0,
    754            lib: [],
    755            name: [],
    756            host: [],
    757            type: [],
    758          },
    759          nativeSymbols: {
    760            length: 0,
    761            address: [],
    762            functionSize: [],
    763            libIndex: [],
    764            name: [],
    765          },
    766        },
    767      ],
    768      profilingLog: [],
    769      shared: {
    770        stringArray: this.stringTableArray,
    771      },
    772    };
    773 
    774    return profile;
    775  }
    776 }
    777 
    778 /**
    779 * Creates a Firefox profiler profile from a JSON string.
    780 *
    781 * @param {string} jsonString - The JSON string to profile
    782 * @param {string} filename - Optional filename to include in the profile
    783 * @returns {object} A Firefox profiler profile object
    784 */
    785 export function createSizeProfile(jsonString, filename) {
    786  const profiler = new JsonSizeProfiler(jsonString, filename);
    787  return profiler.parse();
    788 }