From 367a93062b9e10ea5e1bb15fa29473617be9a15c Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sat, 16 May 2026 00:24:17 -0400 Subject: [PATCH 1/6] Use `Int32Array` for the derived stack table's `frame` column. This doesn't change anything about the profile format, this is just about the derived in-memory representation. --- src/profile-logic/profile-data.ts | 7 +++++-- src/test/store/__snapshots__/profile-view.test.ts.snap | 10 +++++----- src/types/profile-derived.ts | 7 +++---- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/profile-logic/profile-data.ts b/src/profile-logic/profile-data.ts index 787fdc7fda..10100ef72e 100644 --- a/src/profile-logic/profile-data.ts +++ b/src/profile-logic/profile-data.ts @@ -1688,7 +1688,7 @@ export function createStackTableBySkippingDiscarded( keepStack: BitSet ): StackTable { const newStackCount = newPrefixCol.length; - const newFrameCol = new Array(newStackCount); + const newFrameCol = new Int32Array(newStackCount); const newCategoryCol = new Uint8Array(newStackCount); const newSubcategoryCol = stackTable.subcategory instanceof Uint16Array @@ -4764,8 +4764,11 @@ export function computeStackTableFromRawStackTable( subcategoryColumn[stackIndex] = stackSubcategory; } + // The frame column is a typed array in the derived stack table. + const frame = new Int32Array(rawStackTable.frame); + return { - frame: rawStackTable.frame, + frame, category: categoryColumn, subcategory: subcategoryColumn, prefix: rawStackTable.prefix, diff --git a/src/test/store/__snapshots__/profile-view.test.ts.snap b/src/test/store/__snapshots__/profile-view.test.ts.snap index c342a59f5a..738ec4d3d7 100644 --- a/src/test/store/__snapshots__/profile-view.test.ts.snap +++ b/src/test/store/__snapshots__/profile-view.test.ts.snap @@ -2721,7 +2721,7 @@ CallTree { 0, 0, ], - "frame": Array [ + "frame": Int32Array [ 0, 1, 2, @@ -3109,7 +3109,7 @@ Object { 0, 0, ], - "frame": Array [ + "frame": Int32Array [ 0, 1, 2, @@ -3563,7 +3563,7 @@ Object { 0, 0, ], - "frame": Array [ + "frame": Int32Array [ 0, 1, 2, @@ -3949,7 +3949,7 @@ Object { 0, 0, ], - "frame": Array [ + "frame": Int32Array [ 0, 1, 2, @@ -4335,7 +4335,7 @@ Object { 0, 0, ], - "frame": Array [ + "frame": Int32Array [ 0, 1, 2, diff --git a/src/types/profile-derived.ts b/src/types/profile-derived.ts index 75325be75c..89e7cfab68 100644 --- a/src/types/profile-derived.ts +++ b/src/types/profile-derived.ts @@ -29,7 +29,6 @@ import type { JsTracerTable, IndexIntoStackTable, WeightType, - IndexIntoFrameTable, SourceTable, IndexIntoSourceTable, CounterDisplayConfig, @@ -200,7 +199,8 @@ export type Counter = { * The `StackTable` type of the derived thread. * * The only difference from the `RawStackTable` is that the `StackTable` has a - * `category` and a `subcategory` column. + * `category` and a `subcategory` column, and that the `StackTable` always uses + * a typed array for the `frame` column. * * The category of a stack node is always non-null and is derived from a stack's * frame and its prefix. Frames can have null categories, stacks cannot. If a @@ -225,8 +225,7 @@ export type Counter = { * nsAttrAndChildArray::InsertChildAt stack before transforms are applied. */ export type StackTable = { - // Same as in RawStackTable - frame: IndexIntoFrameTable[]; + frame: Int32Array; prefix: Array; length: number; From 09909dac7c8fa895b687535cf9d9de2944dab1a7 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sat, 16 May 2026 00:18:52 -0400 Subject: [PATCH 2/6] Add a JSON utility that serializes typed arrays as regular arrays. `JSON.stringify` serializes typed arrays as objects with stringified numeric keys (e.g. `{"0": 1, "1": 2}`), which is not what we want when a profile contains typed arrays. `jsonEncodeObjectWithTypedArraysAsRegularArrays` traverses the object and converts any typed array it finds to a regular array of numbers before it calls `JSON.stringify`. The new function is not used yet; the next patch in this series will switch profile serialization to use it. --- src/test/unit/json-with-typed-arrays.test.ts | 129 +++++++++++++++++++ src/utils/json-with-typed-arrays.ts | 81 ++++++++++++ 2 files changed, 210 insertions(+) create mode 100644 src/test/unit/json-with-typed-arrays.test.ts create mode 100644 src/utils/json-with-typed-arrays.ts diff --git a/src/test/unit/json-with-typed-arrays.test.ts b/src/test/unit/json-with-typed-arrays.test.ts new file mode 100644 index 0000000000..2ba9a2f070 --- /dev/null +++ b/src/test/unit/json-with-typed-arrays.test.ts @@ -0,0 +1,129 @@ +/* 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/. */ + +import { jsonEncodeObjectWithTypedArraysAsRegularArrays as encode } from '../../utils/json-with-typed-arrays'; + +describe('jsonEncodeObjectWithTypedArraysAsRegularArrays', function () { + it('encodes primitives the same as JSON.stringify', function () { + expect(encode(42)).toBe('42'); + expect(encode('hello')).toBe('"hello"'); + expect(encode(null)).toBe('null'); + expect(encode(true)).toBe('true'); + }); + + it('encodes a plain object the same as JSON.stringify', function () { + const obj = { a: 1, b: 'two', c: [1, 2, 3], d: { nested: true } }; + expect(encode(obj)).toBe(JSON.stringify(obj)); + }); + + it('encodes a top-level typed array as a regular array of numbers', function () { + const arr = new Int32Array([1, 2, 3, 4]); + expect(encode(arr)).toBe('[1,2,3,4]'); + }); + + it('encodes an empty typed array as an empty array', function () { + expect(encode(new Uint8Array())).toBe('[]'); + }); + + it('handles all typed array variants', function () { + expect(encode(new Int8Array([1, -2]))).toBe('[1,-2]'); + expect(encode(new Uint8Array([1, 2]))).toBe('[1,2]'); + expect(encode(new Uint8ClampedArray([1, 2]))).toBe('[1,2]'); + expect(encode(new Int16Array([1, -2]))).toBe('[1,-2]'); + expect(encode(new Uint16Array([1, 2]))).toBe('[1,2]'); + expect(encode(new Int32Array([1, -2]))).toBe('[1,-2]'); + expect(encode(new Uint32Array([1, 2]))).toBe('[1,2]'); + expect(encode(new Float32Array([1.5]))).toBe('[1.5]'); + expect(encode(new Float64Array([1.5]))).toBe('[1.5]'); + }); + + it('encodes typed arrays nested inside an object', function () { + const obj = { + name: 'data', + values: new Int32Array([10, 20, 30]), + }; + expect(encode(obj)).toBe('{"name":"data","values":[10,20,30]}'); + }); + + it('encodes typed arrays nested inside an array', function () { + const arr = [1, new Uint8Array([2, 3]), 4]; + expect(encode(arr)).toBe('[1,[2,3],4]'); + }); + + it('encodes deeply nested typed arrays', function () { + const obj = { + a: { + b: { + c: [ + { d: new Float32Array([1.5, 2.5]) }, + { e: new Int16Array([7, 8]) }, + ], + }, + }, + }; + expect(encode(obj)).toBe('{"a":{"b":{"c":[{"d":[1.5,2.5]},{"e":[7,8]}]}}}'); + }); + + it('does not mutate the original object', function () { + const typedArr = new Int32Array([1, 2, 3]); + const inner = { values: typedArr, label: 'x' }; + const obj = { inner, count: 2 }; + const originalKeys = Object.keys(obj); + const innerKeys = Object.keys(inner); + + encode(obj); + + expect(obj.inner).toBe(inner); + expect(inner.values).toBe(typedArr); + expect(inner.label).toBe('x'); + expect(obj.count).toBe(2); + expect(Object.keys(obj)).toEqual(originalKeys); + expect(Object.keys(inner)).toEqual(innerKeys); + }); + + it('does not mutate the original array', function () { + const typedArr = new Int32Array([1, 2, 3]); + const arr = [1, typedArr, 3]; + + encode(arr); + + expect(arr[1]).toBe(typedArr); + expect(arr).toEqual([1, typedArr, 3]); + }); + + it('leaves DataView alone (encoded as a regular object)', function () { + // DataView is excluded from typed-array handling and falls through + // to the regular object path. JSON.stringify on a DataView produces "{}". + const buffer = new ArrayBuffer(8); + const view = new DataView(buffer); + expect(encode(view)).toBe(JSON.stringify(view)); + }); + + it('handles null values inside objects and arrays', function () { + const obj = { a: null, b: [null, 1, null], c: new Int32Array([5]) }; + expect(encode(obj)).toBe('{"a":null,"b":[null,1,null],"c":[5]}'); + }); + + it('handles an empty object and an empty array', function () { + expect(encode({})).toBe('{}'); + expect(encode([])).toBe('[]'); + }); + + it('preserves array element order with typed arrays interleaved', function () { + const arr = [ + new Uint8Array([1]), + 'middle', + new Uint8Array([2]), + 42, + new Uint8Array([3]), + ]; + expect(encode(arr)).toBe('[[1],"middle",[2],42,[3]]'); + }); + + it('encodes the same shared typed array twice when it appears in two places', function () { + const shared = new Int32Array([7, 8, 9]); + const obj = { first: shared, second: shared }; + expect(encode(obj)).toBe('{"first":[7,8,9],"second":[7,8,9]}'); + }); +}); diff --git a/src/utils/json-with-typed-arrays.ts b/src/utils/json-with-typed-arrays.ts new file mode 100644 index 0000000000..9ab283f35c --- /dev/null +++ b/src/utils/json-with-typed-arrays.ts @@ -0,0 +1,81 @@ +/* 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/. */ + +/** + * JSON.stringify an object which may contain typed arrays somewhere in + * its structure (nested arbitrarily deeply), with those typed arrays + * serialized as regular arrays of numbers. + * + * Calling JSON.stringify on a typed array would normally give you something + * like `{"0": 1, "1": 2}` which is not what you want. + * + * This function does not mutate rootObject. + */ +export function jsonEncodeObjectWithTypedArraysAsRegularArrays( + rootObject: unknown +): string { + // We could use JSON.stringify with a "replacer" here. + // But instead, we do a full traversal of the object first, possibly + // creating new objects (so that the original doesn't get mutated), + // and then a regular JSON.stringify with no replacer. This is 5x faster. + return JSON.stringify(rewriteTypedArrays(rootObject)); +} + +function rewriteTypedArrays(value: unknown): unknown { + if (value === null || typeof value !== 'object') { + return value; + } + if (ArrayBuffer.isView(value) && !(value instanceof DataView)) { + // Found a typed array! Use Array.from to convert it to a regular + // array of numbers. + return Array.from(value as unknown as Iterable); + } + if (Array.isArray(value)) { + return rewriteTypedArraysInArray(value); + } + return rewriteTypedArraysInObject(value as Record); +} + +function rewriteTypedArraysInArray(arr: readonly unknown[]): unknown[] { + let result: unknown[] | null = null; + for (let i = 0; i < arr.length; i++) { + const el = arr[i]; + // Inline fast-path for primitives: an array of 1000 numbers walks this + // loop without any function call. + if (el === null || typeof el !== 'object') { + continue; + } + const replaced = rewriteTypedArrays(el); + if (replaced !== el) { + if (result === null) { + result = arr.slice(); + } + result[i] = replaced; + } + } + return result ?? (arr as unknown[]); +} + +function rewriteTypedArraysInObject( + obj: Record +): Record { + let result: Record | null = null; + for (const key in obj) { + if (!Object.hasOwn(obj, key)) { + continue; + } + const el = obj[key]; + if (el === null || typeof el !== 'object') { + continue; + } + const replaced = rewriteTypedArrays(el); + if (replaced !== el) { + if (result === null) { + result = { ...obj }; + } + result[key] = replaced; + } + } + return result ?? obj; +} From 974517d2752411a4009407e8d552545637ec82b4 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sat, 16 May 2026 00:19:26 -0400 Subject: [PATCH 3/6] Use jsonEncodeObjectWithTypedArraysAsRegularArrays for profile JSON serialization. This will allow us to have typed arrays in the profile and still serialize them as regular JSON arrays whenever the profile is serialized to JSON. --- src/profile-logic/process-profile.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/profile-logic/process-profile.ts b/src/profile-logic/process-profile.ts index 712dda0c52..3781d305fa 100644 --- a/src/profile-logic/process-profile.ts +++ b/src/profile-logic/process-profile.ts @@ -107,6 +107,7 @@ import type { CounterDisplayConfig, } from 'firefox-profiler/types'; import { decompress, isGzip } from 'firefox-profiler/utils/gz'; +import { jsonEncodeObjectWithTypedArraysAsRegularArrays } from 'firefox-profiler/utils/json-with-typed-arrays'; type RegExpResult = null | string[]; /** @@ -2056,7 +2057,7 @@ export function processGeckoProfile(geckoProfile: GeckoProfile): Profile { * Take a processed profile and convert it to a string. */ export function serializeProfileToJsonString(profile: Profile): string { - return JSON.stringify(profile); + return jsonEncodeObjectWithTypedArraysAsRegularArrays(profile); } /** From 7dbb6533ba58e97792119c2c3178cc85aa84af1d Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sat, 16 May 2026 00:24:17 -0400 Subject: [PATCH 4/6] Update tests to deal with typed arrays in profiles. Some of our tests were auto-stringifying profiles (e.g. the ones using fetchMock), which will produce bad results once profiles start containing typed arrays. Use explicit serializeProfileToJsonString calls in those places. --- src/test/components/Root-history.test.tsx | 3 ++- src/test/components/UrlManager.test.tsx | 3 ++- src/test/store/receive-profile.test.ts | 27 +++++++++++++++++------ 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/test/components/Root-history.test.tsx b/src/test/components/Root-history.test.tsx index 13a130aa71..adacbd6510 100644 --- a/src/test/components/Root-history.test.tsx +++ b/src/test/components/Root-history.test.tsx @@ -13,6 +13,7 @@ import { autoMockCanvasContext } from '../fixtures/mocks/canvas-context'; import { fireFullClick } from '../fixtures/utils'; import { getProfileUrlForHash } from '../../utils/profile-fetch'; import { blankStore } from '../fixtures/stores'; +import { serializeProfileToJsonString } from '../../profile-logic/process-profile'; import { getProfileFromTextSamples } from '../fixtures/profiles/processed-profile'; import { autoMockFullNavigation, @@ -198,5 +199,5 @@ describe('Root with history', function () { function mockFetchProfileAtUrl(url: string, profile: Profile): void { window.fetchMock .catch(404) // catchall - .get(url, profile); + .get(url, serializeProfileToJsonString(profile)); } diff --git a/src/test/components/UrlManager.test.tsx b/src/test/components/UrlManager.test.tsx index 86d9f4ef3d..e5811d062a 100644 --- a/src/test/components/UrlManager.test.tsx +++ b/src/test/components/UrlManager.test.tsx @@ -18,6 +18,7 @@ import { waitUntilState } from '../fixtures/utils'; import { createGeckoProfile } from '../fixtures/profiles/gecko-profile'; import { getProfileFromTextSamples } from '../fixtures/profiles/processed-profile'; import { CURRENT_URL_VERSION } from '../../app-logic/url-handling'; +import { serializeProfileToJsonString } from '../../profile-logic/process-profile'; import { autoMockFullNavigation } from '../fixtures/mocks/window-navigation'; import { profilePublished } from 'firefox-profiler/actions/publish'; import { @@ -35,7 +36,7 @@ describe('UrlManager', function () { autoMockFullNavigation(); function getSerializableProfile() { - return getProfileFromTextSamples('A').profile; + return serializeProfileToJsonString(getProfileFromTextSamples('A').profile); } function setup(urlPath?: string) { diff --git a/src/test/store/receive-profile.test.ts b/src/test/store/receive-profile.test.ts index f480f467c9..7a660fa865 100644 --- a/src/test/store/receive-profile.test.ts +++ b/src/test/store/receive-profile.test.ts @@ -879,7 +879,10 @@ describe('actions/receive-profile', function () { const hash = 'c5e53f9ab6aecef926d4be68c84f2de550e2ac2f'; const expectedUrl = `https://storage.googleapis.com/profile-store/${hash}`; - window.fetchMock.get(expectedUrl, _getSimpleProfile()); + window.fetchMock.get( + expectedUrl, + serializeProfileToJsonString(_getSimpleProfile()) + ); const store = blankStore(); await store.dispatch(retrieveProfileFromStore(hash)); @@ -939,7 +942,7 @@ describe('actions/receive-profile', function () { const expectedUrl = `https://storage.googleapis.com/profile-store/${hash}`; window.fetchMock .getOnce(expectedUrl, 403) - .get(expectedUrl, _getSimpleProfile()); + .get(expectedUrl, serializeProfileToJsonString(_getSimpleProfile())); const store = blankStore(); const views = ( @@ -1026,7 +1029,10 @@ describe('actions/receive-profile', function () { it('can retrieve a profile from the web and save it to state', async function () { const expectedUrl = 'https://profiles.club/shared.json'; - window.fetchMock.get(expectedUrl, _getSimpleProfile()); + window.fetchMock.get( + expectedUrl, + serializeProfileToJsonString(_getSimpleProfile()) + ); const store = blankStore(); await store.dispatch(retrieveProfileOrZipFromUrl(expectedUrl)); @@ -1063,7 +1069,7 @@ describe('actions/receive-profile', function () { // The first call will still be a 403 -- remember, it's the default return value. window.fetchMock .getOnce(expectedUrl, 403) - .get(expectedUrl, _getSimpleProfile()); + .get(expectedUrl, serializeProfileToJsonString(_getSimpleProfile())); const store = blankStore(); const views = ( @@ -1526,7 +1532,9 @@ describe('actions/receive-profile', function () { ]) ); } - window.fetchMock.getOnce('*', profile1).getOnce('*', profile2); + window.fetchMock + .getOnce('*', serializeProfileToJsonString(profile1)) + .getOnce('*', serializeProfileToJsonString(profile2)); const { dispatch, getState } = blankStore(); await dispatch(retrieveProfilesToCompare([url1, url2])); @@ -1793,7 +1801,9 @@ describe('actions/receive-profile', function () { it('gives a fatal error when the selected thread index is out of bounds', async function () { const { dispatch, getState } = blankStore(); const { profile1, profile2 } = getSomeProfiles(); - window.fetchMock.getOnce('*', profile1).getOnce('*', profile2); + window.fetchMock + .getOnce('*', serializeProfileToJsonString(profile1)) + .getOnce('*', serializeProfileToJsonString(profile2)); await dispatch( retrieveProfilesToCompare([ @@ -1813,7 +1823,10 @@ describe('actions/receive-profile', function () { location: Partial, requiredProfile: number = 1 ) { - const profile = _getSimpleProfile(); + const simpleProfile = _getSimpleProfile(); + // Create a profile object we can use with fetchMock, which uses JSON.stringify internally. + // `simpleProfile` may contain typed arrays which wouldn't survive JSON.stringfy. + const profile = JSON.parse(serializeProfileToJsonString(simpleProfile)); const geckoProfile = createGeckoProfile(); // Add mock fetch response for the required number of times. From adaaa665975381d0ce955e4a59af48c9aaef511b Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sat, 16 May 2026 00:12:43 -0400 Subject: [PATCH 5/6] Introduce `RawStackTableBuilder` for stack table construction. `RawStackTable` is being prepared to allow `frame` to be an `Int32Array` in a later commit. `Int32Array` is fixed-size and doesn't support `push`, so the existing "push to .frame and bump .length" pattern needs a builder that uses a plain `number[]` during construction and converts to the final representation via `finishRawStackTableBuilder` at the end. Switch all stack table construction sites to use the builder. The builder still produces a plain `number[]` for `frame` in this commit; the type change happens in a follow-up. --- src/profile-logic/data-structures.ts | 33 +++++++++++++++++-- src/profile-logic/global-data-collector.ts | 13 ++++---- src/profile-logic/import/chrome.ts | 2 +- src/profile-logic/import/dhat.ts | 2 +- src/profile-logic/import/flame-graph.ts | 2 +- src/profile-logic/import/simpleperf.ts | 8 +++-- src/profile-logic/js-tracer.ts | 10 +++++- src/profile-logic/merge-compare.ts | 10 ++++-- src/profile-logic/process-profile.ts | 6 ++-- src/profile-logic/profile-data.ts | 7 ++-- src/profile-logic/symbolication.ts | 10 ++++-- src/test/fixtures/profiles/call-nodes.ts | 12 +++++-- .../fixtures/profiles/processed-profile.ts | 17 +++++++--- 13 files changed, 98 insertions(+), 34 deletions(-) diff --git a/src/profile-logic/data-structures.ts b/src/profile-logic/data-structures.ts index 18f8ab12fc..7771052bc7 100644 --- a/src/profile-logic/data-structures.ts +++ b/src/profile-logic/data-structures.ts @@ -27,6 +27,8 @@ import type { CallNodeTable, SourceTable, SourceLocationTable, + IndexIntoFrameTable, + IndexIntoStackTable, } from 'firefox-profiler/types'; /** @@ -47,7 +49,13 @@ export function getEmptySamplesTable(): RawSamplesTable { }; } -export function getEmptyRawStackTable(): RawStackTable { +export type RawStackTableBuilder = { + frame: IndexIntoFrameTable[]; + prefix: Array; + length: number; +}; + +export function getRawStackTableBuilder(): RawStackTableBuilder { return { // Important! // If modifying this structure, please update all callers of this function to ensure @@ -59,6 +67,27 @@ export function getEmptyRawStackTable(): RawStackTable { }; } +export function getRawStackTableBuilderWithExistingContents( + existing: RawStackTable +): RawStackTableBuilder { + return { + frame: [...existing.frame], + prefix: [...existing.prefix], + length: existing.length, + }; +} + +export function finishRawStackTableBuilder( + builder: RawStackTableBuilder +): RawStackTable { + const { frame, prefix, length } = builder; + return { + frame, + prefix, + length, + }; +} + /** * Returns an empty samples table with eventDelay field instead of responsiveness. * eventDelay is a new field and it replaced responsiveness. We should still @@ -393,7 +422,7 @@ export function getEmptyThread(overrides?: Partial): RawThread { export function getEmptySharedData(): RawProfileSharedData { return { - stackTable: getEmptyRawStackTable(), + stackTable: finishRawStackTableBuilder(getRawStackTableBuilder()), frameTable: getEmptyFrameTable(), funcTable: getEmptyFuncTable(), resourceTable: getEmptyResourceTable(), diff --git a/src/profile-logic/global-data-collector.ts b/src/profile-logic/global-data-collector.ts index 78b5b38543..c52e170293 100644 --- a/src/profile-logic/global-data-collector.ts +++ b/src/profile-logic/global-data-collector.ts @@ -4,13 +4,14 @@ import { StringTable } from '../utils/string-table'; import { + finishRawStackTableBuilder, getEmptyFrameTable, getEmptyFuncTable, getEmptyNativeSymbolTable, - getEmptyRawStackTable, getEmptyResourceTable, getEmptySourceTable, getEmptySourceLocationTable, + getRawStackTableBuilder, } from './data-structures'; import type { @@ -22,7 +23,6 @@ import type { RawProfileSharedData, SourceTable, FrameTable, - RawStackTable, FuncTable, ResourceTable, NativeSymbolTable, @@ -34,6 +34,7 @@ import type { Bytes, } from 'firefox-profiler/types'; import { ResourceType } from 'firefox-profiler/types'; +import type { RawStackTableBuilder } from './data-structures'; /** * GlobalDataCollector collects data which is global in the processed profile @@ -50,7 +51,7 @@ export class GlobalDataCollector { _stringTable: StringTable = StringTable.withBackingArray(this._stringArray); _sources: SourceTable = getEmptySourceTable(); _frameTable: FrameTable = getEmptyFrameTable(); - _stackTable: RawStackTable = getEmptyRawStackTable(); + _stackTableBuilder: RawStackTableBuilder = getRawStackTableBuilder(); _funcTable: FuncTable = getEmptyFuncTable(); _resourceTable: ResourceTable = getEmptyResourceTable(); _nativeSymbols: NativeSymbolTable = getEmptyNativeSymbolTable(); @@ -302,15 +303,15 @@ export class GlobalDataCollector { return this._frameTable; } - getStackTable(): RawStackTable { - return this._stackTable; + getStackTableBuilder(): RawStackTableBuilder { + return this._stackTableBuilder; } // Package up all de-duplicated global tables so that they can be embedded in // the profile. finish(): { libs: Lib[]; shared: RawProfileSharedData } { const shared: RawProfileSharedData = { - stackTable: this._stackTable, + stackTable: finishRawStackTableBuilder(this._stackTableBuilder), frameTable: this._frameTable, funcTable: this._funcTable, resourceTable: this._resourceTable, diff --git a/src/profile-logic/import/chrome.ts b/src/profile-logic/import/chrome.ts index 96add0a137..be4253f0ce 100644 --- a/src/profile-logic/import/chrome.ts +++ b/src/profile-logic/import/chrome.ts @@ -501,7 +501,7 @@ async function processTracingEvents( const stringTable = globalDataCollector.getStringTable(); const frameTable = globalDataCollector.getFrameTable(); - const stackTable = globalDataCollector.getStackTable(); + const stackTable = globalDataCollector.getStackTableBuilder(); let profileEvents: (ProfileEvent | CpuProfileEvent)[] = (eventsByName.get( 'Profile' diff --git a/src/profile-logic/import/dhat.ts b/src/profile-logic/import/dhat.ts index dc56c06f17..6f883c2e29 100644 --- a/src/profile-logic/import/dhat.ts +++ b/src/profile-logic/import/dhat.ts @@ -183,7 +183,7 @@ export function attemptToConvertDhat(json: unknown): Profile | null { profile.meta.product = dhat.cmd + ' (dhat)'; profile.meta.importedFrom = `dhat`; const globalDataCollector = new GlobalDataCollector(); - const stackTable = globalDataCollector.getStackTable(); + const stackTable = globalDataCollector.getStackTableBuilder(); const frameTable = globalDataCollector.getFrameTable(); const stringTable = globalDataCollector.getStringTable(); diff --git a/src/profile-logic/import/flame-graph.ts b/src/profile-logic/import/flame-graph.ts index 148a777bd2..b468d4d008 100644 --- a/src/profile-logic/import/flame-graph.ts +++ b/src/profile-logic/import/flame-graph.ts @@ -60,7 +60,7 @@ export function convertFlameGraphProfile(profileText: string): Profile { }); const frameTable = globalDataCollector.getFrameTable(); - const stackTable = globalDataCollector.getStackTable(); + const stackTable = globalDataCollector.getStackTableBuilder(); const { samples } = thread; // Maps to deduplicate stacks, frames, and functions. diff --git a/src/profile-logic/import/simpleperf.ts b/src/profile-logic/import/simpleperf.ts index 89b632cd85..83c38d8929 100644 --- a/src/profile-logic/import/simpleperf.ts +++ b/src/profile-logic/import/simpleperf.ts @@ -24,7 +24,9 @@ import { getEmptyFuncTable, getEmptyResourceTable, getEmptyFrameTable, - getEmptyRawStackTable, + getRawStackTableBuilder, + finishRawStackTableBuilder, + type RawStackTableBuilder, getEmptySamplesTable, getEmptyRawMarkerTable, getEmptyNativeSymbolTable, @@ -189,7 +191,7 @@ class FirefoxFrameTable { class FirefoxSampleTable { strings: StringTable; - stackTable: RawStackTable = getEmptyRawStackTable(); + stackTable: RawStackTableBuilder = getRawStackTableBuilder(); stackMap: Map = new Map(); constructor(strings: StringTable) { @@ -197,7 +199,7 @@ class FirefoxSampleTable { } toJson(): RawStackTable { - return this.stackTable; + return finishRawStackTableBuilder(this.stackTable); } findOrAddStack( diff --git a/src/profile-logic/js-tracer.ts b/src/profile-logic/js-tracer.ts index 8993ddfbca..e4664aee95 100644 --- a/src/profile-logic/js-tracer.ts +++ b/src/profile-logic/js-tracer.ts @@ -4,6 +4,8 @@ import { getEmptySamplesTableWithEventDelay, getEmptyRawMarkerTable, + finishRawStackTableBuilder, + getRawStackTableBuilderWithExistingContents, } from './data-structures'; import { StringTable } from '../utils/string-table'; import { ensureExists } from '../utils/types'; @@ -512,7 +514,10 @@ export function convertJsTracerToThreadWithoutSamples( samples, }; - const { funcTable, frameTable, stackTable } = shared; + const { funcTable, frameTable } = shared; + const stackTable = getRawStackTableBuilderWithExistingContents( + shared.stackTable + ); // Keep a stack of js tracer events, and end timings, that will be used to find // the stack prefixes. Once a JS tracer event starts past another event end, the @@ -620,6 +625,9 @@ export function convertJsTracerToThreadWithoutSamples( unmatchedEventEnds[unmatchedIndex] = end; } + // Write the augmented stackTable back to the shared data. + shared.stackTable = finishRawStackTableBuilder(stackTable); + return { thread, stackMap }; } diff --git a/src/profile-logic/merge-compare.ts b/src/profile-logic/merge-compare.ts index 5eeecb1705..6e9e424b1b 100644 --- a/src/profile-logic/merge-compare.ts +++ b/src/profile-logic/merge-compare.ts @@ -13,7 +13,8 @@ import { getEmptyNativeSymbolTable, getEmptyFrameTable, getEmptyFuncTable, - getEmptyRawStackTable, + getRawStackTableBuilder, + finishRawStackTableBuilder, getEmptyRawMarkerTable, getEmptySamplesTableWithEventDelay, shallowCloneRawMarkerTable, @@ -1117,7 +1118,7 @@ function mergeStackTables( translationMapsForFrames: TranslationMapForFrames[] ): { stackTable: RawStackTable; translationMaps: TranslationMapForStacks[] } { const translationMaps: TranslationMapForStacks[] = []; - const newStackTable = getEmptyRawStackTable(); + const newStackTable = getRawStackTableBuilder(); profiles.forEach((profile, profileIndex) => { const { stackTable } = profile.shared; @@ -1143,7 +1144,10 @@ function mergeStackTables( translationMaps.push(oldStackToNewStackPlusOne); }); - return { stackTable: newStackTable, translationMaps }; + return { + stackTable: finishRawStackTableBuilder(newStackTable), + translationMaps, + }; } /** diff --git a/src/profile-logic/process-profile.ts b/src/profile-logic/process-profile.ts index 3781d305fa..d1f96c4c90 100644 --- a/src/profile-logic/process-profile.ts +++ b/src/profile-logic/process-profile.ts @@ -16,6 +16,7 @@ import { getEmptyRawMarkerTable, getEmptyJsAllocationsTable, getEmptyUnbalancedNativeAllocationsTable, + type RawStackTableBuilder, } from './data-structures'; import { immutableUpdate, ensureExists } from '../utils/types'; import { verifyMagic, SIMPLEPERF as SIMPLEPERF_MAGIC } from '../utils/magic'; @@ -53,7 +54,6 @@ import type { FrameTable, RawCounterSamplesTable, RawSamplesTable, - RawStackTable, RawMarkerTable, LibMapping, IndexIntoStackTable, @@ -549,7 +549,7 @@ function _processFrameTable( */ function _processStackTable( geckoStackTable: GeckoStackStruct, - sharedStackTable: RawStackTable, + sharedStackTable: RawStackTableBuilder, frameIndexOffset: IndexIntoFrameTable ): IndexIntoStackTable { const stackIndexOffset = sharedStackTable.length; @@ -1337,7 +1337,7 @@ function _processThread( ); const stackIndexOffset = _processStackTable( geckoStackTable, - globalDataCollector.getStackTable(), + globalDataCollector.getStackTableBuilder(), frameIndexOffset ); diff --git a/src/profile-logic/profile-data.ts b/src/profile-logic/profile-data.ts index 10100ef72e..43db6eb47f 100644 --- a/src/profile-logic/profile-data.ts +++ b/src/profile-logic/profile-data.ts @@ -6,7 +6,8 @@ import memoize from 'memoize-immutable'; import MixedTupleMap from 'mixedtuplemap'; import { oneLine } from 'common-tags'; import { - getEmptyRawStackTable, + getRawStackTableBuilder, + finishRawStackTableBuilder, getEmptyCallNodeTable, shallowCloneFrameTable, shallowCloneFuncTable, @@ -4343,7 +4344,7 @@ export function nudgeReturnAddresses(profile: Profile): Profile { // Now the frame table contains adjusted / "nudged" addresses. // Make a new stack table which refers to the adjusted frames. - const newStackTable = getEmptyRawStackTable(); + const newStackTable = getRawStackTableBuilder(); const mapForSamplingSelfStacks = new Map< null | IndexIntoStackTable, null | IndexIntoStackTable @@ -4385,7 +4386,7 @@ export function nudgeReturnAddresses(profile: Profile): Profile { const newShared: RawProfileSharedData = { ...profile.shared, frameTable: newFrameTable, - stackTable: newStackTable, + stackTable: finishRawStackTableBuilder(newStackTable), }; const newThreads = updateRawThreadStacksSeparate( diff --git a/src/profile-logic/symbolication.ts b/src/profile-logic/symbolication.ts index a6370bc6fa..dfbfd345c2 100644 --- a/src/profile-logic/symbolication.ts +++ b/src/profile-logic/symbolication.ts @@ -2,7 +2,8 @@ * 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/. */ import { - getEmptyRawStackTable, + getRawStackTableBuilder, + finishRawStackTableBuilder, shallowCloneFuncTable, shallowCloneNativeSymbolTable, shallowCloneFrameTable, @@ -420,7 +421,7 @@ function _computeStackTableWithAddedExpansionStacks( if (frameIndexToInlineExpansionFrames.size === 0) { return null; } - const newStackTable = getEmptyRawStackTable(); + const newStackTable = getRawStackTableBuilder(); const oldStackToNewStack = new Int32Array(stackTable.length); for (let stack = 0; stack < stackTable.length; stack++) { const oldFrame = stackTable.frame[stack]; @@ -452,7 +453,10 @@ function _computeStackTableWithAddedExpansionStacks( } oldStackToNewStack[stack] = prefix ?? -1; } - return { newStackTable, oldStackToNewStack }; + return { + newStackTable: finishRawStackTableBuilder(newStackTable), + oldStackToNewStack, + }; } /** diff --git a/src/test/fixtures/profiles/call-nodes.ts b/src/test/fixtures/profiles/call-nodes.ts index 909b48e7e7..0ffed2dd52 100644 --- a/src/test/fixtures/profiles/call-nodes.ts +++ b/src/test/fixtures/profiles/call-nodes.ts @@ -6,7 +6,8 @@ import type { FuncTable, FrameTable, Profile } from 'firefox-profiler/types'; import { getEmptyThread, getEmptyProfile, - getEmptyRawStackTable, + getRawStackTableBuilder, + finishRawStackTableBuilder, } from '../../../profile-logic/data-structures'; import { StringTable } from '../../../utils/string-table'; @@ -87,7 +88,7 @@ export default function getProfile(): Profile { length: frameFuncs.length, }; - const stackTable = getEmptyRawStackTable(); + const stackTable = getRawStackTableBuilder(); // Provide a utility function for readability. function addToStackTable(frame: any, prefix: any) { @@ -119,7 +120,12 @@ export default function getProfile(): Profile { length: 2, }; - profile.shared = { ...profile.shared, stackTable, funcTable, frameTable }; + profile.shared = { + ...profile.shared, + stackTable: finishRawStackTableBuilder(stackTable), + funcTable, + frameTable, + }; profile.threads.push(thread, { ...thread }, { ...thread }); diff --git a/src/test/fixtures/profiles/processed-profile.ts b/src/test/fixtures/profiles/processed-profile.ts index 07ef1d7bab..708018511f 100644 --- a/src/test/fixtures/profiles/processed-profile.ts +++ b/src/test/fixtures/profiles/processed-profile.ts @@ -8,6 +8,8 @@ import { getEmptyJsAllocationsTable, getEmptyUnbalancedNativeAllocationsTable, getEmptyBalancedNativeAllocationsTable, + getRawStackTableBuilderWithExistingContents, + finishRawStackTableBuilder, } from '../../../profile-logic/data-structures'; import { mergeProfilesForDiffing } from '../../../profile-logic/merge-compare'; import { computeReferenceCPUDeltaPerMs } from '../../../profile-logic/cpu'; @@ -983,7 +985,7 @@ function _buildThreadFromTextOnlyStacks( } // Attempt to find a stack that satisfies the given frameIndex and prefix. - const stackTable = globalDataCollector.getStackTable(); + const stackTable = globalDataCollector.getStackTableBuilder(); let stackIndex; for (let i = 0; i < stackTable.length; i++) { if ( @@ -1994,8 +1996,10 @@ function getStackIndexForCallNodePath( /** * Use this function to add window id information to frames, using call node * paths to point to frames using stacks. + * This mutates `shared`. * * @param thread The thread to mutate. + * @param shared The shared profile data to mutate. * @param listOfOperations A list of pairs { innerWindowID, callNodes } * indicating which call nodes this innerWindowID will * be assigned to. @@ -2037,6 +2041,9 @@ export function addInnerWindowIdToStacks( IndexIntoStackTable >(); + const stackTableBuilder = + getRawStackTableBuilderWithExistingContents(stackTable); + for (const callNode of callNodesToDupe) { const stackIndex = getStackIndexForCallNodePath(shared, callNode); const foundFrameIndex = stackTable.frame[stackIndex]; @@ -2062,14 +2069,16 @@ export function addInnerWindowIdToStacks( frameTable.innerWindowID.push(listOfOperations[1].innerWindowID); // Clone the stack - const newStackIndex = stackTable.length++; - stackTable.prefix.push(stackTable.prefix[stackIndex]); + const newStackIndex = stackTableBuilder.length++; + stackTableBuilder.prefix.push(stackTable.prefix[stackIndex]); // Using the cloned frame index. - stackTable.frame.push(newFrameIndex); + stackTableBuilder.frame.push(newFrameIndex); mapStackIndexToDupe.set(stackIndex, newStackIndex); } + shared.stackTable = finishRawStackTableBuilder(stackTableBuilder); + const sampleTimes = ensureExists(samples.time); for (let sampleIndex = samples.length; sampleIndex >= 0; sampleIndex--) { // We're looping from the end because we'll push some samples to the end From d116be9ff9f0a0fffa74b4384990bfef4103dc82 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sat, 16 May 2026 00:24:17 -0400 Subject: [PATCH 6/6] Allow the `stackTable.frame` column to contain an Int32Array. This is the first typed array that we're supporting inside the profile format. When a profile is saved in the JsonSlabs format, it will now have this column as a separate slab that doesn't require JSON parsing. Profile compacting now always turns `stackTable.frame` into a typed array, even if that column was a regular JS array in the input profile. We still allow a regular JSON array here, because profiles stored as JSON cannot contain typed arrays, and we want to use the same type definition for JSON and JSLB profiles. Here's how this change impacts profile sizes and loading times on this profile: https://storage.googleapis.com/profiler-get-symbols-fixtures/large-speedometer3-profile.json.gz | Version | .jslb.gz size | .jslb size | Load time | Profile of it loading | |---------|---------------|------------|-------------|-----------------------------------| | 64 | 122 MB | 605 MB | 7.6 seconds | https://share.firefox.dev/4ogUKba | | 65 | 125 MB | 544 MB | 6.0 seconds | https://share.firefox.dev/3Qopiem | The compressed size has grown a small bit, but the other savings are significant: - We no longer have 131 MB of text for the frame column in the JSON - the frame column is now stored in a 70 MB i32 slab. - Less time in GZ decompression, because the uncompressed size is now smaller. - There is a lot less time spent in TextDecoder.decode and JSON.parse, because we're no longer decoding and parsing 131 MB of text for the frame column. --- docs-developer/CHANGELOG-formats.md | 4 ++ src/app-logic/constants.ts | 2 +- src/profile-logic/data-structures.ts | 2 +- .../processed-profile-versioning.ts | 5 ++ src/profile-logic/profile-compacting.ts | 60 +++++++++++++--- src/profile-logic/profile-data.ts | 5 +- .../__snapshots__/profiler-edit.test.ts.snap | 8 +-- .../__snapshots__/profile-view.test.ts.snap | 4 +- .../profile-conversion.test.ts.snap | 72 +++++++++---------- .../profile-upgrading.test.ts.snap | 10 +-- src/types/profile.ts | 2 +- 11 files changed, 114 insertions(+), 60 deletions(-) diff --git a/docs-developer/CHANGELOG-formats.md b/docs-developer/CHANGELOG-formats.md index e07d501aa6..b59d5fe0e3 100644 --- a/docs-developer/CHANGELOG-formats.md +++ b/docs-developer/CHANGELOG-formats.md @@ -6,6 +6,10 @@ Note that this is not an exhaustive list. Processed profile format upgraders can ## Processed profile format +### Version 65 + +The stack table's `frame` column (stored at `profile.shared.stackTable.frame`) can now optionally be stored as an `Int32Array`, for profiles loaded from [JsonSlabs](https://github.com/mstange/json-slabs/) files (.jslb, .jslb.gz). Regular JS / JSON arrays are still accepted. + ### Version 64 A new `SourceLocationTable` has been added to `profile.shared.sourceLocationTable`. It holds the original (pre-compilation) source positions produced by source map symbolication, paired with the generated `line`/`column` already on `FrameTable`. diff --git a/src/app-logic/constants.ts b/src/app-logic/constants.ts index 6cba739b59..8d39768163 100644 --- a/src/app-logic/constants.ts +++ b/src/app-logic/constants.ts @@ -12,7 +12,7 @@ export const GECKO_PROFILE_VERSION = 34; // The current version of the "processed" profile format. // Please don't forget to update the processed profile format changelog in // `docs-developer/CHANGELOG-formats.md`. -export const PROCESSED_PROFILE_VERSION = 64; +export const PROCESSED_PROFILE_VERSION = 65; // The following are the margin sizes for the left and right of the timeline. Independent // components need to share these values. diff --git a/src/profile-logic/data-structures.ts b/src/profile-logic/data-structures.ts index 7771052bc7..0599e2153e 100644 --- a/src/profile-logic/data-structures.ts +++ b/src/profile-logic/data-structures.ts @@ -82,7 +82,7 @@ export function finishRawStackTableBuilder( ): RawStackTable { const { frame, prefix, length } = builder; return { - frame, + frame: new Int32Array(frame), prefix, length, }; diff --git a/src/profile-logic/processed-profile-versioning.ts b/src/profile-logic/processed-profile-versioning.ts index bcf3931179..16ab69e409 100644 --- a/src/profile-logic/processed-profile-versioning.ts +++ b/src/profile-logic/processed-profile-versioning.ts @@ -3255,6 +3255,11 @@ const _upgraders: { }; sources.content = new Array(sources.length).fill(null); }, + [65]: (_profile: any) => { + // The type of `profile.shared.stackTable.frame` was changed from + // `IndexIntoFrameTable[]` to `IndexIntoFrameTable[] | Int32Array`. + // All valid v64 profiles are valid v65 profiles, so no upgrader is needed. + }, // If you add a new upgrader here, please document the change in // `docs-developer/CHANGELOG-formats.md`. }; diff --git a/src/profile-logic/profile-compacting.ts b/src/profile-logic/profile-compacting.ts index 492f914716..e7300a3551 100644 --- a/src/profile-logic/profile-compacting.ts +++ b/src/profile-logic/profile-compacting.ts @@ -48,15 +48,22 @@ type ColumnDescription = null extends ( | { type: 'INDEX_REF_OR_NULL'; referencedTable: TableCompactionState } | { type: 'SELF_INDEX_REF_OR_NULL' } | { type: 'NO_REF' } - : - | { type: 'INDEX_REF'; referencedTable: TableCompactionState } - | { type: 'INDEX_REF_OR_NEG_ONE'; referencedTable: TableCompactionState } - | { type: 'NO_REF' }; + : Int32Array extends TCol + ? + | { type: 'INDEX_REF_INT32'; referencedTable: TableCompactionState } + | { type: 'NO_REF' } + : + | { type: 'INDEX_REF'; referencedTable: TableCompactionState } + | { + type: 'INDEX_REF_OR_NEG_ONE'; + referencedTable: TableCompactionState; + } + | { type: 'NO_REF' }; type TableDescription = { - [K in keyof T as T[K] extends Array ? K : never]: ColumnDescription< - T[K] - >; + [K in keyof T as T[K] extends Array | Int32Array + ? K + : never]: ColumnDescription; }; const ColDesc = { @@ -64,6 +71,10 @@ const ColDesc = { type: 'INDEX_REF' as const, referencedTable, }), + indexRefInt32: (referencedTable: TableCompactionState) => ({ + type: 'INDEX_REF_INT32' as const, + referencedTable, + }), indexRefOrNull: (referencedTable: TableCompactionState) => ({ type: 'INDEX_REF_OR_NULL' as const, referencedTable, @@ -145,7 +156,7 @@ export function computeCompactedProfile( }; const stackTableDesc: TableDescription = { - frame: ColDesc.indexRef(tcs.frameTable), + frame: ColDesc.indexRefInt32(tcs.frameTable), prefix: ColDesc.selfIndexRefOrNull(), }; const frameTableDesc: TableDescription = { @@ -326,6 +337,7 @@ function _markTableAndComputeTranslation( const col = (table as any)[key]; switch (desc.type) { case 'INDEX_REF': + case 'INDEX_REF_INT32': markColumn(col, markBuffer, desc.referencedTable.markBuffer); break; case 'INDEX_REF_OR_NULL': @@ -354,7 +366,13 @@ function _markTableAndComputeTranslation( thisTableCompactionState.computeIndexTranslation(); } -function markColumn(col: Array, shouldMark: BitSet, markBuf: BitSet) { +function markColumn( + col: Array | Int32Array, + shouldMark: BitSet, + markBuf: BitSet +) { + // Polymorphic: indexing works the same on Int32Array as on number[], so the + // INDEX_REF and INDEX_REF_INT32 cases share this function. for (let i = 0; i < col.length; i++) { if (checkBit(shouldMark, i)) { const val = col[i]; @@ -499,6 +517,14 @@ function _compactTable( newLength ); break; + case 'INDEX_REF_INT32': + result[key] = _compactColIndexInt32( + oldCol, + markBuffer, + desc.referencedTable.oldIndexToNewIndexPlusOne, + newLength + ); + break; case 'INDEX_REF_OR_NULL': result[key] = _compactColIndexOrNull( oldCol, @@ -564,6 +590,22 @@ function _compactColIndex( return newCol; } +function _compactColIndexInt32( + oldCol: Int32Array, + markBuffer: BitSet, + oldIndexToNewIndexPlusOne: Int32Array, + newLength: number +): Int32Array { + const newCol = new Int32Array(newLength); + let newIndex = 0; + for (let i = 0; i < oldCol.length; i++) { + if (checkBit(markBuffer, i)) { + newCol[newIndex++] = oldIndexToNewIndexPlusOne[oldCol[i]] - 1; + } + } + return newCol; +} + function _compactColIndexOrNull( oldCol: (number | null)[], markBuffer: BitSet, diff --git a/src/profile-logic/profile-data.ts b/src/profile-logic/profile-data.ts index 43db6eb47f..3b6e424499 100644 --- a/src/profile-logic/profile-data.ts +++ b/src/profile-logic/profile-data.ts @@ -4766,7 +4766,10 @@ export function computeStackTableFromRawStackTable( } // The frame column is a typed array in the derived stack table. - const frame = new Int32Array(rawStackTable.frame); + const frame = + rawStackTable.frame instanceof Int32Array + ? rawStackTable.frame + : new Int32Array(rawStackTable.frame); return { frame, diff --git a/src/test/integration/profiler-edit/__snapshots__/profiler-edit.test.ts.snap b/src/test/integration/profiler-edit/__snapshots__/profiler-edit.test.ts.snap index 9e919e59d0..8ffda5719f 100644 --- a/src/test/integration/profiler-edit/__snapshots__/profiler-edit.test.ts.snap +++ b/src/test/integration/profiler-edit/__snapshots__/profiler-edit.test.ts.snap @@ -87,7 +87,7 @@ Object { "markerSchema": Array [], "oscpu": "macOS 14.6.1", "pausedRanges": Array [], - "preprocessedProfileVersion": 64, + "preprocessedProfileVersion": 65, "processType": 0, "product": "a.out", "sampleUnits": Object { @@ -1452,7 +1452,7 @@ Object { "markerSchema": Array [], "oscpu": "macOS 14.6.1", "pausedRanges": Array [], - "preprocessedProfileVersion": 64, + "preprocessedProfileVersion": 65, "processType": 0, "product": "a.out", "sampleUnits": Object { @@ -2817,7 +2817,7 @@ Object { "markerSchema": Array [], "oscpu": "macOS 14.6.1", "pausedRanges": Array [], - "preprocessedProfileVersion": 64, + "preprocessedProfileVersion": 65, "processType": 0, "product": "a.out", "sampleUnits": Object { @@ -4182,7 +4182,7 @@ Object { "markerSchema": Array [], "oscpu": "macOS 14.6.1", "pausedRanges": Array [], - "preprocessedProfileVersion": 64, + "preprocessedProfileVersion": 65, "processType": 0, "product": "a.out", "sampleUnits": Object { diff --git a/src/test/store/__snapshots__/profile-view.test.ts.snap b/src/test/store/__snapshots__/profile-view.test.ts.snap index 738ec4d3d7..0e69b4671b 100644 --- a/src/test/store/__snapshots__/profile-view.test.ts.snap +++ b/src/test/store/__snapshots__/profile-view.test.ts.snap @@ -423,7 +423,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 64, + "preprocessedProfileVersion": 65, "processType": 0, "product": "Firefox", "sourceURL": "", @@ -676,7 +676,7 @@ Object { "startLine": Array [], }, "stackTable": Object { - "frame": Array [ + "frame": Int32Array [ 0, 1, 2, diff --git a/src/test/unit/__snapshots__/profile-conversion.test.ts.snap b/src/test/unit/__snapshots__/profile-conversion.test.ts.snap index 1ef1824e9b..de79c9b9c5 100644 --- a/src/test/unit/__snapshots__/profile-conversion.test.ts.snap +++ b/src/test/unit/__snapshots__/profile-conversion.test.ts.snap @@ -591,7 +591,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 64, + "preprocessedProfileVersion": 65, "processType": 0, "product": "ART Trace (Android)", "sampleUnits": undefined, @@ -43096,7 +43096,7 @@ Object { "startLine": Array [], }, "stackTable": Object { - "frame": Array [ + "frame": Int32Array [ 0, 1, 2, @@ -83833,7 +83833,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 64, + "preprocessedProfileVersion": 65, "processType": 0, "product": "ART Trace (Android)", "sampleUnits": undefined, @@ -167262,7 +167262,7 @@ Object { "startLine": Array [], }, "stackTable": Object { - "frame": Array [ + "frame": Int32Array [ 0, 1, 2, @@ -329278,7 +329278,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 64, + "preprocessedProfileVersion": 65, "processType": 0, "product": "Chrome Trace", "profilingEndTime": 119159778.026, @@ -329817,7 +329817,7 @@ Object { ], }, "stackTable": Object { - "frame": Array [ + "frame": Int32Array [ 0, 1, 2, @@ -364259,7 +364259,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 64, + "preprocessedProfileVersion": 65, "processType": 0, "product": "Chrome Trace", "profilingEndTime": 119159778.026, @@ -364798,7 +364798,7 @@ Object { ], }, "stackTable": Object { - "frame": Array [ + "frame": Int32Array [ 0, 1, 2, @@ -399219,7 +399219,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 64, + "preprocessedProfileVersion": 65, "processType": 0, "product": "Chrome Trace", "profilingEndTime": 66155012.423, @@ -400087,7 +400087,7 @@ Object { ], }, "stackTable": Object { - "frame": Array [ + "frame": Int32Array [ 0, 1, 2, @@ -401927,7 +401927,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 64, + "preprocessedProfileVersion": 65, "processType": 0, "product": "Chrome Trace", "sourceURL": "", @@ -401993,7 +401993,7 @@ Object { "startLine": Array [], }, "stackTable": Object { - "frame": Array [], + "frame": Int32Array [], "length": 0, "prefix": Array [], }, @@ -403765,7 +403765,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 64, + "preprocessedProfileVersion": 65, "processType": 0, "product": "Chrome Trace", "profilingEndTime": 355035987.653, @@ -406722,7 +406722,7 @@ Object { ], }, "stackTable": Object { - "frame": Array [ + "frame": Int32Array [ 0, 1, 2, @@ -407776,7 +407776,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 64, + "preprocessedProfileVersion": 65, "processType": 0, "product": "Chrome Trace", "sourceURL": "", @@ -411314,7 +411314,7 @@ Object { ], }, "stackTable": Object { - "frame": Array [ + "frame": Int32Array [ 0, 1, 2, @@ -413399,7 +413399,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 64, + "preprocessedProfileVersion": 65, "processType": 0, "product": "Chrome Trace", "profilingEndTime": 66155012.423, @@ -414267,7 +414267,7 @@ Object { ], }, "stackTable": Object { - "frame": Array [ + "frame": Int32Array [ 0, 1, 2, @@ -414544,7 +414544,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 64, + "preprocessedProfileVersion": 65, "processType": 0, "product": "Firefox", "sampleUnits": undefined, @@ -417285,7 +417285,7 @@ Object { "startLine": Array [], }, "stackTable": Object { - "frame": Array [ + "frame": Int32Array [ 0, 1, 2, @@ -418704,7 +418704,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 64, + "preprocessedProfileVersion": 65, "processType": 0, "product": "Firefox", "sampleUnits": undefined, @@ -429901,7 +429901,7 @@ Object { "startLine": Array [], }, "stackTable": Object { - "frame": Array [ + "frame": Int32Array [ 0, 1, 2, @@ -434833,7 +434833,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 64, + "preprocessedProfileVersion": 65, "processType": 0, "product": "Firefox", "sampleUnits": undefined, @@ -439076,7 +439076,7 @@ Object { "startLine": Array [], }, "stackTable": Object { - "frame": Array [ + "frame": Int32Array [ 0, 1, 2, @@ -441509,7 +441509,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 64, + "preprocessedProfileVersion": 65, "processType": 0, "product": "Firefox", "sampleUnits": undefined, @@ -454858,7 +454858,7 @@ Object { "startLine": Array [], }, "stackTable": Object { - "frame": Array [ + "frame": Int32Array [ 0, 1, 2, @@ -459410,7 +459410,7 @@ Object { "keepProfileThreadOrder": true, "markerSchema": Array [], "platform": "Android", - "preprocessedProfileVersion": 64, + "preprocessedProfileVersion": 65, "processType": 0, "product": "com.example.sampleapplication", "sourceCodeIsNotOnSearchfox": true, @@ -499592,7 +499592,7 @@ Object { "startLine": Array [], }, "stackTable": Object { - "frame": Array [ + "frame": Int32Array [ 0, 1, 2, @@ -521502,7 +521502,7 @@ Object { "keepProfileThreadOrder": true, "markerSchema": Array [], "platform": "Android", - "preprocessedProfileVersion": 64, + "preprocessedProfileVersion": 65, "processType": 0, "product": "com.example.sampleapplication", "sourceCodeIsNotOnSearchfox": true, @@ -559132,7 +559132,7 @@ Object { "startLine": Array [], }, "stackTable": Object { - "frame": Array [ + "frame": Int32Array [ 0, 1, 2, @@ -587927,7 +587927,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 64, + "preprocessedProfileVersion": 65, "processType": 0, "product": "target/debug/examples/work_log (dhat)", "sourceURL": "", @@ -594273,7 +594273,7 @@ Object { ], }, "stackTable": Object { - "frame": Array [ + "frame": Int32Array [ 0, 56, 55, @@ -597177,7 +597177,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 64, + "preprocessedProfileVersion": 65, "processType": 0, "product": "Flamegraph", "sourceURL": "", @@ -600033,7 +600033,7 @@ Object { "startLine": Array [], }, "stackTable": Object { - "frame": Array [ + "frame": Int32Array [ 0, 1, 2, @@ -905382,7 +905382,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 64, + "preprocessedProfileVersion": 65, "processType": 0, "product": "Flamegraph", "sourceURL": "", @@ -905556,7 +905556,7 @@ Object { "startLine": Array [], }, "stackTable": Object { - "frame": Array [ + "frame": Int32Array [ 0, 1, 2, diff --git a/src/test/unit/__snapshots__/profile-upgrading.test.ts.snap b/src/test/unit/__snapshots__/profile-upgrading.test.ts.snap index bd079be0b9..3715a6c4fa 100644 --- a/src/test/unit/__snapshots__/profile-upgrading.test.ts.snap +++ b/src/test/unit/__snapshots__/profile-upgrading.test.ts.snap @@ -40,7 +40,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 64, + "preprocessedProfileVersion": 65, "processType": 0, "product": "Firefox", "sampleUnits": undefined, @@ -2781,7 +2781,7 @@ Object { "startLine": Array [], }, "stackTable": Object { - "frame": Array [ + "frame": Int32Array [ 0, 1, 2, @@ -7643,7 +7643,7 @@ Object { "misc": "rv:48.0", "oscpu": "Intel Mac OS X 10.11", "platform": "Macintosh", - "preprocessedProfileVersion": 64, + "preprocessedProfileVersion": 65, "processType": 0, "product": "Firefox", "stackwalk": 1, @@ -9019,7 +9019,7 @@ Object { "misc": "rv:48.0", "oscpu": "Intel Mac OS X 10.11", "platform": "Macintosh", - "preprocessedProfileVersion": 64, + "preprocessedProfileVersion": 65, "processType": 0, "product": "Firefox", "stackwalk": 1, @@ -10563,7 +10563,7 @@ Object { "misc": "rv:48.0", "oscpu": "Intel Mac OS X 10.11", "platform": "Macintosh", - "preprocessedProfileVersion": 64, + "preprocessedProfileVersion": 65, "processType": 0, "product": "Firefox", "stackwalk": 1, diff --git a/src/types/profile.ts b/src/types/profile.ts index ad504ac933..fd1ce620f8 100644 --- a/src/types/profile.ts +++ b/src/types/profile.ts @@ -60,7 +60,7 @@ export type Pid = string; * to storing them as actual lists of frames. */ export type RawStackTable = { - frame: IndexIntoFrameTable[]; + frame: IndexIntoFrameTable[] | Int32Array; prefix: Array; length: number; };