diff --git a/docs-developer/CHANGELOG-formats.md b/docs-developer/CHANGELOG-formats.md index 7a3c44138a..5210dc1932 100644 --- a/docs-developer/CHANGELOG-formats.md +++ b/docs-developer/CHANGELOG-formats.md @@ -6,6 +6,18 @@ Note that this is not an exhaustive list. Processed profile format upgraders can ## Processed profile format +### Version 67 + +The following raw columns can now optionally be stored as typed arrays, for profiles loaded from [JsonSlabs](https://github.com/mstange/json-slabs/) files (.jslb, .jslb.gz). Regular JS / JSON arrays are still accepted. + +- `thread.samples.time` and `thread.samples.timeDeltas` (`Float64Array`) +- `counter.samples.time` and `counter.samples.timeDeltas` (`Float64Array`) +- `thread.jsAllocations.time` (`Float64Array`) +- `thread.nativeAllocations.time` (`Float64Array`) +- `profile.shared.frameTable.func` (`Int32Array`) +- `profile.shared.frameTable.address` (`Int32Array`, with `-1` as the sentinel for missing addresses) +- `profile.shared.frameTable.inlineDepth` (`Uint8Array`) + ### Version 66 The `prefix` column of `profile.shared.stackTable` was replaced with a `prefixOffset` column, to improve compressibility. diff --git a/src/app-logic/constants.ts b/src/app-logic/constants.ts index 2979163779..ebd546bdca 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 = 66; +export const PROCESSED_PROFILE_VERSION = 67; // 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/combined-cpu.ts b/src/profile-logic/combined-cpu.ts index a7a7efa163..6966e779f3 100644 --- a/src/profile-logic/combined-cpu.ts +++ b/src/profile-logic/combined-cpu.ts @@ -2,14 +2,14 @@ * 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 type { SamplesTable } from 'firefox-profiler/types'; +import type { Milliseconds, SamplesTable } from 'firefox-profiler/types'; import { bisectionLeft } from '../utils/bisect'; /** * Represents CPU usage over time for a single thread. */ export type CpuRatioTimeSeries = { - time: number[]; + time: Float64Array; cpuRatio: Float64Array; maxCpuRatio: number; length: number; @@ -87,7 +87,7 @@ export function combineCPUDataFromThreads( } const cursors = new Array(threadsWithCPU.length).fill(0); - const combinedTime: number[] = []; + const combinedTime: Milliseconds[] = []; const combinedCPURatio: number[] = []; let combinedMaxCpuRatio = 0; @@ -126,7 +126,7 @@ export function combineCPUDataFromThreads( } return { - time: combinedTime, + time: Float64Array.from(combinedTime), cpuRatio: Float64Array.from(combinedCPURatio), maxCpuRatio: combinedMaxCpuRatio, length: combinedTime.length, diff --git a/src/profile-logic/cpu.ts b/src/profile-logic/cpu.ts index 4ed904bc5c..4048a0d0de 100644 --- a/src/profile-logic/cpu.ts +++ b/src/profile-logic/cpu.ts @@ -103,7 +103,7 @@ export function computeReferenceCPUDeltaPerMs(profile: Profile): number { */ export function computeThreadCPUPercent( threadCPUDelta: Array, - timeDeltas: number[], + timeDeltas: number[] | Float64Array, referenceCPUDeltaPerMs: number ): Uint8Array { const threadCPUPercent: Uint8Array = new Uint8Array( diff --git a/src/profile-logic/data-structures.ts b/src/profile-logic/data-structures.ts index 476745de1a..0ba59c11a6 100644 --- a/src/profile-logic/data-structures.ts +++ b/src/profile-logic/data-structures.ts @@ -11,13 +11,10 @@ import type { RawProfileSharedData, RawThread, RawSamplesTable, - FrameTable, + RawFrameTable, RawStackTable, FuncTable, RawMarkerTable, - JsAllocationsTable, - UnbalancedNativeAllocationsTable, - BalancedNativeAllocationsTable, ResourceTable, NativeSymbolTable, Profile, @@ -28,14 +25,100 @@ import type { SourceTable, SourceLocationTable, IndexIntoFrameTable, + IndexIntoFuncTable, IndexIntoStackTable, + IndexIntoCategoryList, + IndexIntoSubcategoryListForCategory, + IndexIntoNativeSymbolTable, + IndexIntoSourceLocationTable, + InnerWindowID, + Address, + Bytes, + Milliseconds, + Tid, + WeightType, } from 'firefox-profiler/types'; +/** + * Builder-variants of various tables. The columns here use plain + * arrays so that elements can be added one-by-one by pushing to + * the column arrays. + * + * The "raw" variants of these arrays (i.e. what's stored in the + * profile files) may be using typed arrays for some of the columns, + * and you can't push to a typed array. + */ +export type RawSamplesTableBuilder = { + responsiveness?: Array; + eventDelay?: Array; + stack: Array; + time?: Milliseconds[]; + timeDeltas?: Milliseconds[]; + argumentValues?: Array; + weight: null | number[]; + weightType: WeightType; + threadCPUDelta?: Array; + threadId?: Tid[]; + length: number; +}; + +export type RawJsAllocationsTableBuilder = { + time: Milliseconds[]; + className: string[]; + typeName: string[]; + coarseType: string[]; + weight: Bytes[]; + weightType: 'bytes'; + inNursery: boolean[]; + stack: Array; + length: number; +}; + +export type RawUnbalancedNativeAllocationsTableBuilder = { + time: Milliseconds[]; + weight: Bytes[]; + weightType: 'bytes'; + stack: Array; + argumentValues?: Array; + length: number; +}; + +export type RawBalancedNativeAllocationsTableBuilder = { + time: Milliseconds[]; + weight: Bytes[]; + weightType: 'bytes'; + stack: Array; + argumentValues?: Array; + memoryAddress: number[]; + threadId: number[]; + length: number; +}; + +export type RawFrameTableBuilder = { + address: Array
; + inlineDepth: number[]; + category: (IndexIntoCategoryList | null)[]; + subcategory: (IndexIntoSubcategoryListForCategory | null)[]; + func: IndexIntoFuncTable[]; + nativeSymbol: (IndexIntoNativeSymbolTable | null)[]; + innerWindowID: (InnerWindowID | null)[]; + line: (number | null)[]; + column: (number | null)[]; + originalLocation: Array; + length: number; +}; + +export type RawStackTableBuilder = { + frame: IndexIntoFrameTable[]; + prefix: Array; + length: number; +}; + /** * This module collects all of the creation of new empty profile data structures. */ -export function getEmptySamplesTable(): RawSamplesTable { +export function getRawSamplesTableBuilder(): RawSamplesTableBuilder { return { // Important! // If modifying this structure, please update all callers of this function to ensure @@ -49,12 +132,6 @@ export function getEmptySamplesTable(): RawSamplesTable { }; } -export type RawStackTableBuilder = { - frame: IndexIntoFrameTable[]; - prefix: Array; - length: number; -}; - export function getRawStackTableBuilder(): RawStackTableBuilder { return { // Important! @@ -67,6 +144,39 @@ export function getRawStackTableBuilder(): RawStackTableBuilder { }; } +/** + * Return a `RawSamplesTableBuilder` view of an existing samples table. If the + * table's time / timeDeltas columns are already plain arrays, they are aliased + * through so that in-place mutations on the builder are visible via the + * returned reference. If they are typed arrays (only produced by the JSLB + * loader), they are copied into plain arrays. + * + * The returned builder shares object identity with `existing` in the common + * (plain-array) case, so callers do not need to reassign it back onto the + * thread. In the typed-array case a new object is returned; callers should + * reassign it (`thread.samples = builder`) to preserve mutations. + */ +export function getRawSamplesTableBuilderFromExisting( + existing: RawSamplesTable +): RawSamplesTableBuilder { + const time = existing.time; + const timeDeltas = existing.timeDeltas; + if ( + (time === undefined || Array.isArray(time)) && + (timeDeltas === undefined || Array.isArray(timeDeltas)) + ) { + return existing as RawSamplesTableBuilder; + } + return { + ...existing, + time: time === undefined || Array.isArray(time) ? time : Array.from(time), + timeDeltas: + timeDeltas === undefined || Array.isArray(timeDeltas) + ? timeDeltas + : Array.from(timeDeltas), + }; +} + export function getRawStackTableBuilderWithExistingContents( existing: RawStackTable ): RawStackTableBuilder { @@ -103,7 +213,7 @@ export function finishRawStackTableBuilder( * eventDelay is a new field and it replaced responsiveness. We should still * account for older profiles and use both of the flavors if needed. */ -export function getEmptySamplesTableWithEventDelay(): RawSamplesTable { +export function getRawSamplesTableBuilderWithEventDelay(): RawSamplesTableBuilder { return { // Important! // If modifying this structure, please update all callers of this function to ensure @@ -118,7 +228,7 @@ export function getEmptySamplesTableWithEventDelay(): RawSamplesTable { }; } -export function getEmptyFrameTable(): FrameTable { +export function getRawFrameTableBuilder(): RawFrameTableBuilder { return { // Important! // If modifying this structure, please update all callers of this function to ensure @@ -138,17 +248,19 @@ export function getEmptyFrameTable(): FrameTable { }; } -export function shallowCloneFrameTable(frameTable: FrameTable): FrameTable { +export function getRawFrameTableBuilderWithExistingContents( + frameTable: RawFrameTable +): RawFrameTableBuilder { return { // Important! // If modifying this structure, please update all callers of this function to ensure // that they are pushing on correctly to the data structure. These pushes may not // be caught by the type system. - address: frameTable.address.slice(), - inlineDepth: frameTable.inlineDepth.slice(), + address: Array.from(frameTable.address), + inlineDepth: Array.from(frameTable.inlineDepth), category: frameTable.category.slice(), subcategory: frameTable.subcategory.slice(), - func: frameTable.func.slice(), + func: Array.from(frameTable.func), nativeSymbol: frameTable.nativeSymbol.slice(), innerWindowID: frameTable.innerWindowID.slice(), line: frameTable.line.slice(), @@ -274,7 +386,7 @@ export function getEmptyRawMarkerTable(): RawMarkerTable { }; } -export function getEmptyJsAllocationsTable(): JsAllocationsTable { +export function getEmptyRawJsAllocationsTable(): RawJsAllocationsTableBuilder { // Important! // If modifying this structure, please update all callers of this function to ensure // that they are pushing on correctly to the data structure. These pushes may not @@ -296,7 +408,7 @@ export function getEmptyJsAllocationsTable(): JsAllocationsTable { * The native allocation tables come in two varieties. Get one of the members of the * union. */ -export function getEmptyUnbalancedNativeAllocationsTable(): UnbalancedNativeAllocationsTable { +export function getEmptyRawUnbalancedNativeAllocationsTable(): RawUnbalancedNativeAllocationsTableBuilder { // Important! // If modifying this structure, please update all callers of this function to ensure // that they are pushing on correctly to the data structure. These pushes may not @@ -314,7 +426,7 @@ export function getEmptyUnbalancedNativeAllocationsTable(): UnbalancedNativeAllo * The native allocation tables come in two varieties. Get one of the members of the * union. */ -export function getEmptyBalancedNativeAllocationsTable(): BalancedNativeAllocationsTable { +export function getEmptyRawBalancedNativeAllocationsTable(): RawBalancedNativeAllocationsTableBuilder { // Important! // If modifying this structure, please update all callers of this function to ensure // that they are pushing on correctly to the data structure. These pushes may not @@ -420,7 +532,7 @@ export function getEmptyThread(overrides?: Partial): RawThread { pid: '0', tid: 0, // Creating samples with event delay since it's the new samples table. - samples: getEmptySamplesTableWithEventDelay(), + samples: getRawSamplesTableBuilderWithEventDelay(), markers: getEmptyRawMarkerTable(), }; @@ -433,7 +545,7 @@ export function getEmptyThread(overrides?: Partial): RawThread { export function getEmptySharedData(): RawProfileSharedData { return { stackTable: finishRawStackTableBuilder(getRawStackTableBuilder()), - frameTable: getEmptyFrameTable(), + frameTable: getRawFrameTableBuilder(), funcTable: getEmptyFuncTable(), resourceTable: getEmptyResourceTable(), nativeSymbols: getEmptyNativeSymbolTable(), diff --git a/src/profile-logic/global-data-collector.ts b/src/profile-logic/global-data-collector.ts index c52e170293..49987a78cf 100644 --- a/src/profile-logic/global-data-collector.ts +++ b/src/profile-logic/global-data-collector.ts @@ -5,7 +5,7 @@ import { StringTable } from '../utils/string-table'; import { finishRawStackTableBuilder, - getEmptyFrameTable, + getRawFrameTableBuilder, getEmptyFuncTable, getEmptyNativeSymbolTable, getEmptyResourceTable, @@ -22,7 +22,6 @@ import type { IndexIntoSourceTable, RawProfileSharedData, SourceTable, - FrameTable, FuncTable, ResourceTable, NativeSymbolTable, @@ -34,7 +33,10 @@ import type { Bytes, } from 'firefox-profiler/types'; import { ResourceType } from 'firefox-profiler/types'; -import type { RawStackTableBuilder } from './data-structures'; +import type { + RawFrameTableBuilder, + RawStackTableBuilder, +} from './data-structures'; /** * GlobalDataCollector collects data which is global in the processed profile @@ -50,7 +52,7 @@ export class GlobalDataCollector { _stringArray: string[] = []; _stringTable: StringTable = StringTable.withBackingArray(this._stringArray); _sources: SourceTable = getEmptySourceTable(); - _frameTable: FrameTable = getEmptyFrameTable(); + _frameTable: RawFrameTableBuilder = getRawFrameTableBuilder(); _stackTableBuilder: RawStackTableBuilder = getRawStackTableBuilder(); _funcTable: FuncTable = getEmptyFuncTable(); _resourceTable: ResourceTable = getEmptyResourceTable(); @@ -299,7 +301,7 @@ export class GlobalDataCollector { return this._stringTable; } - getFrameTable(): FrameTable { + getFrameTable(): RawFrameTableBuilder { return this._frameTable; } diff --git a/src/profile-logic/import/chrome.ts b/src/profile-logic/import/chrome.ts index fefc76f986..193ebb9bf1 100644 --- a/src/profile-logic/import/chrome.ts +++ b/src/profile-logic/import/chrome.ts @@ -12,6 +12,8 @@ import type { import { getEmptyProfile, getEmptyThread, + getRawSamplesTableBuilderWithEventDelay, + type RawSamplesTableBuilder, type RawStackTableBuilder, } from '../data-structures'; import type { StringTable } from '../../utils/string-table'; @@ -270,6 +272,7 @@ export function attemptToConvertChromeProfile( type ThreadInfo = { thread: RawThread; + samples: RawSamplesTableBuilder; nodeIdToStackId: Map; lastSeenTime: number; lastSampledTime: number; @@ -316,7 +319,8 @@ function getThreadInfo( if (cachedThreadInfo) { return cachedThreadInfo; } - const thread = getEmptyThread(); + const samples = getRawSamplesTableBuilderWithEventDelay(); + const thread = getEmptyThread({ samples }); thread.pid = `${chunk.pid}`; // It looks like the TID information in Chrome's data isn't the system's TID // but some internal values only unique for a pid. Therefore let's generate a @@ -401,6 +405,7 @@ function getThreadInfo( const threadInfo: ThreadInfo = { thread, + samples, nodeIdToStackId, lastSeenTime: chunk.ts / 1000, lastSampledTime: 0, @@ -531,7 +536,7 @@ async function processTracingEvents( profile, profileEvent ); - const { thread, nodeIdToStackId } = threadInfo; + const { samples: samplesTable, nodeIdToStackId } = threadInfo; let profileChunks: any[] = []; if (profileEvent.name === 'Profile') { @@ -565,8 +570,6 @@ async function processTracingEvents( continue; } - const { samples: samplesTable } = thread; - if (nodes) { const parentMap = new Map(); for (const node of nodes) { diff --git a/src/profile-logic/import/dhat.ts b/src/profile-logic/import/dhat.ts index 6f883c2e29..c04e25d808 100644 --- a/src/profile-logic/import/dhat.ts +++ b/src/profile-logic/import/dhat.ts @@ -12,7 +12,7 @@ import type { import { getEmptyProfile, getEmptyThread, - getEmptyUnbalancedNativeAllocationsTable, + getEmptyRawUnbalancedNativeAllocationsTable, } from 'firefox-profiler/profile-logic/data-structures'; import { GlobalDataCollector } from 'firefox-profiler/profile-logic/global-data-collector'; @@ -154,7 +154,7 @@ type WriteCount = number; /** * The dhat convertor converts to the processed profile format, rather than to the Gecko - * format, as it needs the UnbalancedNativeAllocationsTable type, which is unavailable + * format, as it needs the RawUnbalancedNativeAllocationsTable type, which is unavailable * in the Gecko format. In the Gecko format, that data comes in the form of markers, which * would be awkard to target. */ @@ -187,7 +187,7 @@ export function attemptToConvertDhat(json: unknown): Profile | null { const frameTable = globalDataCollector.getFrameTable(); const stringTable = globalDataCollector.getStringTable(); - const allocationsTable = getEmptyUnbalancedNativeAllocationsTable(); + const allocationsTable = getEmptyRawUnbalancedNativeAllocationsTable(); // dhat profiles do no support categories. Fill the category and subcategory information // with 0s. diff --git a/src/profile-logic/import/flame-graph.ts b/src/profile-logic/import/flame-graph.ts index b468d4d008..a32680ac3e 100644 --- a/src/profile-logic/import/flame-graph.ts +++ b/src/profile-logic/import/flame-graph.ts @@ -13,6 +13,7 @@ import type { import { getEmptyProfile, getEmptyThread, + getRawSamplesTableBuilderWithEventDelay, } from 'firefox-profiler/profile-logic/data-structures'; import { GlobalDataCollector } from 'firefox-profiler/profile-logic/global-data-collector'; import { ensureExists } from 'firefox-profiler/utils/types'; @@ -61,7 +62,8 @@ export function convertFlameGraphProfile(profileText: string): Profile { const frameTable = globalDataCollector.getFrameTable(); const stackTable = globalDataCollector.getStackTableBuilder(); - const { samples } = thread; + const samples = getRawSamplesTableBuilderWithEventDelay(); + thread.samples = samples; // Maps to deduplicate stacks, frames, and functions. const stackMap = new Map(); diff --git a/src/profile-logic/import/simpleperf.ts b/src/profile-logic/import/simpleperf.ts index 83c38d8929..44bd33c2bd 100644 --- a/src/profile-logic/import/simpleperf.ts +++ b/src/profile-logic/import/simpleperf.ts @@ -5,7 +5,7 @@ import type { Milliseconds } from 'firefox-profiler/types/units'; import type { CategoryList, CategoryColor, - FrameTable, + RawFrameTable, FuncTable, IndexIntoCategoryList, IndexIntoFrameTable, @@ -14,7 +14,6 @@ import type { IndexIntoStackTable, ProfileMeta, ResourceTable, - RawSamplesTable, Profile, RawProfileSharedData, RawThread, @@ -23,11 +22,13 @@ import type { import { getEmptyFuncTable, getEmptyResourceTable, - getEmptyFrameTable, + getRawFrameTableBuilder, getRawStackTableBuilder, finishRawStackTableBuilder, + type RawFrameTableBuilder, type RawStackTableBuilder, - getEmptySamplesTable, + getRawSamplesTableBuilder, + type RawSamplesTableBuilder, getEmptyRawMarkerTable, getEmptyNativeSymbolTable, getEmptySourceTable, @@ -150,14 +151,14 @@ class FirefoxFuncTable { class FirefoxFrameTable { strings: StringTable; - frameTable: FrameTable = getEmptyFrameTable(); + frameTable: RawFrameTableBuilder = getRawFrameTableBuilder(); frameMap: Map = new Map(); constructor(strings: StringTable) { this.strings = strings; } - toJson(): FrameTable { + toJson(): RawFrameTable { return this.frameTable; } @@ -252,7 +253,7 @@ class FirefoxThread { strings: StringTable; - sampleTable: RawSamplesTable = getEmptySamplesTable(); + sampleTable: RawSamplesTableBuilder = getRawSamplesTableBuilder(); stackTable: FirefoxSampleTable; frameTable: FirefoxFrameTable; diff --git a/src/profile-logic/insert-stack-labels.ts b/src/profile-logic/insert-stack-labels.ts index 5d281bf46a..2f4d3f7cf6 100644 --- a/src/profile-logic/insert-stack-labels.ts +++ b/src/profile-logic/insert-stack-labels.ts @@ -10,7 +10,7 @@ import type { Category, } from '../types/profile'; import { - shallowCloneFrameTable, + getRawFrameTableBuilderWithExistingContents, shallowCloneFuncTable, } from 'firefox-profiler/profile-logic/data-structures'; import { StringTable } from 'firefox-profiler/utils/string-table'; @@ -105,7 +105,7 @@ export function insertStackLabels( sources, stringArray, } = profile.shared; - const frameTable = shallowCloneFrameTable(oldFrameTable); + const frameTable = getRawFrameTableBuilderWithExistingContents(oldFrameTable); const funcTable = shallowCloneFuncTable(oldFuncTable); const stringTable = StringTable.withBackingArray(stringArray); diff --git a/src/profile-logic/js-tracer.ts b/src/profile-logic/js-tracer.ts index e4664aee95..68eb8b2486 100644 --- a/src/profile-logic/js-tracer.ts +++ b/src/profile-logic/js-tracer.ts @@ -2,10 +2,11 @@ * 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 { - getEmptySamplesTableWithEventDelay, + getRawSamplesTableBuilderWithEventDelay, getEmptyRawMarkerTable, finishRawStackTableBuilder, getRawStackTableBuilderWithExistingContents, + getRawFrameTableBuilderWithExistingContents, } from './data-structures'; import { StringTable } from '../utils/string-table'; import { ensureExists } from '../utils/types'; @@ -502,7 +503,7 @@ export function convertJsTracerToThreadWithoutSamples( stackMap: Map; } { const samples: RawSamplesTable = { - ...getEmptySamplesTableWithEventDelay(), + ...getRawSamplesTableBuilderWithEventDelay(), weight: [], weightType: 'tracing-ms', }; @@ -514,7 +515,10 @@ export function convertJsTracerToThreadWithoutSamples( samples, }; - const { funcTable, frameTable } = shared; + const { funcTable } = shared; + const frameTable = getRawFrameTableBuilderWithExistingContents( + shared.frameTable + ); const stackTable = getRawStackTableBuilderWithExistingContents( shared.stackTable ); @@ -625,8 +629,9 @@ export function convertJsTracerToThreadWithoutSamples( unmatchedEventEnds[unmatchedIndex] = end; } - // Write the augmented stackTable back to the shared data. + // Write the augmented stackTable and frameTable back to the shared data. shared.stackTable = finishRawStackTableBuilder(stackTable); + shared.frameTable = frameTable; return { thread, stackMap }; } @@ -798,7 +803,7 @@ export function getSelfTimeSamplesFromJsTracer( const isNearlyEqual = (a: number, b: number) => Math.abs(a - b) < epsilon; // Each event type will have it's own timing information, later collapse these into // a single array. - const samples = getEmptySamplesTableWithEventDelay(); + const samples = getRawSamplesTableBuilderWithEventDelay(); const sampleWeights: number[] = []; samples.weight = sampleWeights; diff --git a/src/profile-logic/merge-compare.ts b/src/profile-logic/merge-compare.ts index 3e6f600dcf..50d751cbad 100644 --- a/src/profile-logic/merge-compare.ts +++ b/src/profile-logic/merge-compare.ts @@ -11,12 +11,12 @@ import { getEmptyProfile, getEmptyResourceTable, getEmptyNativeSymbolTable, - getEmptyFrameTable, + getRawFrameTableBuilder, getEmptyFuncTable, getRawStackTableBuilder, finishRawStackTableBuilder, getEmptyRawMarkerTable, - getEmptySamplesTableWithEventDelay, + getRawSamplesTableBuilderWithEventDelay, shallowCloneRawMarkerTable, getEmptySourceTable, } from './data-structures'; @@ -51,7 +51,7 @@ import type { IndexIntoSourceTable, IndexIntoSourceLocationTable, FuncTable, - FrameTable, + RawFrameTable, Lib, NativeSymbolTable, ResourceTable, @@ -1057,9 +1057,9 @@ function mergeFrameTables( translationMapsForNativeSymbols: TranslationMapForNativeSymbols[], translationMapsForOriginalLocation: TranslationMapForOriginalLocation[], translationMapsForCategories: TranslationMapForCategories[] -): { frameTable: FrameTable; translationMaps: TranslationMapForFrames[] } { +): { frameTable: RawFrameTable; translationMaps: TranslationMapForFrames[] } { const translationMaps: TranslationMapForFrames[] = []; - const newFrameTable = getEmptyFrameTable(); + const newFrameTable = getRawFrameTableBuilder(); profiles.forEach((profile, profileIndex) => { const { frameTable } = profile.shared; @@ -1184,7 +1184,7 @@ function combineSamplesDiffing( const newWeight: number[] = []; const newThreadId: Tid[] = []; const newSamples = { - ...getEmptySamplesTableWithEventDelay(), + ...getRawSamplesTableBuilderWithEventDelay(), weight: newWeight, threadId: newThreadId, }; @@ -1352,9 +1352,8 @@ function combineSamplesForMerging(threads: RawThread[]): RawSamplesTable { const samplesPerThread: RawSamplesTable[] = threads.map( (thread) => thread.samples ); - const sampleTimesPerThread: Milliseconds[][] = samplesPerThread.map( - computeTimeColumnForRawSamplesTable - ); + const sampleTimesPerThread: Float64Array[] = + samplesPerThread.map(computeTimeColumnForRawSamplesTable); // This is the array that holds the latest processed sample index for each // thread's samplesTable. const nextSampleIndexPerThread: number[] = Array( @@ -1365,7 +1364,7 @@ function combineSamplesForMerging(threads: RawThread[]): RawSamplesTable { const newThreadId: Tid[] = []; // Creating a new empty samples table to fill. const newSamples = { - ...getEmptySamplesTableWithEventDelay(), + ...getRawSamplesTableBuilderWithEventDelay(), threadId: newThreadId, }; diff --git a/src/profile-logic/process-profile.ts b/src/profile-logic/process-profile.ts index 2f38fcb4a8..9a640e5b1c 100644 --- a/src/profile-logic/process-profile.ts +++ b/src/profile-logic/process-profile.ts @@ -14,8 +14,9 @@ import { AddressLocator } from './address-locator'; import { getEmptyExtensions, getEmptyRawMarkerTable, - getEmptyJsAllocationsTable, - getEmptyUnbalancedNativeAllocationsTable, + getEmptyRawJsAllocationsTable, + getEmptyRawUnbalancedNativeAllocationsTable, + type RawFrameTableBuilder, type RawStackTableBuilder, } from './data-structures'; import { immutableUpdate, ensureExists } from '../utils/types'; @@ -52,7 +53,6 @@ import type { RawThread, RawCounter, ExtensionTable, - FrameTable, RawCounterSamplesTable, RawSamplesTable, RawMarkerTable, @@ -61,9 +61,9 @@ import type { IndexIntoFuncTable, IndexIntoStringTable, JsTracerTable, - JsAllocationsTable, + RawJsAllocationsTable, ProfilerOverhead, - NativeAllocationsTable, + RawNativeAllocationsTable, Milliseconds, Microseconds, Address, @@ -521,7 +521,7 @@ function _extractUnknownFunctionType( */ function _processFrameTable( geckoFrameStruct: GeckoFrameStruct, - sharedFrameTable: FrameTable, + sharedFrameTable: RawFrameTableBuilder, frameFuncs: IndexIntoFuncTable[], frameAddresses: (Address | null)[] ): IndexIntoFrameTable { @@ -631,8 +631,8 @@ function _convertPayloadStackToIndex( * Process the markers. * Convert stacks to causes. * Process GC markers. - * Extract JS allocations into the JsAllocationsTable. - * Extract Native allocations into the NativeAllocationsTable. + * Extract JS allocations into the RawJsAllocationsTable. + * Extract Native allocations into the RawNativeAllocationsTable. */ function _processMarkers( geckoMarkers: GeckoMarkerStruct, @@ -642,13 +642,13 @@ function _processMarkers( stackIndexOffset: IndexIntoStackTable ): { markers: RawMarkerTable; - jsAllocations: JsAllocationsTable | null; - nativeAllocations: NativeAllocationsTable | null; + jsAllocations: RawJsAllocationsTable | null; + nativeAllocations: RawNativeAllocationsTable | null; } { const markers = getEmptyRawMarkerTable(); - const jsAllocations = getEmptyJsAllocationsTable(); + const jsAllocations = getEmptyRawJsAllocationsTable(); const inProgressNativeAllocations = - getEmptyUnbalancedNativeAllocationsTable(); + getEmptyRawUnbalancedNativeAllocationsTable(); const memoryAddress: number[] = []; const threadId: number[] = []; @@ -1471,10 +1471,9 @@ function _processThread( * has its own timebase, and we don't want to keep converting timestamps when * we deal with the integrated profile. */ -export function adjustTableTimestamps( - table: Table, - delta: Milliseconds -): Table { +export function adjustTableTimestamps< + Table extends { time: Milliseconds[] | Float64Array }, +>(table: Table, delta: Milliseconds): Table { return { ...table, time: table.time.map((time) => time + delta), @@ -1482,7 +1481,7 @@ export function adjustTableTimestamps
( } export function adjustTableTimeDeltas< - Table extends { timeDeltas?: Milliseconds[] }, + Table extends { timeDeltas?: Milliseconds[] | Float64Array }, >(table: Table, delta: Milliseconds): Table { const { timeDeltas } = table; if (timeDeltas === undefined) { diff --git a/src/profile-logic/processed-profile-versioning.ts b/src/profile-logic/processed-profile-versioning.ts index 059fde27e1..ad89075c47 100644 --- a/src/profile-logic/processed-profile-versioning.ts +++ b/src/profile-logic/processed-profile-versioning.ts @@ -3276,6 +3276,18 @@ const _upgraders: { stackTable.prefixOffset = prefixOffset; delete stackTable.prefix; }, + [67]: (_profile: any) => { + // Various raw columns can now optionally be stored as typed arrays: + // - `thread.samples.time` / `timeDeltas` (`Float64Array`) + // - `counter.samples.time` / `timeDeltas` (`Float64Array`) + // - `thread.jsAllocations.time` (`Float64Array`) + // - `thread.nativeAllocations.time` (`Float64Array`) + // - `profile.shared.frameTable.func` (`Int32Array`) + // - `profile.shared.frameTable.address` (`Int32Array`, `-1` sentinel) + // - `profile.shared.frameTable.inlineDepth` (`Uint8Array`) + // Regular JS / JSON arrays are still accepted. All valid v66 profiles + // are valid v67 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 3704452a0c..9adb8404a7 100644 --- a/src/profile-logic/profile-compacting.ts +++ b/src/profile-logic/profile-compacting.ts @@ -12,7 +12,7 @@ import type { RawMarkerTable, IndexIntoStackTable, RawStackTable, - FrameTable, + RawFrameTable, FuncTable, ResourceTable, NativeSymbolTable, @@ -61,7 +61,10 @@ type ColumnDescription = null extends ( | { type: 'NO_REF' }; type TableDescription = { - [K in keyof T as T[K] extends Array | Int32Array + [K in keyof T as T[K] extends + | Array + | Int32Array + | Uint8Array ? K : never]: ColumnDescription; }; @@ -159,12 +162,12 @@ export function computeCompactedProfile( frame: ColDesc.indexRefInt32(tcs.frameTable), prefixOffset: ColDesc.selfPrefixOffset(), }; - const frameTableDesc: TableDescription = { + const frameTableDesc: TableDescription = { address: ColDesc.noRef(), inlineDepth: ColDesc.noRef(), category: ColDesc.noRef(), subcategory: ColDesc.noRef(), - func: ColDesc.indexRef(tcs.funcTable), + func: ColDesc.indexRefInt32(tcs.funcTable), nativeSymbol: ColDesc.indexRefOrNull(tcs.nativeSymbols), innerWindowID: ColDesc.noRef(), line: ColDesc.noRef(), diff --git a/src/profile-logic/profile-data.ts b/src/profile-logic/profile-data.ts index 865efbd41f..266f0586e8 100644 --- a/src/profile-logic/profile-data.ts +++ b/src/profile-logic/profile-data.ts @@ -9,7 +9,7 @@ import { getRawStackTableBuilder, finishRawStackTableBuilder, getEmptyCallNodeTable, - shallowCloneFrameTable, + getRawFrameTableBuilderWithExistingContents, shallowCloneFuncTable, } from './data-structures'; import { @@ -54,6 +54,7 @@ import type { RawStackTable, SampleUnits, StackTable, + RawFrameTable, FrameTable, FuncTable, NativeSymbolTable, @@ -73,6 +74,9 @@ import type { CounterIndex, RawCounterSamplesTable, CounterSamplesTable, + RawJsAllocationsTable, + JsAllocationsTable, + RawNativeAllocationsTable, NativeAllocationsTable, InnerWindowID, BalancedNativeAllocationsTable, @@ -1936,9 +1940,12 @@ function _computeStackMatchesFromFuncMatches( export function computeTimeColumnForRawSamplesTable( samples: RawSamplesTable | RawCounterSamplesTable -): number[] { +): Float64Array { const { time, timeDeltas } = samples; - return time ?? numberSeriesFromDeltas(ensureExists(timeDeltas)); + if (time !== undefined) { + return time instanceof Float64Array ? time : new Float64Array(time); + } + return numberSeriesFromDeltas(ensureExists(timeDeltas)); } export function computeSampleCategoriesAndSubcategories( @@ -2011,7 +2018,10 @@ export function hasUsefulSamples( * This function takes both a SamplesTable and can be used on CounterSamplesTable. */ export function getSampleIndexRangeForSelection( - times: { time: Milliseconds[]; length: number }, + times: { + time: Milliseconds[] | Float64Array; + length: number; + }, rangeStart: number, rangeEnd: number ): [IndexIntoSamplesTable, IndexIntoSamplesTable] { @@ -2019,7 +2029,7 @@ export function getSampleIndexRangeForSelection( } export function getIndexRangeForSelection( - times: Milliseconds[], + times: Milliseconds[] | Float64Array, rangeStart: number, rangeEnd: number ): [IndexIntoSamplesTable, IndexIntoSamplesTable] { @@ -2034,7 +2044,10 @@ export function getIndexRangeForSelection( * sure that some charts will not be cut off at the edges when zoomed in to a range. */ export function getInclusiveSampleIndexRangeForSelection( - table: { time: Milliseconds[]; length: number }, + table: { + time: Milliseconds[] | Float64Array; + length: number; + }, rangeStart: number, rangeEnd: number ): [IndexIntoSamplesTable, IndexIntoSamplesTable] { @@ -2042,7 +2055,7 @@ export function getInclusiveSampleIndexRangeForSelection( } export function getInclusiveIndexRangeForSelection( - times: Milliseconds[], + times: Milliseconds[] | Float64Array, rangeStart: number, rangeEnd: number ): [IndexIntoSamplesTable, IndexIntoSamplesTable] { @@ -2776,6 +2789,48 @@ export function computeSamplesTableFromRawSamplesTable( }; } +export function computeJsAllocationsTableFromRawJsAllocationsTable( + raw: RawJsAllocationsTable +): JsAllocationsTable { + return { + time: Float64Array.from(raw.time), + className: raw.className, + typeName: raw.typeName, + coarseType: raw.coarseType, + weight: raw.weight, + weightType: raw.weightType, + inNursery: raw.inNursery, + stack: raw.stack, + length: raw.length, + }; +} + +export function computeNativeAllocationsTableFromRawNativeAllocationsTable( + raw: RawNativeAllocationsTable +): NativeAllocationsTable { + const time = Float64Array.from(raw.time); + if ('memoryAddress' in raw) { + return { + time, + weight: raw.weight, + weightType: raw.weightType, + stack: raw.stack, + argumentValues: raw.argumentValues, + memoryAddress: raw.memoryAddress, + threadId: raw.threadId, + length: raw.length, + }; + } + return { + time, + weight: raw.weight, + weightType: raw.weightType, + stack: raw.stack, + argumentValues: raw.argumentValues, + length: raw.length, + }; +} + /** * Create the derived Thread. */ @@ -2790,7 +2845,9 @@ export function createThreadFromDerivedTables( stringTable: StringTable, sources: SourceTable, tracedValuesBuffer: ArrayBuffer | undefined, - sourceLocationTable: SourceLocationTable + sourceLocationTable: SourceLocationTable, + jsAllocations: JsAllocationsTable | undefined, + nativeAllocations: NativeAllocationsTable | undefined ): Thread { const { processType, @@ -2807,8 +2864,6 @@ export function createThreadFromDerivedTables( isJsTracer, pid, tid, - jsAllocations, - nativeAllocations, markers, jsTracer, isPrivateBrowsing, @@ -2897,11 +2952,11 @@ export function updateThreadStacksByGeneratingNewStackColumns( newStackTable: StackTable, computeMappedStackColumn: ( oldStack: Array, - sampleTime: Array + sampleTime: Float64Array ) => Array, computeMappedSyncBacktraceStackColumn: ( oldStack: Array, - sampleTime: Array + sampleTime: Float64Array ) => Array, computeMappedMarkerDataColumn: ( markerData: Array @@ -4310,7 +4365,7 @@ export function nudgeReturnAddresses(profile: Profile): Profile { // Create the new frame table. // Frames that were observed both from the instruction pointer and from // stack walking have to be duplicated. - const newFrameTable = shallowCloneFrameTable(frameTable); + const newFrameTable = getRawFrameTableBuilderWithExistingContents(frameTable); // Iterate over all *return address* frames, i.e. all frames that were obtained // by stack walking. for (const [frame, address] of returnAddressFrames) { @@ -4724,9 +4779,39 @@ export function computeTabToThreadIndexesMap( return tabToThreadIndexesMap; } +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); + return { + address, + inlineDepth, + category: rawFrameTable.category, + subcategory: rawFrameTable.subcategory, + func, + nativeSymbol: rawFrameTable.nativeSymbol, + innerWindowID: rawFrameTable.innerWindowID, + line: rawFrameTable.line, + column: rawFrameTable.column, + originalLocation: rawFrameTable.originalLocation, + length: rawFrameTable.length, + }; +} + export function computeStackTableFromRawStackTable( rawStackTable: RawStackTable, - frameTable: FrameTable, + frameTable: RawFrameTable, categories: CategoryList | undefined, defaultCategory: IndexIntoCategoryList ): StackTable { diff --git a/src/profile-logic/source-map-symbolication.ts b/src/profile-logic/source-map-symbolication.ts index f4d5ebe4c6..0dfb0e438c 100644 --- a/src/profile-logic/source-map-symbolication.ts +++ b/src/profile-logic/source-map-symbolication.ts @@ -68,7 +68,7 @@ import { shallowCloneFuncTable, - shallowCloneFrameTable, + getRawFrameTableBuilderWithExistingContents, shallowCloneSourceLocationTable, } from './data-structures'; import { StringTable } from '../utils/string-table'; @@ -92,7 +92,7 @@ import type { IndexIntoFuncTable, IndexIntoFrameTable, FuncTable, - FrameTable, + RawFrameTable, RawProfileSharedData, SourceLocationTable, SourceTable, @@ -132,7 +132,7 @@ type ParsedSource = { // main thread in applySourceMapSymbolicationResponse, which builds new // tables from the current shared state at apply time. export type SourceMapSymbolicationInput = { - frameTable: FrameTable; + frameTable: RawFrameTable; funcTable: FuncTable; sourceLocationTable: SourceLocationTable; sources: SourceTable; @@ -185,7 +185,7 @@ export function symbolicateWithSourceMaps( * candidates for line/column remapping. */ function _identifyToSymbolicate( - frameTable: FrameTable, + frameTable: RawFrameTable, funcTable: FuncTable, sources: SourceTable ): { @@ -949,7 +949,7 @@ export function applySourceMapSymbolicationResponse( response: SourceMapSymbolicationResponse ): { newFuncTable: FuncTable; - newFrameTable: FrameTable; + newFrameTable: RawFrameTable; newSourceLocationTable: SourceLocationTable; newSources: SourceTable; newStringArray: string[]; @@ -958,7 +958,7 @@ export function applySourceMapSymbolicationResponse( shared; const newFuncTable = shallowCloneFuncTable(funcTable); - const newFrameTable = shallowCloneFrameTable(frameTable); + const newFrameTable = getRawFrameTableBuilderWithExistingContents(frameTable); const newSourceLocationTable = shallowCloneSourceLocationTable(sourceLocationTable); const newSources = _shallowCloneSourceTable(sources); diff --git a/src/profile-logic/symbolication.ts b/src/profile-logic/symbolication.ts index 52a53d7c1a..104bd9b3ee 100644 --- a/src/profile-logic/symbolication.ts +++ b/src/profile-logic/symbolication.ts @@ -6,7 +6,7 @@ import { finishRawStackTableBuilder, shallowCloneFuncTable, shallowCloneNativeSymbolTable, - shallowCloneFrameTable, + getRawFrameTableBuilderWithExistingContents, } from './data-structures'; import { SymbolsNotFoundError } from './errors'; @@ -716,7 +716,7 @@ function _partiallyApplySymbolicationStep( // Integrate the new native symbol column into the frame table and make a // copy so that we can add new frames below. - const frameTable = shallowCloneFrameTable({ + const frameTable = getRawFrameTableBuilderWithExistingContents({ ...oldFrameTable, nativeSymbol: newFrameTableNativeSymbolsColumn, }); diff --git a/src/profile-logic/transforms.ts b/src/profile-logic/transforms.ts index 0dd6f2b7ff..57a96ed9d6 100644 --- a/src/profile-logic/transforms.ts +++ b/src/profile-logic/transforms.ts @@ -45,7 +45,6 @@ import type { MarkerIndex, MarkerSchemaByName, CategoryList, - Milliseconds, ProfileIndexTranslationMaps, } from 'firefox-profiler/types'; import type { CallNodeInfo } from 'firefox-profiler/profile-logic/call-node-info'; @@ -1876,7 +1875,7 @@ export function filterSamples( function computeFilteredStackColumn( originalStackColumn: Array, - timeColumn: Milliseconds[] + timeColumn: Float64Array ): Array { const newStackColumn = originalStackColumn.slice(); let sampleIndex = 0; diff --git a/src/profile-query/cpu-activity.ts b/src/profile-query/cpu-activity.ts index 79351ac47b..d1f6b0666d 100644 --- a/src/profile-query/cpu-activity.ts +++ b/src/profile-query/cpu-activity.ts @@ -69,7 +69,7 @@ function collectSliceSubtree( childrenStartPerParent: Array, interestingSliceIndexes: Set, nestingDepth: number, - time: number[], + time: Float64Array, result: CpuActivityEntry[], tsManager: TimestampManager ) { diff --git a/src/selectors/per-thread/thread.tsx b/src/selectors/per-thread/thread.tsx index 54966a55c7..f83961bbbe 100644 --- a/src/selectors/per-thread/thread.tsx +++ b/src/selectors/per-thread/thread.tsx @@ -27,7 +27,9 @@ import type { JsTracerTable, RawSamplesTable, SamplesTable, + RawNativeAllocationsTable, NativeAllocationsTable, + RawJsAllocationsTable, JsAllocationsTable, SamplesLikeTable, Selector, @@ -143,11 +145,25 @@ export function getBasicThreadSelectorsPerThread( ) : null ); - const getNativeAllocations: Selector = ( + const getRawNativeAllocations: Selector = ( state ) => getRawThread(state).nativeAllocations; - const getJsAllocations: Selector = (state) => + const getRawJsAllocations: Selector = (state) => getRawThread(state).jsAllocations; + const getNativeAllocations: Selector = + createSelector(getRawNativeAllocations, (raw) => + raw + ? ProfileData.computeNativeAllocationsTableFromRawNativeAllocationsTable( + raw + ) + : undefined + ); + const getJsAllocations: Selector = + createSelector(getRawJsAllocations, (raw) => + raw + ? ProfileData.computeJsAllocationsTableFromRawJsAllocationsTable(raw) + : undefined + ); const getThreadRange: Selector = (state) => // This function is already memoized in profile-data.js, so we don't need to // memoize it here with `createSelector`. @@ -183,8 +199,7 @@ export function getBasicThreadSelectorsPerThread( getRawThread, getSamplesTable, ProfileSelectors.getStackTable, - (state: State) => - ProfileSelectors.getRawProfileSharedData(state).frameTable, + ProfileSelectors.getFrameTable, ProfileSelectors.getFunctionsReservedFuncTable, (state: State) => ProfileSelectors.getRawProfileSharedData(state).nativeSymbols, @@ -195,6 +210,8 @@ export function getBasicThreadSelectorsPerThread( getTracedValuesBuffer, (state: State) => ProfileSelectors.getRawProfileSharedData(state).sourceLocationTable, + getJsAllocations, + getNativeAllocations, ProfileData.createThreadFromDerivedTables ); diff --git a/src/selectors/profile.ts b/src/selectors/profile.ts index 896ca26b27..9a2df2e36a 100644 --- a/src/selectors/profile.ts +++ b/src/selectors/profile.ts @@ -20,6 +20,7 @@ import { computeInnerWindowIDToTabMap, computeTabToThreadIndexesMap, computeStackTableFromRawStackTable, + computeFrameTableFromRawFrameTable, reserveFunctionsForCollapsedResources, computeSamplesTableFromRawSamplesTable, } from '../profile-logic/profile-data'; @@ -39,6 +40,7 @@ import type { Profile, RawProfileSharedData, StackTable, + FrameTable, CategoryList, IndexIntoCategoryList, RawThread, @@ -283,6 +285,11 @@ export const getStackTable: Selector = createSelector( computeStackTableFromRawStackTable ); +export const getFrameTable: Selector = createSelector( + (state: State) => getRawProfileSharedData(state).frameTable, + computeFrameTableFromRawFrameTable +); + export const getFuncTableWithReservedFunctions: Selector = createSelector( (state: State) => getRawProfileSharedData(state).funcTable, diff --git a/src/test/components/EmptyThreadIndicator.test.tsx b/src/test/components/EmptyThreadIndicator.test.tsx index d835738784..ca5b77e044 100644 --- a/src/test/components/EmptyThreadIndicator.test.tsx +++ b/src/test/components/EmptyThreadIndicator.test.tsx @@ -29,7 +29,7 @@ describe('EmptyThreadIndicator', function () { thread.processShutdownTime = 10; thread.registerTime = 3; thread.unregisterTime = 9; - thread.samples.time = [5, 6, 7]; + thread.samples.time = Float64Array.of(5, 6, 7); // Make it really easy to generate the component's props. function propsFromViewportRange(viewport: StartEndRange) { diff --git a/src/test/fixtures/profiles/call-nodes.ts b/src/test/fixtures/profiles/call-nodes.ts index 0ffed2dd52..8692daf1ab 100644 --- a/src/test/fixtures/profiles/call-nodes.ts +++ b/src/test/fixtures/profiles/call-nodes.ts @@ -1,7 +1,7 @@ /* 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 type { FuncTable, FrameTable, Profile } from 'firefox-profiler/types'; +import type { FuncTable, RawFrameTable, Profile } from 'firefox-profiler/types'; import { getEmptyThread, @@ -74,7 +74,7 @@ export default function getProfile(): Profile { funcFFrame, ] = frameFuncs.map((_, i) => i); - const frameTable: FrameTable = { + const frameTable: RawFrameTable = { func: frameFuncs.map((stringIndex) => funcTable.name.indexOf(stringIndex)), address: Array(frameFuncs.length).fill(-1), inlineDepth: Array(frameFuncs.length).fill(0), diff --git a/src/test/fixtures/profiles/processed-profile.ts b/src/test/fixtures/profiles/processed-profile.ts index fea127bc5c..384bc94e68 100644 --- a/src/test/fixtures/profiles/processed-profile.ts +++ b/src/test/fixtures/profiles/processed-profile.ts @@ -5,11 +5,13 @@ import { getEmptyProfile, getEmptyThread, getEmptyJsTracerTable, - getEmptyJsAllocationsTable, - getEmptyUnbalancedNativeAllocationsTable, - getEmptyBalancedNativeAllocationsTable, + getEmptyRawJsAllocationsTable, + getEmptyRawUnbalancedNativeAllocationsTable, + getEmptyRawBalancedNativeAllocationsTable, getRawStackTableBuilderWithExistingContents, + getRawSamplesTableBuilderFromExisting, finishRawStackTableBuilder, + getRawFrameTableBuilderWithExistingContents, } from '../../../profile-logic/data-structures'; import { mergeProfilesForDiffing } from '../../../profile-logic/merge-compare'; import { computeReferenceCPUDeltaPerMs } from '../../../profile-logic/cpu'; @@ -209,7 +211,8 @@ export function addMarkersToThreadWithCorrespondingSamples( // control the initial range. Because of that we need to add samples so that // the range includes these markers. Note that when a thread has no sample, // then the markers are used to compute the initial range. - const { samples } = thread; + const samples = getRawSamplesTableBuilderFromExisting(thread.samples); + thread.samples = samples; if (samples.length) { const firstMarkerTime = Math.min(...allTimes); const lastMarkerTime = Math.max(...allTimes); @@ -1501,13 +1504,13 @@ export function getCounterForThread( pid: thread.pid, mainThreadIndex, samples: { - time: sampleTimes.slice(), + time: Array.from(sampleTimes), // Create some arbitrary (positive integer) values for the number. number: config.hasCountNumber - ? sampleTimes.map((_, i) => Math.floor(50 * Math.sin(i) + 50)) + ? Array.from(sampleTimes, (_, i) => Math.floor(50 * Math.sin(i) + 50)) : undefined, // Create some arbitrary values for the count. - count: sampleTimes.map((_, i) => Math.sin(i)), + count: Array.from(sampleTimes, (_, i) => Math.sin(i)), length: thread.samples.length, }, display: deriveCounterDisplay(category, name), @@ -1522,7 +1525,7 @@ export function getCounterForThreadWithSamples( thread: RawThread, mainThreadIndex: ThreadIndex, samples: { - time?: number[]; + time?: number[] | Float64Array; number?: number[]; count?: number[]; length: number; @@ -1628,8 +1631,8 @@ export function getProfileWithJsAllocations() { I[lib:libI.so] `); - // Now add a JsAllocationsTable. - const jsAllocations = getEmptyJsAllocationsTable(); + // Now add a RawJsAllocationsTable. + const jsAllocations = getEmptyRawJsAllocationsTable(); profile.threads[0].jsAllocations = jsAllocations; // The stack table is built sequentially, so we can assume that the stack indexes @@ -1708,8 +1711,8 @@ export function getProfileWithUnbalancedNativeAllocations() { ` ); - // Now add a NativeAllocationsTable. - const nativeAllocations = getEmptyUnbalancedNativeAllocationsTable(); + // Now add a RawNativeAllocationsTable. + const nativeAllocations = getEmptyRawUnbalancedNativeAllocationsTable(); profile.threads[0].nativeAllocations = nativeAllocations; // The stack table is built sequentially, so we can assume that the stack indexes @@ -1776,8 +1779,8 @@ export function getProfileWithBalancedNativeAllocations() { ` ); - // Now add a NativeAllocationsTable. - const nativeAllocations = getEmptyBalancedNativeAllocationsTable(); + // Now add a RawNativeAllocationsTable. + const nativeAllocations = getEmptyRawBalancedNativeAllocationsTable(); const [thread] = profile.threads; thread.nativeAllocations = nativeAllocations; const threadId = thread.tid; @@ -2013,7 +2016,8 @@ export function addInnerWindowIdToStacks( callNodesToDupe?: CallNodePath[] ) { const { stackTable, frameTable } = shared; - const { samples } = thread; + const samples = getRawSamplesTableBuilderFromExisting(thread.samples); + thread.samples = samples; const usedInnerWindowIDsSet = new Set(); for (const { innerWindowID, callNodes } of listOfOperations) { @@ -2043,30 +2047,38 @@ export function addInnerWindowIdToStacks( const stackTableBuilder = getRawStackTableBuilderWithExistingContents(stackTable); + const frameTableBuilder = + getRawFrameTableBuilderWithExistingContents(frameTable); for (const callNode of callNodesToDupe) { const stackIndex = getStackIndexForCallNodePath(shared, callNode); const foundFrameIndex = stackTable.frame[stackIndex]; // The found one comes from the first tab. - frameTable.innerWindowID[foundFrameIndex] = + frameTableBuilder.innerWindowID[foundFrameIndex] = listOfOperations[0].innerWindowID; // Clone this frame - const newFrameIndex = frameTable.length++; - frameTable.address.push(frameTable.address[foundFrameIndex]); - frameTable.inlineDepth.push(frameTable.inlineDepth[foundFrameIndex]); - frameTable.category.push(frameTable.category[foundFrameIndex]); - frameTable.subcategory.push(frameTable.subcategory[foundFrameIndex]); - frameTable.func.push(frameTable.func[foundFrameIndex]); - frameTable.nativeSymbol.push(frameTable.nativeSymbol[foundFrameIndex]); - frameTable.line.push(frameTable.line[foundFrameIndex]); - frameTable.column.push(frameTable.column[foundFrameIndex]); - frameTable.originalLocation.push( + const newFrameIndex = frameTableBuilder.length++; + frameTableBuilder.address.push(frameTable.address[foundFrameIndex]); + frameTableBuilder.inlineDepth.push( + frameTable.inlineDepth[foundFrameIndex] + ); + frameTableBuilder.category.push(frameTable.category[foundFrameIndex]); + frameTableBuilder.subcategory.push( + frameTable.subcategory[foundFrameIndex] + ); + frameTableBuilder.func.push(frameTable.func[foundFrameIndex]); + frameTableBuilder.nativeSymbol.push( + frameTable.nativeSymbol[foundFrameIndex] + ); + frameTableBuilder.line.push(frameTable.line[foundFrameIndex]); + frameTableBuilder.column.push(frameTable.column[foundFrameIndex]); + frameTableBuilder.originalLocation.push( frameTable.originalLocation[foundFrameIndex] ); // And that one comes from the second tab. - frameTable.innerWindowID.push(listOfOperations[1].innerWindowID); + frameTableBuilder.innerWindowID.push(listOfOperations[1].innerWindowID); // Clone the stack const newStackIndex = stackTableBuilder.length++; @@ -2079,6 +2091,7 @@ export function addInnerWindowIdToStacks( } shared.stackTable = finishRawStackTableBuilder(stackTableBuilder); + shared.frameTable = frameTableBuilder; const sampleTimes = ensureExists(samples.time); for (let sampleIndex = samples.length; sampleIndex >= 0; sampleIndex--) { diff --git a/src/test/fixtures/utils.ts b/src/test/fixtures/utils.ts index d91e76e8fa..83c68020cb 100644 --- a/src/test/fixtures/utils.ts +++ b/src/test/fixtures/utils.ts @@ -18,7 +18,10 @@ import { getOriginAnnotationForFunc, createThreadFromDerivedTables, computeStackTableFromRawStackTable, + computeFrameTableFromRawFrameTable, computeSamplesTableFromRawSamplesTable, + computeJsAllocationsTableFromRawJsAllocationsTable, + computeNativeAllocationsTableFromRawNativeAllocationsTable, } from 'firefox-profiler/profile-logic/profile-data'; import { getProfileWithDicts } from './profiles/processed-profile'; import { StringTable } from '../../utils/string-table'; @@ -162,18 +165,31 @@ export function computeThreadFromRawThread( const tracedValuesBuffer = rawThread.tracedValuesBuffer ? base64StringToBytes(rawThread.tracedValuesBuffer) : undefined; + const jsAllocations = rawThread.jsAllocations + ? computeJsAllocationsTableFromRawJsAllocationsTable( + rawThread.jsAllocations + ) + : undefined; + const nativeAllocations = rawThread.nativeAllocations + ? computeNativeAllocationsTableFromRawNativeAllocationsTable( + rawThread.nativeAllocations + ) + : undefined; + const frameTable = computeFrameTableFromRawFrameTable(shared.frameTable); return createThreadFromDerivedTables( rawThread, samples, stackTable, - shared.frameTable, + frameTable, shared.funcTable, shared.nativeSymbols, shared.resourceTable, stringTable, shared.sources, tracedValuesBuffer, - shared.sourceLocationTable + shared.sourceLocationTable, + jsAllocations, + nativeAllocations ); } 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 d186387a95..ddef611540 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": 66, + "preprocessedProfileVersion": 67, "processType": 0, "product": "a.out", "sampleUnits": Object { @@ -1452,7 +1452,7 @@ Object { "markerSchema": Array [], "oscpu": "macOS 14.6.1", "pausedRanges": Array [], - "preprocessedProfileVersion": 66, + "preprocessedProfileVersion": 67, "processType": 0, "product": "a.out", "sampleUnits": Object { @@ -2817,7 +2817,7 @@ Object { "markerSchema": Array [], "oscpu": "macOS 14.6.1", "pausedRanges": Array [], - "preprocessedProfileVersion": 66, + "preprocessedProfileVersion": 67, "processType": 0, "product": "a.out", "sampleUnits": Object { @@ -4182,7 +4182,7 @@ Object { "markerSchema": Array [], "oscpu": "macOS 14.6.1", "pausedRanges": Array [], - "preprocessedProfileVersion": 66, + "preprocessedProfileVersion": 67, "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 0479910d2b..4416a8277d 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": 66, + "preprocessedProfileVersion": 67, "processType": 0, "product": "Firefox", "sourceURL": "", @@ -2396,7 +2396,7 @@ CallTree { 100, 100, ], - "time": Array [ + "time": Float64Array [ 4, 5, ], @@ -2410,7 +2410,7 @@ CallTree { "_thread": Object { "eTLD+1": undefined, "frameTable": Object { - "address": Array [ + "address": Int32Array [ -1, -1, -1, @@ -2443,7 +2443,7 @@ CallTree { null, null, ], - "func": Array [ + "func": Int32Array [ 0, 1, 2, @@ -2454,7 +2454,7 @@ CallTree { 7, 8, ], - "inlineDepth": Array [ + "inlineDepth": Uint8Array [ 0, 0, 0, @@ -2684,7 +2684,7 @@ CallTree { 100, 100, ], - "time": Array [ + "time": Float64Array [ 3, 4, 5, @@ -2798,7 +2798,7 @@ exports[`snapshots of selectors/profile matches the last stored run of selectedT Object { "eTLD+1": undefined, "frameTable": Object { - "address": Array [ + "address": Int32Array [ -1, -1, -1, @@ -2831,7 +2831,7 @@ Object { null, null, ], - "func": Array [ + "func": Int32Array [ 0, 1, 2, @@ -2842,7 +2842,7 @@ Object { 7, 8, ], - "inlineDepth": Array [ + "inlineDepth": Uint8Array [ 0, 0, 0, @@ -3072,7 +3072,7 @@ Object { 100, 100, ], - "time": Array [ + "time": Float64Array [ 3, 4, 5, @@ -3266,7 +3266,7 @@ exports[`snapshots of selectors/profile matches the last stored run of selectedT Object { "eTLD+1": undefined, "frameTable": Object { - "address": Array [ + "address": Int32Array [ -1, -1, -1, @@ -3299,7 +3299,7 @@ Object { null, null, ], - "func": Array [ + "func": Int32Array [ 0, 1, 2, @@ -3310,7 +3310,7 @@ Object { 7, 8, ], - "inlineDepth": Array [ + "inlineDepth": Uint8Array [ 0, 0, 0, @@ -3530,7 +3530,7 @@ Object { 100, 100, ], - "time": Array [ + "time": Float64Array [ 4, 5, ], @@ -3640,7 +3640,7 @@ exports[`snapshots of selectors/profile matches the last stored run of selectedT Object { "eTLD+1": undefined, "frameTable": Object { - "address": Array [ + "address": Int32Array [ -1, -1, -1, @@ -3673,7 +3673,7 @@ Object { null, null, ], - "func": Array [ + "func": Int32Array [ 0, 1, 2, @@ -3684,7 +3684,7 @@ Object { 7, 8, ], - "inlineDepth": Array [ + "inlineDepth": Uint8Array [ 0, 0, 0, @@ -3914,7 +3914,7 @@ Object { 100, 100, ], - "time": Array [ + "time": Float64Array [ 3, 4, 5, @@ -4026,7 +4026,7 @@ exports[`snapshots of selectors/profile matches the last stored run of selectedT Object { "eTLD+1": undefined, "frameTable": Object { - "address": Array [ + "address": Int32Array [ -1, -1, -1, @@ -4059,7 +4059,7 @@ Object { null, null, ], - "func": Array [ + "func": Int32Array [ 0, 1, 2, @@ -4070,7 +4070,7 @@ Object { 7, 8, ], - "inlineDepth": Array [ + "inlineDepth": Uint8Array [ 0, 0, 0, @@ -4300,7 +4300,7 @@ Object { 100, 100, ], - "time": Array [ + "time": Float64Array [ 3, 4, 5, diff --git a/src/test/store/profile-view.test.ts b/src/test/store/profile-view.test.ts index 9fda33ec65..b7adb6bf99 100644 --- a/src/test/store/profile-view.test.ts +++ b/src/test/store/profile-view.test.ts @@ -21,7 +21,7 @@ import { import { getEmptyThread, getEmptyProfile, - getEmptySamplesTableWithEventDelay, + getRawSamplesTableBuilderWithEventDelay, } from '../../profile-logic/data-structures'; import { withAnalyticsMock } from '../fixtures/mocks/analytics'; import { getProfileWithNiceTracks } from '../fixtures/profiles/tracks'; @@ -53,11 +53,11 @@ import { checkBit } from '../../utils/bitset'; import type { TrackReference, Milliseconds, - RawThread, StartEndRange, Marker, MixedObject, CallNodePath, + RawThread, } from 'firefox-profiler/types'; describe('call node paths on implementation filter change', function () { @@ -3824,7 +3824,7 @@ describe('getProcessedEventDelays', function () { const profile = getEmptyProfile(); // Create event delay values. - const samples = getEmptySamplesTableWithEventDelay(); + const samples = getRawSamplesTableBuilderWithEventDelay(); if (eventDelay) { samples.eventDelay = eventDelay; } else { diff --git a/src/test/store/useful-tabs.test.ts b/src/test/store/useful-tabs.test.ts index 2e770a2581..3e0bb0a54a 100644 --- a/src/test/store/useful-tabs.test.ts +++ b/src/test/store/useful-tabs.test.ts @@ -13,7 +13,7 @@ import { getMergedProfileFromTextSamples, getProfileWithUnbalancedNativeAllocations, } from '../fixtures/profiles/processed-profile'; -import { getEmptySamplesTableWithEventDelay } from '../../profile-logic/data-structures'; +import { getRawSamplesTableBuilderWithEventDelay } from '../../profile-logic/data-structures'; describe('getUsefulTabs', function () { it('hides the network chart and JS tracer when no data is in the thread', function () { @@ -79,7 +79,7 @@ describe('getUsefulTabs', function () { it('shows sample related tabs even when there are only allocation samples in the profile', function () { const { profile } = getProfileWithUnbalancedNativeAllocations(); for (const thread of profile.threads) { - thread.samples = getEmptySamplesTableWithEventDelay(); + thread.samples = getRawSamplesTableBuilderWithEventDelay(); } const { getState } = storeWithProfile(profile); expect(selectedThreadSelectors.getUsefulTabs(getState())).toEqual([ diff --git a/src/test/unit/__snapshots__/profile-conversion.test.ts.snap b/src/test/unit/__snapshots__/profile-conversion.test.ts.snap index 713bff2592..b25922c7b4 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": 66, + "preprocessedProfileVersion": 67, "processType": 0, "product": "ART Trace (Android)", "sampleUnits": undefined, @@ -83833,7 +83833,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 66, + "preprocessedProfileVersion": 67, "processType": 0, "product": "ART Trace (Android)", "sampleUnits": undefined, @@ -329278,7 +329278,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 66, + "preprocessedProfileVersion": 67, "processType": 0, "product": "Chrome Trace", "profilingEndTime": 119159778.026, @@ -364259,7 +364259,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 66, + "preprocessedProfileVersion": 67, "processType": 0, "product": "Chrome Trace", "profilingEndTime": 119159778.026, @@ -399219,7 +399219,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 66, + "preprocessedProfileVersion": 67, "processType": 0, "product": "Chrome Trace", "profilingEndTime": 66155012.423, @@ -401927,7 +401927,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 66, + "preprocessedProfileVersion": 67, "processType": 0, "product": "Chrome Trace", "sourceURL": "", @@ -403765,7 +403765,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 66, + "preprocessedProfileVersion": 67, "processType": 0, "product": "Chrome Trace", "profilingEndTime": 355035987.653, @@ -407776,7 +407776,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 66, + "preprocessedProfileVersion": 67, "processType": 0, "product": "Chrome Trace", "sourceURL": "", @@ -413399,7 +413399,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 66, + "preprocessedProfileVersion": 67, "processType": 0, "product": "Chrome Trace", "profilingEndTime": 66155012.423, @@ -414544,7 +414544,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 66, + "preprocessedProfileVersion": 67, "processType": 0, "product": "Firefox", "sampleUnits": undefined, @@ -418704,7 +418704,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 66, + "preprocessedProfileVersion": 67, "processType": 0, "product": "Firefox", "sampleUnits": undefined, @@ -434833,7 +434833,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 66, + "preprocessedProfileVersion": 67, "processType": 0, "product": "Firefox", "sampleUnits": undefined, @@ -441509,7 +441509,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 66, + "preprocessedProfileVersion": 67, "processType": 0, "product": "Firefox", "sampleUnits": undefined, @@ -459410,7 +459410,7 @@ Object { "keepProfileThreadOrder": true, "markerSchema": Array [], "platform": "Android", - "preprocessedProfileVersion": 66, + "preprocessedProfileVersion": 67, "processType": 0, "product": "com.example.sampleapplication", "sourceCodeIsNotOnSearchfox": true, @@ -521502,7 +521502,7 @@ Object { "keepProfileThreadOrder": true, "markerSchema": Array [], "platform": "Android", - "preprocessedProfileVersion": 66, + "preprocessedProfileVersion": 67, "processType": 0, "product": "com.example.sampleapplication", "sourceCodeIsNotOnSearchfox": true, @@ -587927,7 +587927,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 66, + "preprocessedProfileVersion": 67, "processType": 0, "product": "target/debug/examples/work_log (dhat)", "sourceURL": "", @@ -597177,7 +597177,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 66, + "preprocessedProfileVersion": 67, "processType": 0, "product": "Flamegraph", "sourceURL": "", @@ -905382,7 +905382,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 66, + "preprocessedProfileVersion": 67, "processType": 0, "product": "Flamegraph", "sourceURL": "", diff --git a/src/test/unit/__snapshots__/profile-upgrading.test.ts.snap b/src/test/unit/__snapshots__/profile-upgrading.test.ts.snap index 4b1a8867e6..1285801404 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": 66, + "preprocessedProfileVersion": 67, "processType": 0, "product": "Firefox", "sampleUnits": undefined, @@ -7643,7 +7643,7 @@ Object { "misc": "rv:48.0", "oscpu": "Intel Mac OS X 10.11", "platform": "Macintosh", - "preprocessedProfileVersion": 66, + "preprocessedProfileVersion": 67, "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": 66, + "preprocessedProfileVersion": 67, "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": 66, + "preprocessedProfileVersion": 67, "processType": 0, "product": "Firefox", "stackwalk": 1, diff --git a/src/test/unit/activity-slice-tree.test.ts b/src/test/unit/activity-slice-tree.test.ts index 95af457973..91daea025b 100644 --- a/src/test/unit/activity-slice-tree.test.ts +++ b/src/test/unit/activity-slice-tree.test.ts @@ -5,7 +5,7 @@ import { getSlices, printSliceTree } from '../../utils/slice-tree'; function getSlicesEasy(threadCPUPercentage: number[]): string[] { - const time = threadCPUPercentage.map((_, i) => i); + const time = Float64Array.from(threadCPUPercentage, (_, i) => i); const cpuRatio = new Float64Array(threadCPUPercentage.map((p) => p / 100)); const slices = getSlices([0.05, 0.2, 0.4, 0.6, 0.8], cpuRatio, time); return printSliceTree(slices); @@ -46,7 +46,7 @@ describe('Activity slice tree', function () { { start: 0, end: 1, avg: 0.9, sum: 1000, parent: 0 }, ]; - expect(printSliceTree({ slices, time: [0, 1] })).toEqual([ + expect(printSliceTree({ slices, time: Float64Array.of(0, 1) })).toEqual([ '- 10% for 1.0ms (1 samples): 0.0ms - 1.0ms', ' - 90% for 1.0ms (1 samples): 0.0ms - 1.0ms', '- 20% for 1.0ms (1 samples): 0.0ms - 1.0ms', diff --git a/src/test/unit/combined-cpu.test.ts b/src/test/unit/combined-cpu.test.ts index 87fbc6501a..aa75cff535 100644 --- a/src/test/unit/combined-cpu.test.ts +++ b/src/test/unit/combined-cpu.test.ts @@ -10,7 +10,7 @@ function createSamplesTable(time: number[], cpuRatio: number[]): SamplesTable { const percentValues = cpuRatio.map((v) => Math.round(v * 100)); percentValues.push(0); return { - time, + time: Float64Array.from(time), threadCPUPercent: Uint8Array.from(percentValues), hasCPUDeltas: true, // Other required fields (stubbed for test purposes) @@ -35,7 +35,7 @@ describe('combineCPUDataFromThreads', function () { const result = combineCPUDataFromThreads(samples); expect(result).not.toBeNull(); - expect(result!.time).toEqual([0, 100, 200]); + expect(Array.from(result!.time)).toEqual([0, 100, 200]); expect(Array.from(result!.cpuRatio)).toEqual([0.0, 0.5, 0.8]); }); @@ -48,7 +48,7 @@ describe('combineCPUDataFromThreads', function () { const result = combineCPUDataFromThreads(samples); expect(result).not.toBeNull(); - expect(result!.time).toEqual([0, 100, 200]); + expect(Array.from(result!.time)).toEqual([0, 100, 200]); expect(Array.from(result!.cpuRatio)).toEqual([0, 0.9, 0.8]); }); @@ -62,7 +62,7 @@ describe('combineCPUDataFromThreads', function () { expect(result).not.toBeNull(); // Should have all unique time points - expect(result!.time).toEqual([0, 50, 100, 150, 200, 250]); + expect(Array.from(result!.time)).toEqual([0, 50, 100, 150, 200, 250]); // 0: thread1=bef, thread2=bef → 0.0 // 0- 50: thread1=0.5, thread2=bef → 0.5 @@ -87,7 +87,7 @@ describe('combineCPUDataFromThreads', function () { const result = combineCPUDataFromThreads(samples); expect(result).not.toBeNull(); - expect(result!.time).toEqual([0, 10, 20, 30, 40, 50]); + expect(Array.from(result!.time)).toEqual([0, 10, 20, 30, 40, 50]); // At times 0, 10, 20: only thread1 has samples // At times 30, 40, 50: thread1 has ended (30 > 20), only thread2 contributes diff --git a/src/test/unit/process-profile.test.ts b/src/test/unit/process-profile.test.ts index 11c3e63f05..84bd6bfc3b 100644 --- a/src/test/unit/process-profile.test.ts +++ b/src/test/unit/process-profile.test.ts @@ -284,12 +284,16 @@ describe('gecko counters processing', function () { expect(extractTime(childCounter)).toEqual(originalTime); expect( - computeTimeColumnForRawSamplesTable(processedCounters[0].samples) + Array.from( + computeTimeColumnForRawSamplesTable(processedCounters[0].samples) + ) ).toEqual(originalTime); // The subprocess times are offset when processed: expect( - computeTimeColumnForRawSamplesTable(processedCounters[1].samples) + Array.from( + computeTimeColumnForRawSamplesTable(processedCounters[1].samples) + ) ).toEqual(offsetTime); }); }); @@ -645,7 +649,7 @@ describe('gecko samples table processing', function () { hardcodedStackAfterProcessing ); const sampleTimes = computeTimeColumnForRawSamplesTable(processedSamples); - expect(sampleTimes.slice(0, 2)).toEqual(hardcodedTime); + expect(Array.from(sampleTimes.slice(0, 2))).toEqual(hardcodedTime); expect(ensureExists(processedSamples.eventDelay).slice(0, 2)).toEqual( hardcodedEventDelay ); diff --git a/src/test/unit/profile-data.test.ts b/src/test/unit/profile-data.test.ts index 7be0938fde..206e8a07b6 100644 --- a/src/test/unit/profile-data.test.ts +++ b/src/test/unit/profile-data.test.ts @@ -22,6 +22,7 @@ import { extractProfileFilterPageData, findAddressProofForFile, calculateFunctionSizeLowerBound, + computeFrameTableFromRawFrameTable, getNativeSymbolsForCallNode, getNativeSymbolInfo, computeTimeColumnForRawSamplesTable, @@ -1475,7 +1476,7 @@ describe('calculateFunctionSizeLowerBound', function () { const nativeSymbolIndex = nativeSymbolsDict.symSomeFunc; const functionSizeLowerBound = calculateFunctionSizeLowerBound( - shared.frameTable, + computeFrameTableFromRawFrameTable(shared.frameTable), 0x1000, nativeSymbolIndex ); @@ -1769,12 +1770,13 @@ describe('getNativeSymbolInfo', function () { const { shared } = profile; const stringTable = StringTable.withBackingArray(shared.stringArray); const { symSomeFunc, symOtherFunc } = nativeSymbolsDictPerThread[0]; + const frameTable = computeFrameTableFromRawFrameTable(shared.frameTable); expect( getNativeSymbolInfo( symSomeFunc, shared.nativeSymbols, - shared.frameTable, + frameTable, stringTable ) ).toEqual({ @@ -1788,7 +1790,7 @@ describe('getNativeSymbolInfo', function () { getNativeSymbolInfo( symOtherFunc, shared.nativeSymbols, - shared.frameTable, + frameTable, stringTable ) ).toEqual({ diff --git a/src/test/unit/profile-query/cpu-activity.test.ts b/src/test/unit/profile-query/cpu-activity.test.ts index 5d1b7f8dc0..9528d19ac4 100644 --- a/src/test/unit/profile-query/cpu-activity.test.ts +++ b/src/test/unit/profile-query/cpu-activity.test.ts @@ -12,7 +12,7 @@ describe('profile-query cpu activity', function () { { start: 1, end: 3, avg: 0.75, sum: 40, parent: 0 }, { start: 2, end: 3, avg: 1, sum: 20, parent: 1 }, ]; - const time = [0, 10, 20, 30, 40]; + const time = Float64Array.of(0, 10, 20, 30, 40); const tsManager = new TimestampManager({ start: 0, end: 40 }); const result = collectSliceTree({ slices, time }, tsManager); @@ -43,6 +43,8 @@ describe('profile-query cpu activity', function () { it('returns an empty list when there are no slices', function () { const tsManager = new TimestampManager({ start: 0, end: 10 }); - expect(collectSliceTree({ slices: [], time: [] }, tsManager)).toEqual([]); + expect( + collectSliceTree({ slices: [], time: new Float64Array() }, tsManager) + ).toEqual([]); }); }); diff --git a/src/test/unit/profile-tree.test.ts b/src/test/unit/profile-tree.test.ts index 566bedf105..90f8a59a91 100644 --- a/src/test/unit/profile-tree.test.ts +++ b/src/test/unit/profile-tree.test.ts @@ -13,6 +13,7 @@ import { } from '../../profile-logic/call-tree'; import { computeFlameGraphRows } from '../../profile-logic/flame-graph'; import { + computeFrameTableFromRawFrameTable, getCallNodeInfo, getInvertedCallNodeInfo, getOriginAnnotationForFunc, @@ -726,7 +727,7 @@ describe('origin annotation', function () { return getOriginAnnotationForFunc( funcNames.indexOf(funcName), null, - shared.frameTable, + computeFrameTableFromRawFrameTable(shared.frameTable), shared.funcTable, shared.resourceTable, stringTable, @@ -795,7 +796,7 @@ describe('getOriginAnnotationForFunc with originalLocation', function () { return getOriginAnnotationForFunc( 0, 0, - shared.frameTable, + computeFrameTableFromRawFrameTable(shared.frameTable), shared.funcTable, shared.resourceTable, stringTable, @@ -883,7 +884,7 @@ describe('getOriginalPositionForFrame', function () { getOriginalPositionForFrame( 0, 0, - shared.frameTable, + computeFrameTableFromRawFrameTable(shared.frameTable), shared.funcTable, shared.sourceLocationTable ) @@ -901,7 +902,7 @@ describe('getOriginalPositionForFrame', function () { getOriginalPositionForFrame( 0, 0, - shared.frameTable, + computeFrameTableFromRawFrameTable(shared.frameTable), shared.funcTable, shared.sourceLocationTable ) @@ -914,7 +915,7 @@ describe('getOriginalPositionForFrame', function () { getOriginalPositionForFrame( 0, 0, - shared.frameTable, + computeFrameTableFromRawFrameTable(shared.frameTable), shared.funcTable, shared.sourceLocationTable ) @@ -929,7 +930,7 @@ describe('getOriginalPositionForFrame', function () { getOriginalPositionForFrame( 0, 0, - shared.frameTable, + computeFrameTableFromRawFrameTable(shared.frameTable), shared.funcTable, shared.sourceLocationTable ) @@ -947,7 +948,7 @@ describe('getOriginalPositionForFrame', function () { getOriginalPositionForFrame( null, 0, - shared.frameTable, + computeFrameTableFromRawFrameTable(shared.frameTable), shared.funcTable, shared.sourceLocationTable ) @@ -966,7 +967,7 @@ describe('getOriginalPositionForFrame', function () { getOriginalPositionForFrame( 0, 0, - shared.frameTable, + computeFrameTableFromRawFrameTable(shared.frameTable), shared.funcTable, null ) diff --git a/src/types/actions.ts b/src/types/actions.ts index 100bf3f6ab..ec2537f46b 100644 --- a/src/types/actions.ts +++ b/src/types/actions.ts @@ -15,7 +15,7 @@ import type { PageList, IndexIntoSourceTable, FuncTable, - FrameTable, + RawFrameTable, SourceLocationTable, SourceTable, } from './profile'; @@ -454,7 +454,7 @@ type ReceiveProfileAction = | { readonly type: 'BULK_SOURCE_MAP_SYMBOLICATION'; readonly newFuncTable: FuncTable; - readonly newFrameTable: FrameTable; + readonly newFrameTable: RawFrameTable; readonly newSourceLocationTable: SourceLocationTable; readonly newSources: SourceTable; readonly newStringArray: string[]; diff --git a/src/types/profile-derived.ts b/src/types/profile-derived.ts index 6568b3b209..f7d61057f6 100644 --- a/src/types/profile-derived.ts +++ b/src/types/profile-derived.ts @@ -7,10 +7,13 @@ import type { MarkerPayload, MarkerSchema } from './markers'; import type { ThreadIndex, Pid, + InnerWindowID, IndexIntoFuncTable, IndexIntoJsTracerEvents, IndexIntoCategoryList, + IndexIntoSubcategoryListForCategory, IndexIntoResourceTable, + IndexIntoNativeSymbolTable, IndexIntoLibs, CounterIndex, IndexIntoRawMarkerTable, @@ -19,10 +22,7 @@ import type { Tid, ProcessType, PausedRange, - JsAllocationsTable, - NativeAllocationsTable, RawMarkerTable, - FrameTable, FuncTable, ResourceTable, NativeSymbolTable, @@ -31,6 +31,7 @@ import type { WeightType, SourceTable, IndexIntoSourceTable, + IndexIntoSourceLocationTable, CounterDisplayConfig, SourceLocationTable, } from './profile'; @@ -130,7 +131,7 @@ export type SamplesTable = { // This is optional because older profiles didn't have that field. eventDelay?: Array; stack: Array; - time: Milliseconds[]; + time: Float64Array; // An optional weight array. If not present, then the weight is assumed to be 1. // See the WeightType type for more information. weight: null | number[]; @@ -165,7 +166,7 @@ export type SampleCategoriesAndSubcategories = { export type SamplesLikeTable = { stack: Array; - time: Milliseconds[]; + time: Float64Array; // An optional weight array. If not present, then the weight is assumed to be 1. // See the WeightType type for more information. weight: null | number[]; @@ -175,7 +176,7 @@ export type SamplesLikeTable = { }; export type CounterSamplesTable = { - time: Milliseconds[]; + time: Float64Array; // The number of times the Counter's "number" was changed since the previous sample. // This property was mandatory until the format version 42, it was made optional in 43. number?: number[]; @@ -185,6 +186,55 @@ export type CounterSamplesTable = { length: number; }; +/** + * The `JsAllocationsTable` type of the derived thread. + * + * Differs from `RawJsAllocationsTable` in that the `time` column is always a + * `Float64Array`. + */ +export type JsAllocationsTable = { + time: Float64Array; + className: string[]; + typeName: string[]; + coarseType: string[]; + weight: Bytes[]; + weightType: 'bytes'; + inNursery: boolean[]; + stack: Array; + length: number; +}; + +/** + * The `UnbalancedNativeAllocationsTable` type of the derived thread. + * + * Differs from `RawUnbalancedNativeAllocationsTable` in that the `time` column + * is always a `Float64Array`. + */ +export type UnbalancedNativeAllocationsTable = { + time: Float64Array; + weight: Bytes[]; + weightType: 'bytes'; + stack: Array; + argumentValues?: Array; + length: number; +}; + +/** + * The `BalancedNativeAllocationsTable` type of the derived thread. + */ +export type BalancedNativeAllocationsTable = + UnbalancedNativeAllocationsTable & { + memoryAddress: number[]; + threadId: number[]; + }; + +/** + * The `NativeAllocationsTable` type of the derived thread. + */ +export type NativeAllocationsTable = + | UnbalancedNativeAllocationsTable + | BalancedNativeAllocationsTable; + export type Counter = { name: string; category: string; @@ -235,6 +285,33 @@ export type StackTable = { subcategory: Uint8Array | Uint16Array; // represents a Map }; +/** + * The `FrameTable` type of the derived thread. + * + * Differs from `RawFrameTable` in that the following columns are always + * stored as typed arrays: `address` (`Int32Array`, with `-1` as the sentinel + * for missing addresses), `inlineDepth` (`Uint8Array`), and `func` + * (`Int32Array`). In `RawFrameTable`, these columns may be either regular + * arrays or typed arrays, since regular arrays are convenient during + * construction. + */ +export type FrameTable = { + // Differs from RawFrameTable: always Int32Array (-1 sentinel preserved). + address: Int32Array; + // Differs from RawFrameTable: always Uint8Array. + inlineDepth: Uint8Array; + category: (IndexIntoCategoryList | null)[]; + subcategory: (IndexIntoSubcategoryListForCategory | null)[]; + // Differs from RawFrameTable: always Int32Array. + func: Int32Array; + nativeSymbol: (IndexIntoNativeSymbolTable | null)[]; + innerWindowID: (InnerWindowID | null)[]; + line: (number | null)[]; + column: (number | null)[]; + originalLocation: Array; + length: number; +}; + /** * Similar to the StackTable, but based on functions rather than on frames. * diff --git a/src/types/profile.ts b/src/types/profile.ts index 8b470516e2..bbac5d671b 100644 --- a/src/types/profile.ts +++ b/src/types/profile.ts @@ -126,9 +126,9 @@ export type RawSamplesTable = { // This is optional because older profiles didn't have that field. eventDelay?: Array; stack: Array; - time?: Milliseconds[]; + time?: Milliseconds[] | Float64Array; // If the `time` column is not present, then the `timeDeltas` column must be present. - timeDeltas?: Milliseconds[]; + timeDeltas?: Milliseconds[] | Float64Array; argumentValues?: Array; // An optional weight array. If not present, then the weight is assumed to be 1. // See the WeightType type for more information. @@ -151,9 +151,11 @@ export type RawSamplesTable = { /** * JS allocations are recorded as a marker payload, but in profile processing they * are moved to the Thread. This allows them to be part of the stack processing pipeline. + * + * There is also a derived `JsAllocationsTable` type, see profile-derived.js. */ -export type JsAllocationsTable = { - time: Milliseconds[]; +export type RawJsAllocationsTable = { + time: Milliseconds[] | Float64Array; className: string[]; typeName: string[]; // Currently only 'JSObject' coarseType: string[]; // Currently only 'Object', @@ -170,8 +172,8 @@ export type JsAllocationsTable = { * This variant is the original version of the table, before the memory address * and threadId were added. */ -export type UnbalancedNativeAllocationsTable = { - time: Milliseconds[]; +export type RawUnbalancedNativeAllocationsTable = { + time: Milliseconds[] | Float64Array; // "weight" is used here rather than "bytes", so that this type can be // used as a SamplesLikeTable. weight: Bytes[]; @@ -184,8 +186,8 @@ export type UnbalancedNativeAllocationsTable = { /** * The memory address and thread ID were added later. */ -export type BalancedNativeAllocationsTable = - UnbalancedNativeAllocationsTable & { +export type RawBalancedNativeAllocationsTable = + RawUnbalancedNativeAllocationsTable & { memoryAddress: number[]; threadId: number[]; }; @@ -195,10 +197,12 @@ export type BalancedNativeAllocationsTable = * are moved to the Thread. This allows them to be part of the stack processing pipeline. * Currently they include native allocations and deallocations. However, both * of them are sampled independently, so they will be unbalanced if summed togther. + * + * There is also a derived `NativeAllocationsTable` type, see profile-derived.js. */ -export type NativeAllocationsTable = - | UnbalancedNativeAllocationsTable - | BalancedNativeAllocationsTable; +export type RawNativeAllocationsTable = + | RawUnbalancedNativeAllocationsTable + | RawBalancedNativeAllocationsTable; /** * Markers represent arbitrary events that happen within the browser. They have a @@ -229,7 +233,7 @@ export type RawMarkerTable = { * Frames contain the context information about the function execution at the moment in * time. The caller/callee relationship between frames is defined by the StackTable. */ -export type FrameTable = { +export type RawFrameTable = { // If this is a frame for native code, the address is the address of the frame's // assembly instruction, relative to the native library that contains it. // @@ -241,7 +245,9 @@ export type FrameTable = { // // The library which this address is relative to is given by the frame's nativeSymbol: // frame -> nativeSymbol -> lib. - address: Array
; + // + // Frames with no address use the sentinel value `-1`. + address: Array
| Int32Array; // The inline depth for this frame. If there is an inline stack at an address, // we create multiple frames with the same address, one for each depth. @@ -269,7 +275,7 @@ export type FrameTable = { // // The frames of an inline stack at an address all have the same address and the same // nativeSymbol, but each has a different func and line. - inlineDepth: number[]; + inlineDepth: number[] | Uint8Array; // The category of the frame. This is used to calculate the category of the stack nodes // which use this frame: @@ -286,7 +292,7 @@ export type FrameTable = { subcategory: (IndexIntoSubcategoryListForCategory | null)[]; // The frame's function. - func: IndexIntoFuncTable[]; + func: IndexIntoFuncTable[] | Int32Array; // The symbol index (referring into this thread's nativeSymbols table) corresponding // to symbol that covers the frame address of this frame. Only non-null for native @@ -530,8 +536,8 @@ export type JsTracerTable = { }; export type RawCounterSamplesTable = { - time?: Milliseconds[]; - timeDeltas?: Milliseconds[]; + time?: Milliseconds[] | Float64Array; + timeDeltas?: Milliseconds[] | Float64Array; // The number of times the Counter's "number" was changed since the previous sample. // This property was mandatory until the format version 42, it was made optional in 43. number?: number[]; @@ -764,8 +770,8 @@ export type RawThread = { pid: Pid; tid: Tid; samples: RawSamplesTable; - jsAllocations?: JsAllocationsTable; - nativeAllocations?: NativeAllocationsTable; + jsAllocations?: RawJsAllocationsTable; + nativeAllocations?: RawNativeAllocationsTable; markers: RawMarkerTable; jsTracer?: JsTracerTable; // If present and true, this thread was launched for a private browsing session only. @@ -1082,7 +1088,7 @@ export type SourceLocationTable = { export type RawProfileSharedData = { stackTable: RawStackTable; - frameTable: FrameTable; + frameTable: RawFrameTable; funcTable: FuncTable; resourceTable: ResourceTable; nativeSymbols: NativeSymbolTable; diff --git a/src/utils/number-series.ts b/src/utils/number-series.ts index e676a9faeb..5a3252e4cf 100644 --- a/src/utils/number-series.ts +++ b/src/utils/number-series.ts @@ -2,8 +2,10 @@ * 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 numberSeriesFromDeltas(deltas: number[]): number[] { - const values = new Array(deltas.length); +export function numberSeriesFromDeltas( + deltas: number[] | Float64Array +): Float64Array { + const values = new Float64Array(deltas.length); let prev = 0; for (let i = 0; i < deltas.length; i++) { const current = prev + deltas[i]; @@ -13,7 +15,9 @@ export function numberSeriesFromDeltas(deltas: number[]): number[] { return values; } -export function numberSeriesToDeltas(values: number[]): number[] { +export function numberSeriesToDeltas( + values: number[] | Float64Array +): number[] { const deltas = new Array(values.length); let prev = 0; for (let i = 0; i < values.length; i++) { diff --git a/src/utils/slice-tree.ts b/src/utils/slice-tree.ts index c2289b2b7b..c218a2eb80 100644 --- a/src/utils/slice-tree.ts +++ b/src/utils/slice-tree.ts @@ -13,7 +13,7 @@ export type Slice = { function addIndexIntervalsExceedingThreshold( threshold: number, cpuRatio: Float64Array, - time: number[], + time: Float64Array, items: Slice[], parent: number | null, startIndex: number = 0, @@ -74,13 +74,13 @@ function addIndexIntervalsExceedingThreshold( export type SliceTree = { slices: Slice[]; - time: number[]; + time: Float64Array; }; export function getSlices( thresholds: number[], cpuRatio: Float64Array, - time: number[], + time: Float64Array, startIndex: number = 0, endIndex: number = cpuRatio.length - 1 ): SliceTree { @@ -114,7 +114,7 @@ export function getSlices( return { slices, time }; } -function sliceToString(slice: Slice, time: number[]): string { +function sliceToString(slice: Slice, time: Float64Array): string { const { avg, start, end } = slice; const startTime = time[start]; const endTime = time[end]; @@ -130,7 +130,7 @@ function appendSliceSubtree( childrenStartPerParent: Array, interestingSliceIndexes: Set, nestingDepth: number, - time: number[], + time: Float64Array, s: string[] ) { for (let i = startIndex; i < slices.length; i++) {