diff --git a/src/node-tools/profiler-edit.ts b/src/node-tools/profiler-edit.ts index ed67f35e43..2dba4034d3 100644 --- a/src/node-tools/profiler-edit.ts +++ b/src/node-tools/profiler-edit.ts @@ -11,6 +11,7 @@ import { import { parse as parseToml } from 'smol-toml'; import { + optimizeProfileForStorage, serializeProfileToJsonSlabsFile, serializeProfileToJsonString, unserializeProfileOfArbitraryFormat, @@ -205,7 +206,9 @@ async function encodeProfileWithFilename( filename: string ): Promise { if (filename.endsWith('.jslb') || filename.endsWith('.jslb.gz')) { - const bytes = serializeProfileToJsonSlabsFile(profile); + const bytes = serializeProfileToJsonSlabsFile( + optimizeProfileForStorage(profile) + ); if (filename.endsWith('.jslb.gz')) { return compress(bytes); } diff --git a/src/profile-logic/process-profile.ts b/src/profile-logic/process-profile.ts index 3e5278f474..ee554fe080 100644 --- a/src/profile-logic/process-profile.ts +++ b/src/profile-logic/process-profile.ts @@ -47,6 +47,11 @@ import { getFriendlyThreadName, nudgeReturnAddresses, } from '../profile-logic/profile-data'; +import { + toInt32Array, + toUint8Array, + toFloat64Array, +} from '../utils/typed-arrays'; import { computeStringIndexMarkerFieldsByDataType } from '../profile-logic/marker-schema'; import { convertJsTracerToThread } from '../profile-logic/js-tracer'; @@ -109,6 +114,7 @@ import type { IndexIntoCategoryList, IndexIntoFrameTable, CounterDisplayConfig, + RawProfileSharedData, } from 'firefox-profiler/types'; import { decompress, isGzip } from 'firefox-profiler/utils/gz'; import { jsonEncodeObjectWithTypedArraysAsRegularArrays } from 'firefox-profiler/utils/json-with-typed-arrays'; @@ -2058,6 +2064,90 @@ export function serializeProfileToJsonString(profile: Profile): string { return jsonEncodeObjectWithTypedArraysAsRegularArrays(profile); } +/** + * Convert all columns of a profile which are eligible to be stored as typed + * arrays into typed arrays. + * + * This is useful if the profile will be saved as a JSLB file: + * `serializeProfileToJsonSlabsFile` will be able to store these columns as binary + * slabs instead of as JSON arrays, which speeds up both the saving and the loading + * of the file. + * + * Does not mutate the input profile. + */ +export function optimizeProfileForStorage(profile: Profile): Profile { + return { + ...profile, + shared: convertSharedTablesEligibleColumns(profile.shared), + threads: profile.threads.map(convertThreadEligibleColumns), + counters: profile.counters?.map(convertCounterEligibleColumns), + }; +} + +function convertSharedTablesEligibleColumns( + shared: RawProfileSharedData +): RawProfileSharedData { + const { stackTable, frameTable } = shared; + return { + ...shared, + stackTable: { + frame: toInt32Array(stackTable.frame), + prefixOffset: toInt32Array(stackTable.prefixOffset), + length: stackTable.length, + }, + frameTable: { + ...frameTable, + address: toInt32Array(frameTable.address), + inlineDepth: toUint8Array(frameTable.inlineDepth), + func: toInt32Array(frameTable.func), + }, + }; +} + +function convertThreadEligibleColumns(thread: RawThread): RawThread { + const newThread: RawThread = { + ...thread, + samples: convertSamplesTimesToTypedArrays(thread.samples), + }; + if (thread.jsAllocations !== undefined) { + newThread.jsAllocations = { + ...thread.jsAllocations, + time: toFloat64Array(thread.jsAllocations.time), + }; + } + if (thread.nativeAllocations !== undefined) { + newThread.nativeAllocations = { + ...thread.nativeAllocations, + time: toFloat64Array(thread.nativeAllocations.time), + }; + } + return newThread; +} + +function convertSamplesTimesToTypedArrays( + samples: RawSamplesTable +): RawSamplesTable { + const result = { ...samples }; + if (samples.time !== undefined) { + result.time = toFloat64Array(samples.time); + } + if (samples.timeDeltas !== undefined) { + result.timeDeltas = toFloat64Array(samples.timeDeltas); + } + return result; +} + +function convertCounterEligibleColumns(counter: RawCounter): RawCounter { + const samples: RawCounterSamplesTable = { ...counter.samples }; + if (counter.samples.time !== undefined) { + samples.time = toFloat64Array(counter.samples.time); + } + if (counter.samples.timeDeltas !== undefined) { + samples.timeDeltas = toFloat64Array(counter.samples.timeDeltas); + } + return { ...counter, samples }; +} + /** * Take a profile and convert it to a Uint8Array in the JsonSlabs format. * diff --git a/src/profile-logic/profile-data.ts b/src/profile-logic/profile-data.ts index 0f80103f34..085fc28fd4 100644 --- a/src/profile-logic/profile-data.ts +++ b/src/profile-logic/profile-data.ts @@ -111,6 +111,11 @@ import type { } from 'firefox-profiler/types'; import { SelectedState, ResourceType } from 'firefox-profiler/types'; import type { CallNodeInfo, SuffixOrderIndex } from './call-node-info'; +import { + toFloat64Array, + toInt32Array, + toUint8Array, +} from 'firefox-profiler/utils/typed-arrays'; /** * Various helpers for dealing with the profile as a data structure. @@ -1944,7 +1949,7 @@ export function computeTimeColumnForRawSamplesTable( ): Float64Array { const { time, timeDeltas } = samples; if (time !== undefined) { - return time instanceof Float64Array ? time : new Float64Array(time); + return toFloat64Array(time); } return numberSeriesFromDeltas(ensureExists(timeDeltas)); } @@ -2793,8 +2798,7 @@ export function computeJsAllocationsTableFromRawJsAllocationsTable( raw: RawJsAllocationsTable ): JsAllocationsTable { return { - time: - raw.time instanceof Float64Array ? raw.time : new Float64Array(raw.time), + time: toFloat64Array(raw.time), className: raw.className, typeName: raw.typeName, coarseType: raw.coarseType, @@ -2809,8 +2813,7 @@ export function computeJsAllocationsTableFromRawJsAllocationsTable( export function computeNativeAllocationsTableFromRawNativeAllocationsTable( raw: RawNativeAllocationsTable ): NativeAllocationsTable { - const time = - raw.time instanceof Float64Array ? raw.time : new Float64Array(raw.time); + const time = toFloat64Array(raw.time); if ('memoryAddress' in raw) { return { time, @@ -4784,18 +4787,9 @@ export function computeTabToThreadIndexesMap( export function computeFrameTableFromRawFrameTable( rawFrameTable: RawFrameTable ): FrameTable { - const address = - rawFrameTable.address instanceof Int32Array - ? rawFrameTable.address - : new Int32Array(rawFrameTable.address); - const inlineDepth = - rawFrameTable.inlineDepth instanceof Uint8Array - ? rawFrameTable.inlineDepth - : new Uint8Array(rawFrameTable.inlineDepth); - const func = - rawFrameTable.func instanceof Int32Array - ? rawFrameTable.func - : new Int32Array(rawFrameTable.func); + const address = toInt32Array(rawFrameTable.address); + const inlineDepth = toUint8Array(rawFrameTable.inlineDepth); + const func = toInt32Array(rawFrameTable.func); return { address, inlineDepth, @@ -4863,10 +4857,7 @@ export function computeStackTableFromRawStackTable( } // The frame column is a typed array in the derived stack table. - const frame = - rawStackTable.frame instanceof Int32Array - ? rawStackTable.frame - : new Int32Array(rawStackTable.frame); + const frame = toInt32Array(rawStackTable.frame); return { frame, diff --git a/src/utils/typed-arrays.ts b/src/utils/typed-arrays.ts new file mode 100644 index 0000000000..95a601d225 --- /dev/null +++ b/src/utils/typed-arrays.ts @@ -0,0 +1,21 @@ +/* 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/. */ + +export function toInt32Array( + arr: Array | Int32Array +): Int32Array { + return arr instanceof Int32Array ? arr : new Int32Array(arr); +} + +export function toUint8Array( + arr: Array | Uint8Array +): Uint8Array { + return arr instanceof Uint8Array ? arr : new Uint8Array(arr); +} + +export function toFloat64Array( + arr: Array | Float64Array +): Float64Array { + return arr instanceof Float64Array ? arr : new Float64Array(arr); +}