From ea3fdb5e311cbc918dffbd1e1c4dbddaae1c8048 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sat, 1 Aug 2026 14:45:35 -0400 Subject: [PATCH] Give the frameTable a lib column. In the past, the way to get the library for a frame would be to go frame -> func -> resource -> lib. For native frames, the resource is of type Library and points at the lib. This had the following implications: - Native frames cannot ever have a resource of a non-Library type. - If two native frames want to share a func, they must also share the same library - otherwise they'll be in a different resource, and different resource means different func. This makes it hard to represent a few cases: - JS JIT frames with assembly. To have assembly, we must have a native library, but we also want JIT frames to have a JS file as their resource, and we want JITted frames for a JS function to share the func with non-JITted frames (interpreter frames) of the same JS function. - Comparison profiles / the "diff" thread for different builds, with assembly code. For example if you have two Firefox builds, one with and one without a patch, they'll have different libxul.so libraries, but you still want to combine C++ functions of the same name in the diffed tree. - Resources for Rust crates (not implemented here): We might want to assign different Rust frames from the same native binary to different resources, with one resource per Rust crate. This commit makes it so that frames now point at their library directly, and removes the lib column from the resourceTable. A resource of type Library only carries the library's name; several libs can share one. mergeLibs used to key on name + debugName, which collapsed two builds of the same library into one lib (and one resource). Now it keeps the libs separate but still collapses the resources by name. For func-only contexts there is no longer a func -> lib edge. formatFunctionNameWithLibrary and the new getLibNameForFunc read the name off the func's resource, which is all they ever needed. profile-query's functionInfo, which wants the full Lib record, scans the frame table once per query. --- docs-developer/CHANGELOG-formats.md | 8 + .../test/unit/call-tree-formatting.test.ts | 6 +- src/app-logic/constants.ts | 2 +- src/profile-logic/bottom-box.ts | 47 ++- src/profile-logic/data-structures.ts | 5 +- src/profile-logic/global-data-collector.ts | 37 +-- src/profile-logic/import/chrome.ts | 1 + src/profile-logic/import/dhat.ts | 2 + src/profile-logic/import/flame-graph.ts | 1 + src/profile-logic/import/simpleperf.ts | 2 +- src/profile-logic/insert-stack-labels.ts | 1 + src/profile-logic/js-tracer.ts | 1 + src/profile-logic/merge-compare.ts | 43 +-- src/profile-logic/process-profile.ts | 47 ++- .../processed-profile-versioning.ts | 23 ++ src/profile-logic/profile-compacting.ts | 40 ++- src/profile-logic/profile-data.ts | 12 +- src/profile-logic/sanitize.ts | 2 - src/profile-logic/symbolication.ts | 153 +++++---- src/profile-query/formatters/call-tree.ts | 11 +- src/profile-query/formatters/marker-info.ts | 18 +- src/profile-query/formatters/thread-info.ts | 19 +- src/profile-query/function-annotate.ts | 8 +- src/profile-query/function-list.ts | 77 +++-- src/profile-query/index.ts | 16 +- src/profile-query/types.ts | 4 + src/test/fixtures/profiles/call-nodes.ts | 1 + .../fixtures/profiles/processed-profile.ts | 16 +- .../__snapshots__/profiler-edit.test.ts.snap | 216 ++++++++++-- .../__snapshots__/profile-view.test.ts.snap | 74 ++++- src/test/store/symbolication.test.ts | 4 +- .../process-profile.test.ts.snap | 2 +- .../profile-conversion.test.ts.snap | 36 +- .../profile-upgrading.test.ts.snap | 314 ++++++++++++++++-- src/test/unit/merge-compare.test.ts | 64 +++- src/test/unit/process-profile.test.ts | 27 +- src/test/unit/profile-query/call-tree.test.ts | 45 +-- .../unit/profile-query/function-list.test.ts | 6 +- src/test/unit/profile-tree.test.ts | 1 - src/test/unit/sanitize.test.ts | 24 ++ src/types/profile-derived.ts | 10 +- src/types/profile.ts | 27 +- 42 files changed, 1051 insertions(+), 402 deletions(-) diff --git a/docs-developer/CHANGELOG-formats.md b/docs-developer/CHANGELOG-formats.md index 6d1ed2b5c8..ecd794b08c 100644 --- a/docs-developer/CHANGELOG-formats.md +++ b/docs-developer/CHANGELOG-formats.md @@ -6,6 +6,14 @@ Note that this is not an exhaustive list. Processed profile format upgraders can ## Processed profile format +### Version 70 + +The `lib` column moved off the `resourceTable` and onto the `frameTable`. Previously a frame's library was reached indirectly, via `frame -> func -> resource -> lib`; now the frame points at its library directly, and `resourceTable` has no `lib` column at all. Frames with no library have their lib set to the `-1` sentinel. + +This decouples resources from libraries. + +The `lib` column can optionally be stored as an `Int32Array`, for profiles loaded from [JsonSlabs](https://github.com/mstange/json-slabs/) files (.jslb, .jslb.gz). Regular JS / JSON arrays are still accepted - but note that `-1` (not `null`) must be used regardless of format. + ### Version 69 A new marker schema display location, `timeline-network`, was added. A marker schema can list `timeline-network` in its `display` array to have markers of that type surfaced in the Network track. diff --git a/profiler-cli/src/test/unit/call-tree-formatting.test.ts b/profiler-cli/src/test/unit/call-tree-formatting.test.ts index db77920db4..28f0da1343 100644 --- a/profiler-cli/src/test/unit/call-tree-formatting.test.ts +++ b/profiler-cli/src/test/unit/call-tree-formatting.test.ts @@ -52,9 +52,8 @@ function buildTopDownResult( const state = store.getState(); const threadSelectors = getThreadSelectors(0); const callTree = threadSelectors.getCallTree(state); - const libs = profile.libs; - const regularCallTree = collectCallTree(callTree, libs, options); + const regularCallTree = collectCallTree(callTree, options); return { type: 'thread-samples-top-down', @@ -76,7 +75,6 @@ function buildBottomUpResult( const store = storeWithProfile(profile); const state = store.getState(); const threadSelectors = getThreadSelectors(0); - const libs = profile.libs; // Build inverted call tree (bottom-up view) let collectedInvertedTree = null; @@ -118,7 +116,7 @@ function buildBottomUpResult( weightType ); - collectedInvertedTree = collectCallTree(invertedTree, libs, options); + collectedInvertedTree = collectCallTree(invertedTree, options); } catch (e) { // Failed to create inverted tree console.error('Failed to create inverted call tree:', e); diff --git a/src/app-logic/constants.ts b/src/app-logic/constants.ts index 1f6b47f2d5..f8a4d98330 100644 --- a/src/app-logic/constants.ts +++ b/src/app-logic/constants.ts @@ -12,7 +12,7 @@ export const GECKO_PROFILE_VERSION = 36; // 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 = 69; +export const PROCESSED_PROFILE_VERSION = 70; // 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/bottom-box.ts b/src/profile-logic/bottom-box.ts index cbd935ed8f..0be9735f00 100644 --- a/src/profile-logic/bottom-box.ts +++ b/src/profile-logic/bottom-box.ts @@ -2,12 +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 { ResourceType } from 'firefox-profiler/types'; - import type { Thread, IndexIntoStackTable, IndexIntoCallNodeTable, + IndexIntoLibs, BottomBoxInfo, SamplesLikeTable, } from 'firefox-profiler/types'; @@ -37,14 +36,8 @@ export function getBottomBoxInfoForCallNode( thread: Thread, samples: SamplesLikeTable ): BottomBoxInfo { - const { - stackTable, - frameTable, - funcTable, - stringTable, - resourceTable, - nativeSymbols, - } = thread; + const { stackTable, frameTable, funcTable, stringTable, nativeSymbols } = + thread; const funcIndex = callNodeInfo.funcForNode(callNodeIndex); const { source: sourceIndex, line: funcLine } = getOriginalPositionForFrame( @@ -54,17 +47,24 @@ export function getBottomBoxInfoForCallNode( funcTable, thread.sourceLocationTable ); - const resource = funcTable.resource[funcIndex]; - const libIndex = - resource !== -1 && resourceTable.type[resource] === ResourceType.Library - ? resourceTable.lib[resource] - : null; const callNodeFramePerStack = getCallNodeFramePerStack( callNodeIndex, callNodeInfo, stackTable ); + // All frames of a call node share the same func, but in principle they can + // come from different libraries (e.g. two builds of the same library in a + // comparison profile). Use the library of the first frame that has one. + let libIndex: IndexIntoLibs | null = null; + for (const frameIndex of callNodeFramePerStack.values()) { + const frameLib = frameTable.lib[frameIndex]; + if (frameLib !== -1) { + libIndex = frameLib; + break; + } + } + // If we have at least one native symbol to show assembly for, pick // the one with the highest total. But first, create the full list of // native symbols for this call node, including even those symbols @@ -148,14 +148,8 @@ export function getBottomBoxInfoForStackFrame( stackIndex: IndexIntoStackTable, thread: Thread ): BottomBoxInfo { - const { - stackTable, - frameTable, - funcTable, - resourceTable, - nativeSymbols, - stringTable, - } = thread; + const { stackTable, frameTable, funcTable, nativeSymbols, stringTable } = + thread; const frameIndex = stackTable.frame[stackIndex]; const funcIndex = frameTable.func[frameIndex]; @@ -166,11 +160,8 @@ export function getBottomBoxInfoForStackFrame( funcTable, thread.sourceLocationTable ); - const resource = funcTable.resource[funcIndex]; - const libIndex = - resource !== -1 && resourceTable.type[resource] === ResourceType.Library - ? resourceTable.lib[resource] - : null; + const frameLib = frameTable.lib[frameIndex]; + const libIndex = frameLib !== -1 ? frameLib : null; // Get native symbol for this frame const nativeSymbol = frameTable.nativeSymbol[frameIndex]; diff --git a/src/profile-logic/data-structures.ts b/src/profile-logic/data-structures.ts index 1aa38d66fd..8be28b7a2c 100644 --- a/src/profile-logic/data-structures.ts +++ b/src/profile-logic/data-structures.ts @@ -35,6 +35,7 @@ import type { IndexIntoSubcategoryListForCategory, IndexIntoNativeSymbolTable, IndexIntoSourceLocationTable, + IndexIntoLibs, InnerWindowID, Address, Bytes, @@ -117,6 +118,7 @@ export type RawFrameTableBuilder = { category: (IndexIntoCategoryList | null)[]; subcategory: (IndexIntoSubcategoryListForCategory | null)[]; func: IndexIntoFuncTable[]; + lib: Array; nativeSymbol: (IndexIntoNativeSymbolTable | null)[]; innerWindowID: (InnerWindowID | null)[]; line: (number | null)[]; @@ -289,6 +291,7 @@ export function getRawFrameTableBuilder(): RawFrameTableBuilder { category: [], subcategory: [], func: [], + lib: [], nativeSymbol: [], innerWindowID: [], line: [], @@ -311,6 +314,7 @@ export function getRawFrameTableBuilderWithExistingContents( category: frameTable.category.slice(), subcategory: frameTable.subcategory.slice(), func: Array.from(frameTable.func), + lib: Array.from(frameTable.lib), nativeSymbol: frameTable.nativeSymbol.slice(), innerWindowID: frameTable.innerWindowID.slice(), line: frameTable.line.slice(), @@ -409,7 +413,6 @@ export function getEmptyResourceTable(): ResourceTable { // 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. - lib: [], name: [], host: [], type: [], diff --git a/src/profile-logic/global-data-collector.ts b/src/profile-logic/global-data-collector.ts index 03333b58c7..a614eed8a9 100644 --- a/src/profile-logic/global-data-collector.ts +++ b/src/profile-logic/global-data-collector.ts @@ -61,8 +61,6 @@ export class GlobalDataCollector { _funcKeyToFuncIndex: Map = new Map(); _nativeSymbolKeyToNativeSymbolIndex: Map = new Map(); - _libIndexToResourceIndex: Map = - new Map(); _libNameToResourceIndex: Map = new Map(); _originToResourceIndex: Map = new Map(); @@ -179,7 +177,6 @@ export class GlobalDataCollector { const idIndex = this._stringTable.indexForString(extensions.id[i]); - resourceTable.lib[resourceIndex] = null; resourceTable.name[resourceIndex] = this._stringTable.indexForString(name); resourceTable.host[resourceIndex] = idIndex; @@ -223,7 +220,6 @@ export class GlobalDataCollector { this._originToResourceIndex.set(origin, resourceIndex); if (host) { // This is a webhost URL. - resourceTable.lib[resourceIndex] = null; resourceTable.name[resourceIndex] = this._stringTable.indexForString(origin); resourceTable.host[resourceIndex] = @@ -232,7 +228,6 @@ export class GlobalDataCollector { } else { // This is a URL, but it doesn't point to something on the web, e.g. a // chrome url. - resourceTable.lib[resourceIndex] = null; resourceTable.name[resourceIndex] = this._stringTable.indexForString(scriptURI); resourceTable.host[resourceIndex] = null; @@ -241,25 +236,9 @@ export class GlobalDataCollector { return resourceIndex; } - indexForLibResource(libIndex: IndexIntoLibs): IndexIntoResourceTable { - let resourceIndex = this._libIndexToResourceIndex.get(libIndex); - if (resourceIndex !== undefined) { - return resourceIndex; - } - - const resourceTable = this._resourceTable; - - resourceIndex = this._resourceTable.length++; - this._libIndexToResourceIndex.set(libIndex, resourceIndex); - resourceTable.lib[resourceIndex] = libIndex; - resourceTable.name[resourceIndex] = this._stringTable.indexForString( - this._libs[libIndex].name - ); - resourceTable.host[resourceIndex] = null; - resourceTable.type[resourceIndex] = ResourceType.Library; - return resourceIndex; - } - + // Returns the resource for a library, identified only by its name. Two + // different libs with the same name share one resource; the frames keep the + // libs apart via their own `lib` column. indexForNameOnlyLibResource( libNameStringIndex: IndexIntoStringTable ): IndexIntoResourceTable { @@ -272,13 +251,21 @@ export class GlobalDataCollector { resourceIndex = this._resourceTable.length++; this._libNameToResourceIndex.set(libNameStringIndex, resourceIndex); - resourceTable.lib[resourceIndex] = null; resourceTable.name[resourceIndex] = libNameStringIndex; resourceTable.host[resourceIndex] = null; resourceTable.type[resourceIndex] = ResourceType.Library; return resourceIndex; } + // Same as indexForNameOnlyLibResource, for when you have a lib index rather + // than just a name. Note that the resulting resource only records the lib's + // name; the lib itself belongs on the frame. + indexForLibResource(libIndex: IndexIntoLibs): IndexIntoResourceTable { + return this.indexForNameOnlyLibResource( + this._stringTable.indexForString(this._libs[libIndex].name) + ); + } + indexForNativeSymbol( libIndex: IndexIntoLibs, address: Address, diff --git a/src/profile-logic/import/chrome.ts b/src/profile-logic/import/chrome.ts index 0a448666e0..eba5e6879c 100644 --- a/src/profile-logic/import/chrome.ts +++ b/src/profile-logic/import/chrome.ts @@ -699,6 +699,7 @@ async function processTracingEvents( frameTable.category[frameIndex] = category; frameTable.subcategory[frameIndex] = 0; frameTable.func[frameIndex] = funcId; + frameTable.lib[frameIndex] = -1; frameTable.nativeSymbol[frameIndex] = null; frameTable.innerWindowID[frameIndex] = 0; frameTable.line[frameIndex] = diff --git a/src/profile-logic/import/dhat.ts b/src/profile-logic/import/dhat.ts index e0dd3f240c..94c9cc859e 100644 --- a/src/profile-logic/import/dhat.ts +++ b/src/profile-logic/import/dhat.ts @@ -212,6 +212,7 @@ export function attemptToConvertDhat(json: unknown): Profile | null { frameTable.category.push(otherCategory); frameTable.subcategory.push(otherSubCategory); frameTable.innerWindowID.push(null); + frameTable.lib.push(-1); frameTable.nativeSymbol.push(null); frameTable.inlineDepth.push(0); frameTable.func.push(rootFuncIndex); @@ -279,6 +280,7 @@ export function attemptToConvertDhat(json: unknown): Profile | null { frameTable.category.push(otherCategory); frameTable.subcategory.push(otherSubCategory); frameTable.innerWindowID.push(null); + frameTable.lib.push(-1); frameTable.nativeSymbol.push(null); frameTable.inlineDepth.push(0); frameTable.func.push(funcIndex); diff --git a/src/profile-logic/import/flame-graph.ts b/src/profile-logic/import/flame-graph.ts index 6a7e4a1fff..71504573b1 100644 --- a/src/profile-logic/import/flame-graph.ts +++ b/src/profile-logic/import/flame-graph.ts @@ -120,6 +120,7 @@ export function convertFlameGraphProfile(profileText: string): Profile { frameTable.category.push(category); frameTable.subcategory.push(0); frameTable.func.push(funcIndex); + frameTable.lib.push(-1); frameTable.nativeSymbol.push(null); frameTable.innerWindowID.push(null); frameTable.line.push(null); diff --git a/src/profile-logic/import/simpleperf.ts b/src/profile-logic/import/simpleperf.ts index 75fce1f0d9..39be0d9fb2 100644 --- a/src/profile-logic/import/simpleperf.ts +++ b/src/profile-logic/import/simpleperf.ts @@ -99,7 +99,6 @@ class FirefoxResourceTable { findOrAddResource(file: report.IFile): IndexIntoResourceTable { let resourceIndex = this.resourcesMap.get(file.id!); if (!resourceIndex) { - this.resourceTable.lib.push(null); this.resourceTable.name.push(this.strings.indexForString(file.path!)); this.resourceTable.host.push(null); this.resourceTable.type.push(1); // Library @@ -177,6 +176,7 @@ class FirefoxFrameTable { this.frameTable.category.push(category); this.frameTable.subcategory.push(0); this.frameTable.func.push(funcIndex); + this.frameTable.lib.push(-1); this.frameTable.nativeSymbol.push(null); this.frameTable.innerWindowID.push(null); this.frameTable.line.push(null); diff --git a/src/profile-logic/insert-stack-labels.ts b/src/profile-logic/insert-stack-labels.ts index e614569e82..988a30cb23 100644 --- a/src/profile-logic/insert-stack-labels.ts +++ b/src/profile-logic/insert-stack-labels.ts @@ -136,6 +136,7 @@ export function insertStackLabels( frameTable.func[frameIndex] = funcIndex; frameTable.category[frameIndex] = labelCategoryIndex; frameTable.subcategory[frameIndex] = 0; + frameTable.lib[frameIndex] = -1; frameTable.nativeSymbol[frameIndex] = null; frameTable.address[frameIndex] = 0; frameTable.inlineDepth[frameIndex] = 0; diff --git a/src/profile-logic/js-tracer.ts b/src/profile-logic/js-tracer.ts index 92ef451f82..2ad48571c2 100644 --- a/src/profile-logic/js-tracer.ts +++ b/src/profile-logic/js-tracer.ts @@ -610,6 +610,7 @@ export function convertJsTracerToThreadWithoutSamples( frameTable.inlineDepth.push(0); frameTable.category.push(otherCategory); frameTable.func.push(funcIndex); + frameTable.lib.push(-1); frameTable.nativeSymbol.push(null); frameTable.innerWindowID.push(0); frameTable.line.push(line); diff --git a/src/profile-logic/merge-compare.ts b/src/profile-logic/merge-compare.ts index 10e6a3fd14..e115b2fcb8 100644 --- a/src/profile-logic/merge-compare.ts +++ b/src/profile-logic/merge-compare.ts @@ -461,11 +461,7 @@ export function mergeSharedData(profiles: Profile[]): { const { resourceTable: newResourceTable, translationMaps: translationMapsForResources, - } = mergeResourceTables( - profiles, - translationMapsForStrings, - translationMapsForLibs - ); + } = mergeResourceTables(profiles, translationMapsForStrings); const { nativeSymbols: newNativeSymbols, translationMaps: translationMapsForNativeSymbols, @@ -490,7 +486,8 @@ export function mergeSharedData(profiles: Profile[]): { translationMapsForFuncs, translationMapsForNativeSymbols, translationMapsForOriginalLocation, - translationMapsForCategories + translationMapsForCategories, + translationMapsForLibs ); const { stackTable: newStackTable, @@ -721,7 +718,12 @@ function mergeLibs(libsPerProfile: Lib[][]): { const oldLibToNewLibPlusOne = new Int32Array(libs.length); libs.forEach((lib, i) => { - const insertedLibKey = [lib.name, lib.debugName].join('#'); + // Two builds of the same library have the same name and debugName but + // different breakpadIds. They must stay separate libs, because symbols + // have to be looked up separately for each build. Frames keep track of + // which build they came from via frameTable.lib; the two builds still + // share a single resource, which only carries the name. + const insertedLibKey = [lib.debugName, lib.breakpadId].join('#'); const insertedLibIndex = mapOfInsertedLibs.get(insertedLibKey); if (insertedLibIndex !== undefined) { oldLibToNewLibPlusOne[i] = insertedLibIndex + 1; @@ -747,11 +749,14 @@ function _mapLib( return oldLibToNewLibPlusOne[libIndex] - 1; } -function _mapNullableLib( - libIndex: IndexIntoLibs | null, +function _mapFrameLib( + libIndex: IndexIntoLibs | -1, oldLibToNewLibPlusOne: TranslationMapForLibs -): IndexIntoLibs | null { - return libIndex !== null ? oldLibToNewLibPlusOne[libIndex] - 1 : null; +): IndexIntoLibs | -1 { + if (libIndex === -1) { + return -1; + } + return oldLibToNewLibPlusOne[libIndex] - 1; } function _mapString( @@ -846,8 +851,7 @@ function _mapNullableStack( */ function mergeResourceTables( profiles: ReadonlyArray, - translationMapsForStrings: TranslationMapForStrings[], - translationMapsForLibs: TranslationMapForLibs[] + translationMapsForStrings: TranslationMapForStrings[] ): { resourceTable: ResourceTable; translationMaps: TranslationMapForResources[]; @@ -857,7 +861,6 @@ function mergeResourceTables( const newResourceTable = getEmptyResourceTable(); profiles.forEach((profile, profileIndex) => { - const oldLibToNewLibPlusOne = translationMapsForLibs[profileIndex]; const oldStringToNewStringPlusOne = translationMapsForStrings[profileIndex]; const { resourceTable } = profile.shared; const oldResourceToNewResourcePlusOne = new Int32Array( @@ -865,10 +868,6 @@ function mergeResourceTables( ); for (let i = 0; i < resourceTable.length; i++) { - const libIndex = _mapNullableLib( - resourceTable.lib[i], - oldLibToNewLibPlusOne - ); const nameIndex = _mapString( resourceTable.name[i], oldStringToNewStringPlusOne @@ -890,7 +889,6 @@ function mergeResourceTables( oldResourceToNewResourcePlusOne[i] = newResourceTable.length + 1; mapOfInsertedResources.set(resourceKey, newResourceTable.length); - newResourceTable.lib.push(libIndex); newResourceTable.name.push(nameIndex); newResourceTable.host.push(hostIndex); newResourceTable.type.push(type); @@ -1059,7 +1057,8 @@ function mergeFrameTables( translationMapsForFuncs: TranslationMapForFuncs[], translationMapsForNativeSymbols: TranslationMapForNativeSymbols[], translationMapsForOriginalLocation: TranslationMapForOriginalLocation[], - translationMapsForCategories: TranslationMapForCategories[] + translationMapsForCategories: TranslationMapForCategories[], + translationMapsForLibs: TranslationMapForLibs[] ): { frameTable: RawFrameTable; translationMaps: TranslationMapForFrames[] } { const translationMaps: TranslationMapForFrames[] = []; const newFrameTable = getRawFrameTableBuilder(); @@ -1073,6 +1072,7 @@ function mergeFrameTables( translationMapsForOriginalLocation[profileIndex]; const oldCategoryToNewCategoryPlusOne = translationMapsForCategories[profileIndex]; + const oldLibToNewLibPlusOne = translationMapsForLibs[profileIndex]; const oldFrameToNewFramePlusOne = new Int32Array(frameTable.length); for (let i = 0; i < frameTable.length; i++) { @@ -1094,6 +1094,9 @@ function mergeFrameTables( newFrameTable.subcategory.push(subcategory); newFrameTable.nativeSymbol.push(nativeSymbol); newFrameTable.func.push(func); + newFrameTable.lib.push( + _mapFrameLib(frameTable.lib[i], oldLibToNewLibPlusOne) + ); newFrameTable.innerWindowID.push(frameTable.innerWindowID[i]); newFrameTable.line.push(frameTable.line[i]); newFrameTable.column.push(frameTable.column[i]); diff --git a/src/profile-logic/process-profile.ts b/src/profile-logic/process-profile.ts index 69c4712e7a..f64effd1fc 100644 --- a/src/profile-logic/process-profile.ts +++ b/src/profile-logic/process-profile.ts @@ -116,6 +116,7 @@ import type { GeckoSourceTable, IndexIntoCategoryList, IndexIntoFrameTable, + IndexIntoLibs, CounterDisplayConfig, RawProfileSharedData, } from 'firefox-profiler/types'; @@ -202,7 +203,11 @@ type ExtractionInfo = { addressLocator: AddressLocator; stringToNewFuncIndexAndFrameAddress: Map< string, - { funcIndex: IndexIntoFuncTable; frameAddress: Address | null } + { + funcIndex: IndexIntoFuncTable; + frameAddress: Address | null; + libIndex: IndexIntoLibs | null; + } >; globalDataCollector: GlobalDataCollector; geckoSourceTable: GeckoSourceTable; @@ -226,6 +231,7 @@ export function extractFuncsAndResourcesFromFrameLocations( ): { frameFuncs: IndexIntoFuncTable[]; frameAddresses: (Address | null)[]; + frameLibs: (IndexIntoLibs | null)[]; } { const stringTable = globalDataCollector.getStringTable(); @@ -242,6 +248,7 @@ export function extractFuncsAndResourcesFromFrameLocations( // information by applying various string matching heuristics. const frameFuncs = []; const frameAddresses = []; + const frameLibs: (IndexIntoLibs | null)[] = []; for (let frameIndex = 0; frameIndex < frameLocations.length; frameIndex++) { const originalLocationIndex = frameLocations[frameIndex]; const locationString = ensureExists( @@ -253,9 +260,10 @@ export function extractFuncsAndResourcesFromFrameLocations( extractionInfo.stringToNewFuncIndexAndFrameAddress.get(locationString); if (info !== undefined) { // The location string was already processed. - const { funcIndex, frameAddress } = info; + const { funcIndex, frameAddress, libIndex } = info; frameFuncs.push(funcIndex); frameAddresses.push(frameAddress); + frameLibs.push(libIndex); continue; } @@ -263,6 +271,7 @@ export function extractFuncsAndResourcesFromFrameLocations( // resource information. let funcIndex = null; let frameAddress = null; + let libIndex: IndexIntoLibs | null = null; const unsymbolicatedInfo = _extractUnsymbolicatedFunction( extractionInfo, locationString, @@ -271,6 +280,7 @@ export function extractFuncsAndResourcesFromFrameLocations( if (unsymbolicatedInfo !== null) { funcIndex = unsymbolicatedInfo.funcIndex; frameAddress = unsymbolicatedInfo.frameAddress; + libIndex = unsymbolicatedInfo.libIndex; } else { funcIndex = _extractCppFunction(extractionInfo, locationString); if (funcIndex === null) { @@ -289,15 +299,18 @@ export function extractFuncsAndResourcesFromFrameLocations( extractionInfo.stringToNewFuncIndexAndFrameAddress.set(locationString, { funcIndex, frameAddress, + libIndex, }); frameFuncs.push(funcIndex); frameAddresses.push(frameAddress); + frameLibs.push(libIndex); } return { frameFuncs, frameAddresses, + frameLibs, }; } @@ -308,23 +321,28 @@ export function extractFuncsAndResourcesFromFrameLocations( * into the same function, so cannot do any function grouping. So we get one "function" per * address. * We also associate the address with the library that contains it, and convert the address - * into a library-relative offset. This association is established via the function's - * "resource": The function points to the resource (of type ResourceType.Library), and the - * resource has the index to the library in thread.libs. - * We return the index of the newly-added function, and the address as a library-relative - * offset. + * into a library-relative offset. The library index goes onto the frame (frameTable.lib); + * the function additionally gets a "resource" of type ResourceType.Library, which names + * the library but does not identify it. + * We return the index of the newly-added function, the library, and the address as a + * library-relative offset. */ function _extractUnsymbolicatedFunction( extractionInfo: ExtractionInfo, locationString: string, locationIndex: IndexIntoStringTable -): { funcIndex: IndexIntoFuncTable; frameAddress: Address } | null { +): { + funcIndex: IndexIntoFuncTable; + frameAddress: Address; + libIndex: IndexIntoLibs | null; +} | null { if (!locationString.startsWith('0x')) { return null; } const { addressLocator, globalDataCollector } = extractionInfo; let resourceIndex = -1; + let libIndex: IndexIntoLibs | null = null; let addressRelativeToLib: Address = -1; try { @@ -343,7 +361,7 @@ function _extractUnsymbolicatedFunction( // Yes, we found the library whose mapping covers this address! addressRelativeToLib = relativeAddress; - const libIndex = globalDataCollector.indexForLib(lib); + libIndex = globalDataCollector.indexForLib(lib); resourceIndex = globalDataCollector.indexForLibResource(libIndex); } } catch (_e) { @@ -359,7 +377,7 @@ function _extractUnsymbolicatedFunction( null, null ); - return { funcIndex, frameAddress: addressRelativeToLib }; + return { funcIndex, frameAddress: addressRelativeToLib, libIndex }; } /** @@ -535,7 +553,8 @@ function _processFrameTable( geckoFrameStruct: GeckoFrameStruct, sharedFrameTable: RawFrameTableBuilder, frameFuncs: IndexIntoFuncTable[], - frameAddresses: (Address | null)[] + frameAddresses: (Address | null)[], + frameLibs: (IndexIntoLibs | null)[] ): IndexIntoFrameTable { const frameIndexOffset = sharedFrameTable.length; for (let i = 0; i < geckoFrameStruct.length; i++) { @@ -545,6 +564,7 @@ function _processFrameTable( sharedFrameTable.category[newIndex] = geckoFrameStruct.category[i]; sharedFrameTable.subcategory[newIndex] = geckoFrameStruct.subcategory[i]; sharedFrameTable.func[newIndex] = frameFuncs[i]; + sharedFrameTable.lib[newIndex] = frameLibs[i] ?? -1; sharedFrameTable.nativeSymbol[newIndex] = null; sharedFrameTable.innerWindowID[newIndex] = geckoFrameStruct.innerWindowID[i]; @@ -1327,7 +1347,7 @@ function _processThread( const { libs, pausedRanges, meta, sources } = processProfile; const { shutdownTime } = meta; - const { frameFuncs, frameAddresses } = + const { frameFuncs, frameAddresses, frameLibs } = extractFuncsAndResourcesFromFrameLocations( geckoFrameStruct.location, geckoFrameStruct.relevantForJS, @@ -1341,7 +1361,8 @@ function _processThread( geckoFrameStruct, globalDataCollector.getFrameTable(), frameFuncs, - frameAddresses + frameAddresses, + frameLibs ); const stackIndexOffset = _processStackTable( geckoStackTable, diff --git a/src/profile-logic/processed-profile-versioning.ts b/src/profile-logic/processed-profile-versioning.ts index 0999f5c078..89d5e4e2f9 100644 --- a/src/profile-logic/processed-profile-versioning.ts +++ b/src/profile-logic/processed-profile-versioning.ts @@ -3310,6 +3310,29 @@ const _upgraders: { } } }, + [70]: (profile: any) => { + // The `lib` column moved off the resourceTable and onto the frameTable, so + // that resources and libraries can vary independently: multiple libs can + // now share one resource. + // + // Before: frame -> func -> resource -> lib + // After: frame -> lib + // Frames with no library use the sentinel value -1. + const { frameTable, funcTable, resourceTable } = profile.shared; + const libForFunc = new Int32Array(funcTable.length).fill(-1); + for (let funcIndex = 0; funcIndex < funcTable.length; funcIndex++) { + const resourceIndex = funcTable.resource[funcIndex]; + if (resourceIndex !== -1) { + libForFunc[funcIndex] = resourceTable.lib[resourceIndex] ?? -1; + } + } + const lib = new Array(frameTable.length).fill(-1); + for (let frameIndex = 0; frameIndex < frameTable.length; frameIndex++) { + lib[frameIndex] = libForFunc[frameTable.func[frameIndex]]; + } + frameTable.lib = lib; + delete resourceTable.lib; + }, // 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 9adb8404a7..d5b6f91a10 100644 --- a/src/profile-logic/profile-compacting.ts +++ b/src/profile-logic/profile-compacting.ts @@ -50,6 +50,10 @@ type ColumnDescription = null extends ( : Int32Array extends TCol ? | { type: 'INDEX_REF_INT32'; referencedTable: TableCompactionState } + | { + type: 'INDEX_REF_OR_NEG_ONE_INT32'; + referencedTable: TableCompactionState; + } | { type: 'SELF_RELATIVE_PARENT' } | { type: 'NO_REF' } : @@ -86,6 +90,10 @@ const ColDesc = { type: 'INDEX_REF_OR_NEG_ONE' as const, referencedTable, }), + indexRefOrNegOneInt32: (referencedTable: TableCompactionState) => ({ + type: 'INDEX_REF_OR_NEG_ONE_INT32' as const, + referencedTable, + }), selfPrefixOffset: () => ({ type: 'SELF_RELATIVE_PARENT' as const }), noRef: () => ({ type: 'NO_REF' as const }), }; @@ -168,6 +176,7 @@ export function computeCompactedProfile( category: ColDesc.noRef(), subcategory: ColDesc.noRef(), func: ColDesc.indexRefInt32(tcs.funcTable), + lib: ColDesc.indexRefOrNegOneInt32(tcs.libs), nativeSymbol: ColDesc.indexRefOrNull(tcs.nativeSymbols), innerWindowID: ColDesc.noRef(), line: ColDesc.noRef(), @@ -192,7 +201,6 @@ export function computeCompactedProfile( const resourceTableDesc: TableDescription = { name: ColDesc.indexRef(tcs.stringArray), host: ColDesc.indexRefOrNull(tcs.stringArray), - lib: ColDesc.indexRefOrNull(tcs.libs), type: ColDesc.noRef(), }; const nativeSymbolsDesc: TableDescription = { @@ -353,6 +361,7 @@ function _markTableAndComputeTranslation( case 'SELF_RELATIVE_PARENT': break; // already handled in the first pass case 'INDEX_REF_OR_NEG_ONE': + case 'INDEX_REF_OR_NEG_ONE_INT32': markColumnWithNegOneableFields( col, markBuffer, @@ -417,10 +426,12 @@ function markSelfColumnPrefixOffset( } function markColumnWithNegOneableFields( - col: Array, + col: Array | Int32Array, shouldMark: BitSet, markBuf: BitSet ) { + // Polymorphic: indexing works the same on Int32Array as on number[], so the + // INDEX_REF_OR_NEG_ONE and INDEX_REF_OR_NEG_ONE_INT32 cases share this function. for (let i = 0; i < col.length; i++) { if (checkBit(shouldMark, i)) { const val = col[i]; @@ -552,6 +563,14 @@ function _compactTable( newLength ); break; + case 'INDEX_REF_OR_NEG_ONE_INT32': + result[key] = _compactColIndexOrNegOneInt32( + oldCol, + markBuffer, + desc.referencedTable.oldIndexToNewIndexPlusOne, + newLength + ); + break; case 'NO_REF': result[key] = _compactColCopy(oldCol, markBuffer, newLength); break; @@ -644,6 +663,23 @@ function _compactColIndexOrNegOne( return newCol; } +function _compactColIndexOrNegOneInt32( + oldCol: Array | Int32Array, + markBuffer: BitSet, + oldIndexToNewIndexPlusOne: Int32Array, + newLength: number +): Int32Array { + const newCol = new Int32Array(newLength); + let newIndex = 0; + for (let i = 0; i < oldCol.length; i++) { + if (checkBit(markBuffer, i)) { + const val = oldCol[i]; + newCol[newIndex++] = val !== -1 ? oldIndexToNewIndexPlusOne[val] - 1 : -1; + } + } + return newCol; +} + function _compactColSelfPrefixOffset( oldCol: Int32Array, markBuffer: BitSet, diff --git a/src/profile-logic/profile-data.ts b/src/profile-logic/profile-data.ts index be33781b78..1dd01f3f64 100644 --- a/src/profile-logic/profile-data.ts +++ b/src/profile-logic/profile-data.ts @@ -4358,6 +4358,7 @@ export function nudgeReturnAddresses(profile: Profile): Profile { newFrameTable.category.push(frameTable.category[frame]); newFrameTable.subcategory.push(frameTable.subcategory[frame]); newFrameTable.func.push(frameTable.func[frame]); + newFrameTable.lib.push(frameTable.lib[frame]); newFrameTable.nativeSymbol.push(frameTable.nativeSymbol[frame]); newFrameTable.innerWindowID.push(frameTable.innerWindowID[frame]); newFrameTable.line.push(frameTable.line[frame]); @@ -4453,7 +4454,7 @@ export function findAddressProofForFile( sourceIndex: IndexIntoSourceTable ): AddressProof | null { const { libs } = profile; - const { frameTable, funcTable, resourceTable } = profile.shared; + const { frameTable, funcTable } = profile.shared; const func = funcTable.source.indexOf(sourceIndex); if (func === -1) { return null; @@ -4466,12 +4467,8 @@ export function findAddressProofForFile( if (address === null) { return null; } - const resource = funcTable.resource[func]; - if (resourceTable.type[resource] !== ResourceType.Library) { - return null; - } - const libIndex = resourceTable.lib[resource]; - if (libIndex === null) { + const libIndex = frameTable.lib[frame]; + if (libIndex === -1) { return null; } const lib = libs[libIndex]; @@ -4768,6 +4765,7 @@ export function computeFrameTableFromRawFrameTable( category: rawFrameTable.category, subcategory: rawFrameTable.subcategory, func, + lib: toInt32Array(rawFrameTable.lib), nativeSymbol: rawFrameTable.nativeSymbol, innerWindowID: rawFrameTable.innerWindowID, line: rawFrameTable.line, diff --git a/src/profile-logic/sanitize.ts b/src/profile-logic/sanitize.ts index 9bba0aedc4..81a78c1032 100644 --- a/src/profile-logic/sanitize.ts +++ b/src/profile-logic/sanitize.ts @@ -257,7 +257,6 @@ export function sanitizePII( if (resourcesToBeSanitized.size) { const newResourceTable = (newShared.resourceTable = { ...resourceTable, - lib: resourceTable.lib.slice(), name: resourceTable.name.slice(), host: resourceTable.host.slice(), }); @@ -272,7 +271,6 @@ export function sanitizePII( `` ); newResourceTable.name[resourceIndex] = name; - newResourceTable.lib[resourceIndex] = null; newResourceTable.host[resourceIndex] = null; } } diff --git a/src/profile-logic/symbolication.ts b/src/profile-logic/symbolication.ts index b0c8d1d7e6..ef1ea047e0 100644 --- a/src/profile-logic/symbolication.ts +++ b/src/profile-logic/symbolication.ts @@ -31,7 +31,6 @@ import type { CallNodePath, Lib, } from 'firefox-profiler/types'; -import { ResourceType } from 'firefox-profiler/types'; import type { AbstractSymbolStore, AddressResult, @@ -119,9 +118,13 @@ import { updateRawThreadStacks } from './profile-data'; * It also creates a nativeSymbols table but leaves it completely empty. * - The frame and its func both get their address field set to the * library-relative offset. - * - The func's resource field is set to a resource of type "library" that - * points to the lib object in the thread's "libs" list that contained this - * address. The frame's and func's address fields are relative to that lib. + * - The frame's lib field is set to the index of the containing library in + * profile.libs. The frame's and func's address fields are relative to that + * lib. Symbolication groups frames by this field. + * - The func's resource field is set to a resource of type "library". The + * resource names the library, but it does not identify it: several libs can + * share one resource, for example two builds of the same library in a + * comparison profile. * - All frames start out with their nativeSymbol field set to null. * - All return addresses are adjusted by subtracting one byte, to point into * the call instruction. See nudgeReturnAddresses for details. @@ -208,8 +211,9 @@ export type SymbolicationStepCallback = ( ) => void; type ProfileLibSymbolicationInfo = { - // The resourceIndex for this lib in this thread. - resourceIndex: IndexIntoResourceTable; + // The resource to give to funcs which are newly created for this lib, or -1 + // if this lib's frames have no resource. Multiple libs can share a resource. + resourceIndex: IndexIntoResourceTable | -1; // The libIndex for this lib in this thread. libIndex: IndexIntoLibs; // The set of funcs for this lib in this thread. @@ -283,78 +287,112 @@ function makeConsensusMap( /** * Gather the symbols needed in this thread, and some auxiliary information that * allows the symbol substitation step at the end to work efficiently. - * Returns a map with one entry for each library resource. + * Returns a map with one entry for each library that has frames in the profile. + * + * This makes a small number of passes over the frame, func and native symbol + * tables in total, rather than one pass per library. Profiles routinely have + * hundreds of libraries and millions of frames, so a per-library pass would be + * far too slow. */ function getSymbolicationInfo( shared: RawProfileSharedData, libs: Lib[] ): ProfileSymbolicationInfo { - const { frameTable, funcTable, nativeSymbols, resourceTable } = shared; + const { frameTable, funcTable, nativeSymbols } = shared; - const map = new Map(); - for ( - let resourceIndex = 0; - resourceIndex < resourceTable.length; - resourceIndex++ - ) { - const resourceType = resourceTable.type[resourceIndex]; - if (resourceType !== ResourceType.Library) { + // Pass 1 over the frames: group frames (and their addresses) by library, and + // work out which library each func belongs to. + // + // A func can be referenced by frames from more than one library, for example + // when two builds of the same library share a resource and symbolication has + // resolved frames from both builds to the same name. Such a func must not be + // recycled by either library, so we mark it with FUNC_LIB_SHARED. + const FUNC_LIB_NONE = -1; + const FUNC_LIB_SHARED = -2; + const libForFunc = new Int32Array(funcTable.length).fill(FUNC_LIB_NONE); + const framesByLib = new Map< + IndexIntoLibs, + { frames: IndexIntoFrameTable[]; addresses: Address[] } + >(); + for (let frameIndex = 0; frameIndex < frameTable.length; frameIndex++) { + const libIndex = frameTable.lib[frameIndex]; + if (libIndex === -1) { continue; } - const libIndex = resourceTable.lib[resourceIndex]; - if (libIndex === null) { - // We can get here if we have pre-symbolicated "funcName (in LibraryName)" - // frames. Those get ResourceType.Library but no libIndex. - continue; + let entry = framesByLib.get(libIndex); + if (entry === undefined) { + entry = { frames: [], addresses: [] }; + framesByLib.set(libIndex, entry); } - const lib = libs[libIndex]; - if (lib === undefined) { - throw new Error('Did not find a lib.'); + entry.frames.push(frameIndex); + entry.addresses.push(frameTable.address[frameIndex]); + + const funcIndex = frameTable.func[frameIndex]; + const knownLib = libForFunc[funcIndex]; + if (knownLib === FUNC_LIB_NONE) { + libForFunc[funcIndex] = libIndex; + } else if (knownLib !== libIndex) { + libForFunc[funcIndex] = FUNC_LIB_SHARED; } + } - // Collect the set of funcs for this library in this thread. - const allFuncsForThisLib = new Set(); - for (let funcIndex = 0; funcIndex < funcTable.length; funcIndex++) { - if (funcTable.resource[funcIndex] !== resourceIndex) { - continue; + // Pass over the funcs: assign each func to the library that gets to recycle + // it, and remember one resource per library for the funcs we create later. + const funcsByLib = new Map>(); + const resourceForLib = new Map(); + for (let funcIndex = 0; funcIndex < funcTable.length; funcIndex++) { + const libIndex = libForFunc[funcIndex]; + if (libIndex < 0) { + continue; + } + let funcs = funcsByLib.get(libIndex); + if (funcs === undefined) { + funcs = new Set(); + funcsByLib.set(libIndex, funcs); + } + funcs.add(funcIndex); + if (!resourceForLib.has(libIndex)) { + const resourceIndex = funcTable.resource[funcIndex]; + if (resourceIndex !== -1) { + resourceForLib.set(libIndex, resourceIndex); } - allFuncsForThisLib.add(funcIndex); } + } - // Collect the set of native symbols for this library in this thread. - const allNativeSymbolsForThisLib: Set = - new Set(); - for ( - let nativeSymbolIndex = 0; - nativeSymbolIndex < nativeSymbols.length; - nativeSymbolIndex++ - ) { - if (nativeSymbols.libIndex[nativeSymbolIndex] !== libIndex) { - continue; - } - allNativeSymbolsForThisLib.add(nativeSymbolIndex); + // Pass over the native symbols: group them by library. + const nativeSymbolsByLib = new Map< + IndexIntoLibs, + Set + >(); + for ( + let nativeSymbolIndex = 0; + nativeSymbolIndex < nativeSymbols.length; + nativeSymbolIndex++ + ) { + const libIndex = nativeSymbols.libIndex[nativeSymbolIndex]; + let symbols = nativeSymbolsByLib.get(libIndex); + if (symbols === undefined) { + symbols = new Set(); + nativeSymbolsByLib.set(libIndex, symbols); } + symbols.add(nativeSymbolIndex); + } - // Collect the sets of frames and addresses for this library. - const allFramesForThisLib = []; - const frameAddresses = []; - for (let frameIndex = 0; frameIndex < frameTable.length; frameIndex++) { - const funcIndex = frameTable.func[frameIndex]; - if (funcTable.resource[funcIndex] !== resourceIndex) { - continue; - } - allFramesForThisLib.push(frameIndex); - frameAddresses.push(frameTable.address[frameIndex]); + const map = new Map(); + for (const [libIndex, { frames, addresses }] of framesByLib) { + const lib = libs[libIndex]; + if (lib === undefined) { + throw new Error('Did not find a lib.'); } const libKey = `${lib.debugName}/${lib.breakpadId}`; map.set(libKey, { libIndex, - resourceIndex, - allFuncsForThisLib, - allNativeSymbolsForThisLib, - allFramesForThisLib, - frameAddresses, + resourceIndex: resourceForLib.get(libIndex) ?? -1, + allFuncsForThisLib: funcsByLib.get(libIndex) ?? new Set(), + allNativeSymbolsForThisLib: nativeSymbolsByLib.get(libIndex) ?? new Set(), + allFramesForThisLib: frames, + frameAddresses: addresses, }); } return map; @@ -903,6 +941,7 @@ function _partiallyApplySymbolicationStep( frameTable.subcategory[expansionFrameIndex] = subcategory; frameTable.innerWindowID[expansionFrameIndex] = innerWindowID; frameTable.address[expansionFrameIndex] = address; + frameTable.lib[expansionFrameIndex] = libIndex; frameTable.nativeSymbol[expansionFrameIndex] = nativeSymbolIndex; frameTable.originalLocation[expansionFrameIndex] = null; diff --git a/src/profile-query/formatters/call-tree.ts b/src/profile-query/formatters/call-tree.ts index 5113b81607..fb3a96b26b 100644 --- a/src/profile-query/formatters/call-tree.ts +++ b/src/profile-query/formatters/call-tree.ts @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import type { CallTree } from 'firefox-profiler/profile-logic/call-tree'; -import type { IndexIntoCallNodeTable, Lib } from 'firefox-profiler/types'; +import type { IndexIntoCallNodeTable } from 'firefox-profiler/types'; import { assertExhaustiveCheck } from 'firefox-profiler/utils/types'; import type { CallTreeNode, @@ -172,7 +172,6 @@ export type CallTreeCollectionOptions = { */ export function collectCallTree( tree: CallTree, - libs: Lib[], options: CallTreeCollectionOptions = {} ): CallTreeNode { const maxNodes = options.maxNodes ?? 100; @@ -244,7 +243,7 @@ export function collectCallTree( } } - return buildTreeStructure(tree, includedNodes, libs); + return buildTreeStructure(tree, includedNodes); } /** @@ -252,8 +251,7 @@ export function collectCallTree( */ function buildTreeStructure( tree: CallTree, - includedNodes: Set, - libs: Lib[] + includedNodes: Set ): CallTreeNode { // Get total sample count from the tree for percentage calculations const totalSampleCount = tree.getTotal(); @@ -299,8 +297,7 @@ function buildTreeStructure( // Format function name with library prefix const nameWithLibrary = formatFunctionNameWithLibrary( funcIndex, - tree._thread, - libs + tree._thread ); const inlineStatus = inlineStatusForNode(tree, callNodeIndex); diff --git a/src/profile-query/formatters/marker-info.ts b/src/profile-query/formatters/marker-info.ts index a28b166313..cb0597ab33 100644 --- a/src/profile-query/formatters/marker-info.ts +++ b/src/profile-query/formatters/marker-info.ts @@ -929,8 +929,7 @@ function collectStackTrace( return null; } - const { stackTable, frameTable, funcTable, stringTable, resourceTable } = - thread; + const { stackTable, frameTable, funcTable, stringTable } = thread; const frames: StackTraceData['frames'] = []; let currentStackIndex: IndexIntoStackTable = stackIndex; @@ -938,19 +937,12 @@ function collectStackTrace( const frameIndex = stackTable.frame[currentStackIndex]; const funcIndex = frameTable.func[frameIndex]; const funcName = stringTable.getString(funcTable.name[funcIndex]); - const nameWithLibrary = formatFunctionNameWithLibrary( - funcIndex, - thread, - libs - ); + const nameWithLibrary = formatFunctionNameWithLibrary(funcIndex, thread); let library: string | undefined; - const resourceIndex = funcTable.resource[funcIndex]; - if (resourceIndex !== -1) { - const libIndex = resourceTable.lib[resourceIndex]; - if (libIndex !== null && libs) { - library = libs[libIndex].name; - } + const libIndex = frameTable.lib[frameIndex]; + if (libIndex !== -1 && libs) { + library = libs[libIndex].name; } frames.push({ name: funcName, nameWithLibrary, library }); diff --git a/src/profile-query/formatters/thread-info.ts b/src/profile-query/formatters/thread-info.ts index 96095e4ffc..4495d45eaf 100644 --- a/src/profile-query/formatters/thread-info.ts +++ b/src/profile-query/formatters/thread-info.ts @@ -9,7 +9,6 @@ import { import { getCategories, getDefaultCategory, - getProfile, } from 'firefox-profiler/selectors/profile'; import { collectSliceTree } from '../cpu-activity'; import { computeThreadNetworkSummary } from '../network-summary'; @@ -114,14 +113,13 @@ export function collectThreadSamples( const threadSelectors = getThreadSelectors(threadIndexes); const friendlyThreadName = threadSelectors.getFriendlyThreadName(state); const thread = threadSelectors.getFilteredThread(state); - const libs = getProfile(state).libs; // Get call trees for analysis const functionListTree = threadSelectors.getFunctionListTree(state); const callTree = threadSelectors.getCallTree(state); // Extract function data - const functions = extractFunctionData(functionListTree, thread, libs); + const functions = extractFunctionData(functionListTree, thread); // Sort by total and take top 50 const sortedByTotal = functions @@ -201,11 +199,7 @@ export function collectThreadSamples( let hasInlinedFrames = false; const frames = heaviestPath.map((funcIndex, depth) => { - const funcName = formatFunctionNameWithLibrary( - funcIndex, - thread, - libs - ); + const funcName = formatFunctionNameWithLibrary(funcIndex, thread); const funcData = funcMap.get(funcIndex); const prefixPath = heaviestPath.slice(0, depth + 1); const frameCallNodeIndex = @@ -307,8 +301,7 @@ export function collectThreadSamplesBottomUp( weightType ); - const libs = getProfile(state).libs; - const invertedCallTree = collectCallTree(invertedTree, libs, callTreeOptions); + const invertedCallTree = collectCallTree(invertedTree, callTreeOptions); return { type: 'thread-samples-bottom-up', @@ -337,10 +330,9 @@ export function collectThreadSamplesTopDown( const threadSelectors = getThreadSelectors(threadIndexes); const friendlyThreadName = threadSelectors.getFriendlyThreadName(state); const callTree = threadSelectors.getCallTree(state); - const libs = getProfile(state).libs; // Collect regular call tree - const regularCallTree = collectCallTree(callTree, libs, callTreeOptions); + const regularCallTree = collectCallTree(callTree, callTreeOptions); return { type: 'thread-samples-top-down', @@ -369,13 +361,12 @@ export function collectThreadFunctions( const threadSelectors = getThreadSelectors(threadIndexes); const friendlyThreadName = threadSelectors.getFriendlyThreadName(state); const thread = threadSelectors.getFilteredThread(state); - const libs = getProfile(state).libs; // Get function list tree const functionListTree = threadSelectors.getFunctionListTree(state); // Extract function data - const allFunctions = extractFunctionData(functionListTree, thread, libs); + const allFunctions = extractFunctionData(functionListTree, thread); const totalFunctionCount = allFunctions.length; // Check if we're zoomed (have committed ranges) diff --git a/src/profile-query/function-annotate.ts b/src/profile-query/function-annotate.ts index 20c86a4444..715e18a22b 100644 --- a/src/profile-query/function-annotate.ts +++ b/src/profile-query/function-annotate.ts @@ -6,7 +6,7 @@ import { getProfile } from 'firefox-profiler/selectors/profile'; import { getSelectedThreadIndexes } from 'firefox-profiler/selectors/url-state'; import { getThreadSelectors } from 'firefox-profiler/selectors/per-thread'; import { parseFunctionHandle } from './function-map'; -import { getLibForFunc } from './function-list'; +import { getLibNameForFunc } from './function-list'; import type { ThreadMap } from './thread-map'; import { getStackLineInfo, @@ -343,12 +343,12 @@ export async function functionAnnotate( const funcIndex = parseFunctionHandle(functionHandle, funcTable.length); const funcName = stringArray[funcTable.name[funcIndex]]; - const libraryName = getLibForFunc( + const libraryName = getLibNameForFunc( funcIndex, funcTable, resourceTable, - profile.libs - )?.name; + stringArray + ); const fullName = libraryName ? `${libraryName}!${funcName}` : funcName; const threadIndexes = getSelectedThreadIndexes(state); diff --git a/src/profile-query/function-list.ts b/src/profile-query/function-list.ts index ebb5edca97..e0bbd0bfcb 100644 --- a/src/profile-query/function-list.ts +++ b/src/profile-query/function-list.ts @@ -5,31 +5,64 @@ import type { Thread, Lib, + RawFrameTable, FuncTable, ResourceTable, } from 'firefox-profiler/types'; +import { ResourceType } from 'firefox-profiler/types'; import { getFunctionHandle } from './function-map'; /** * Look up the Lib record for a function, or undefined if none is associated. + * + * For funcs which are shared across frames from multiple different libs, this + * will return the first match. + * + * The library lives on the frame, not on the func, so this has to scan the + * frame table for a frame belonging to this func. It is meant for one-off + * lookups, such as answering a single query; don't call it in a loop over + * funcs. If all you need is the library's name, use getLibNameForFunc instead, + * which reads it straight off the func's resource. */ -export function getLibForFunc( +export function getAnyLibForFunc( funcIndex: number, - funcTable: FuncTable, - resourceTable: ResourceTable, + frameTable: RawFrameTable, libs: Lib[] ): Lib | undefined { - const resourceIndex = funcTable.resource[funcIndex]; - if (resourceIndex === -1) { - return undefined; - } - const libIndex = resourceTable.lib[resourceIndex]; - if (libIndex !== null && libIndex !== undefined && libIndex >= 0) { - return libs[libIndex]; + for (let frameIndex = 0; frameIndex < frameTable.length; frameIndex++) { + if (frameTable.func[frameIndex] !== funcIndex) { + continue; + } + const libIndex = frameTable.lib[frameIndex]; + if (libIndex !== -1) { + return libs[libIndex]; + } } return undefined; } +/** + * Look up the name of the library a function came from, or null if the function + * isn't native code. This is the name of the func's resource, which is cheap to + * get at; note that it names the library but does not identify a specific + * build, since several libs can share one resource. + */ +export function getLibNameForFunc( + funcIndex: number, + funcTable: FuncTable, + resourceTable: ResourceTable, + stringArray: string[] +): string | null { + const resourceIndex = funcTable.resource[funcIndex]; + if ( + resourceIndex === -1 || + resourceTable.type[resourceIndex] !== ResourceType.Library + ) { + return null; + } + return stringArray[resourceTable.name[resourceIndex]]; +} + export type FunctionData = { funcName: string; funcIndex: number; @@ -351,22 +384,13 @@ export function truncateFunctionName( */ export function formatFunctionNameWithLibrary( funcIndex: number, - thread: Thread, - libs: Lib[] + thread: Thread ): string { const funcName = thread.stringTable.getString( thread.funcTable.name[funcIndex] ); - const lib = getLibForFunc( - funcIndex, - thread.funcTable, - thread.resourceTable, - libs - ); - if (lib) { - return `${lib.name}!${funcName}`; - } - // Fall back to resource name if no library + // The func's resource carries the library name for native code, and the + // origin / URL for JS code. const resourceIndex = thread.funcTable.resource[funcIndex]; if (resourceIndex !== -1) { const resourceName = thread.stringTable.getString( @@ -393,18 +417,13 @@ export function extractFunctionData( selfRelative: number; }; }, - thread: Thread, - libs: Lib[] + thread: Thread ): FunctionData[] { const roots = tree.getRoots(); return roots.map((nodeIndex) => { const data = tree.getNodeData(nodeIndex); // The node index IS the function index for function list trees - const formattedName = formatFunctionNameWithLibrary( - nodeIndex, - thread, - libs - ); + const formattedName = formatFunctionNameWithLibrary(nodeIndex, thread); return { ...data, funcName: formattedName, diff --git a/src/profile-query/index.ts b/src/profile-query/index.ts index 11d58a0cf8..c752ee4c74 100644 --- a/src/profile-query/index.ts +++ b/src/profile-query/index.ts @@ -61,7 +61,7 @@ import { type EligibleSource, } from 'firefox-profiler/profile-logic/source-map-matching'; import { assertExhaustiveCheck } from 'firefox-profiler/utils/types'; -import { getLibForFunc } from './function-list'; +import { getAnyLibForFunc, getLibNameForFunc } from './function-list'; import { MarkerMap } from './marker-map'; import { loadProfileFromFileOrUrl, type LoadOptions } from './loader'; import { collectProfileInfo } from './formatters/profile-info'; @@ -1110,12 +1110,9 @@ export class ProfileQuerier { // Look up the function const funcIndex = parseFunctionHandle(functionHandle, funcTable.length); const funcName = stringArray[funcTable.name[funcIndex]]; - const library = getLibForFunc( - funcIndex, - funcTable, - resourceTable, - profile.libs - )?.name; + const library = + getLibNameForFunc(funcIndex, funcTable, resourceTable, stringArray) ?? + undefined; const fullName = library ? `${library}!${funcName}` : funcName; return { @@ -1156,10 +1153,9 @@ export class ProfileQuerier { }; } - const lib = getLibForFunc( + const lib = getAnyLibForFunc( funcIndex, - funcTable, - resourceTable, + profile.shared.frameTable, profile.libs ); if (lib) { diff --git a/src/profile-query/types.ts b/src/profile-query/types.ts index 1263ec84b5..935cdeac2b 100644 --- a/src/profile-query/types.ts +++ b/src/profile-query/types.ts @@ -183,6 +183,10 @@ export type FunctionInfoResult = { name: string; index: number; }; + // The library of this function, if any. + // Note that, these days, funcs can be associated with multiple + // libraries, so this isn't the best representation anymore. In those + // cases this will be set to one of them, but it's arbitrary which one. library?: { name: string; path: string; diff --git a/src/test/fixtures/profiles/call-nodes.ts b/src/test/fixtures/profiles/call-nodes.ts index 8692daf1ab..9f35027858 100644 --- a/src/test/fixtures/profiles/call-nodes.ts +++ b/src/test/fixtures/profiles/call-nodes.ts @@ -78,6 +78,7 @@ export default function getProfile(): Profile { func: frameFuncs.map((stringIndex) => funcTable.name.indexOf(stringIndex)), address: Array(frameFuncs.length).fill(-1), inlineDepth: Array(frameFuncs.length).fill(0), + lib: Array(frameFuncs.length).fill(-1), nativeSymbol: Array(frameFuncs.length).fill(null), category: Array(frameFuncs.length).fill(null), subcategory: Array(frameFuncs.length).fill(null), diff --git a/src/test/fixtures/profiles/processed-profile.ts b/src/test/fixtures/profiles/processed-profile.ts index dc886ee205..f9b48dd01a 100644 --- a/src/test/fixtures/profiles/processed-profile.ts +++ b/src/test/fixtures/profiles/processed-profile.ts @@ -41,6 +41,7 @@ import type { Thread, ThreadIndex, IndexIntoCategoryList, + IndexIntoLibs, IndexIntoStackTable, CategoryList, JsTracerTable, @@ -583,7 +584,7 @@ export type ProfileWithDicts = { * * The following func and frame attributes are supported: * - [cat:*] - The category name, affects frameTable.category - * - [lib:*] - The library name, affects funcTable.resource + resourceTable + libs + * - [lib:*] - The library name, affects frameTable.lib + funcTable.resource + resourceTable + libs * - [file:*] - The filename, affects funcTable.file * - [line:*] - The line, affects frameTable.line * - [address:*] - The frame address, affects frameTable.address @@ -600,8 +601,8 @@ function getFrame( ) { const funcIndex = frameTable.func[frameIndex]; let s = stringTable.getString(funcTable.name[funcIndex]); - const libIndex = resourceTable.lib[funcTable.resource[funcIndex]]; - if (libIndex !== null) { + const libIndex = frameTable.lib[frameIndex]; + if (libIndex !== -1) { const libName = libs[libIndex].name; s += `[lib:${libName}]`; } @@ -902,7 +903,7 @@ function _buildThreadFromTextOnlyStacks( // Find the library name from the function name and create an entry if needed. const libraryName = _findLibNameFromFuncName(funcNameWithModifier); let resourceIndex = -1; - let libIndex = null; + let libIndex: IndexIntoLibs | -1 = -1; if (libraryName !== null) { libIndex = globalDataCollector.indexForLib({ arch: '', @@ -947,7 +948,7 @@ function _buildThreadFromTextOnlyStacks( const nativeSymbolInfo = _findNativeSymbolNameFromFuncName(funcNameWithModifier); if (nativeSymbolInfo) { - if (libIndex === null) { + if (libIndex === -1) { throw new Error( `[sym:] has to be used together with [lib:] - missing lib in "${funcNameWithModifier}"` ); @@ -975,7 +976,8 @@ function _buildThreadFromTextOnlyStacks( lineNumber === frameTable.line[i] && address === frameTable.address[i] && inlineDepth === frameTable.inlineDepth[i] && - nativeSymbol === frameTable.nativeSymbol[i] + nativeSymbol === frameTable.nativeSymbol[i] && + libIndex === frameTable.lib[i] ) { frameIndex = i; break; @@ -984,6 +986,7 @@ function _buildThreadFromTextOnlyStacks( if (frameIndex === undefined) { frameTable.func.push(funcIndex); + frameTable.lib.push(libIndex); frameTable.address.push(address); frameTable.inlineDepth.push(inlineDepth); frameTable.category.push(category); @@ -2084,6 +2087,7 @@ export function addInnerWindowIdToStacks( frameTable.subcategory[foundFrameIndex] ); frameTableBuilder.func.push(frameTable.func[foundFrameIndex]); + frameTableBuilder.lib.push(frameTable.lib[foundFrameIndex]); frameTableBuilder.nativeSymbol.push( frameTable.nativeSymbol[foundFrameIndex] ); 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 8df0ad49ac..4732177b42 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": 69, + "preprocessedProfileVersion": 70, "processType": 0, "product": "a.out", "sampleUnits": Object { @@ -370,6 +370,50 @@ Object { null, ], "length": 42, + "lib": Array [ + 0, + 1, + 2, + 3, + 2, + 1, + 1, + 4, + 4, + 3, + 2, + 1, + 1, + 4, + 4, + 3, + 1, + 2, + 1, + 1, + 4, + 4, + 3, + 1, + 2, + 1, + 1, + 4, + 4, + 3, + 1, + 2, + 2, + 2, + 5, + 2, + 1, + 1, + 4, + 4, + 3, + 1, + ], "line": Array [ null, null, @@ -743,14 +787,6 @@ Object { null, ], "length": 6, - "lib": Array [ - 0, - 1, - 2, - 3, - 4, - 5, - ], "name": Array [ 0, 1, @@ -1452,7 +1488,7 @@ Object { "markerSchema": Array [], "oscpu": "macOS 14.6.1", "pausedRanges": Array [], - "preprocessedProfileVersion": 69, + "preprocessedProfileVersion": 70, "processType": 0, "product": "a.out", "sampleUnits": Object { @@ -1735,6 +1771,50 @@ Object { null, ], "length": 42, + "lib": Array [ + 0, + 1, + 2, + 3, + 2, + 1, + 1, + 4, + 4, + 3, + 2, + 1, + 1, + 4, + 4, + 3, + 1, + 2, + 1, + 1, + 4, + 4, + 3, + 1, + 2, + 1, + 1, + 4, + 4, + 3, + 1, + 2, + 2, + 2, + 5, + 2, + 1, + 1, + 4, + 4, + 3, + 1, + ], "line": Array [ null, null, @@ -2108,14 +2188,6 @@ Object { null, ], "length": 6, - "lib": Array [ - 0, - 1, - 2, - 3, - 4, - 5, - ], "name": Array [ 0, 1, @@ -2817,7 +2889,7 @@ Object { "markerSchema": Array [], "oscpu": "macOS 14.6.1", "pausedRanges": Array [], - "preprocessedProfileVersion": 69, + "preprocessedProfileVersion": 70, "processType": 0, "product": "a.out", "sampleUnits": Object { @@ -3100,6 +3172,50 @@ Object { null, ], "length": 42, + "lib": Array [ + 0, + 1, + 2, + 3, + 2, + 1, + 1, + 4, + 4, + 3, + 2, + 1, + 1, + 4, + 4, + 3, + 1, + 2, + 1, + 1, + 4, + 4, + 3, + 1, + 2, + 1, + 1, + 4, + 4, + 3, + 1, + 2, + 2, + 2, + 5, + 2, + 1, + 1, + 4, + 4, + 3, + 1, + ], "line": Array [ null, null, @@ -3473,14 +3589,6 @@ Object { null, ], "length": 6, - "lib": Array [ - 0, - 1, - 2, - 3, - 4, - 5, - ], "name": Array [ 0, 1, @@ -4182,7 +4290,7 @@ Object { "markerSchema": Array [], "oscpu": "macOS 14.6.1", "pausedRanges": Array [], - "preprocessedProfileVersion": 69, + "preprocessedProfileVersion": 70, "processType": 0, "product": "a.out", "sampleUnits": Object { @@ -4465,6 +4573,50 @@ Object { null, ], "length": 42, + "lib": Array [ + 0, + 1, + 2, + 3, + 2, + 1, + 1, + 4, + 4, + 3, + 2, + 1, + 1, + 4, + 4, + 3, + 1, + 2, + 1, + 1, + 4, + 4, + 3, + 1, + 2, + 1, + 1, + 4, + 4, + 3, + 1, + 2, + 2, + 2, + 5, + 2, + 1, + 1, + 4, + 4, + 3, + 1, + ], "line": Array [ null, null, @@ -4838,14 +4990,6 @@ Object { null, ], "length": 6, - "lib": Array [ - 0, - 1, - 2, - 3, - 4, - 5, - ], "name": Array [ 0, 1, diff --git a/src/test/store/__snapshots__/profile-view.test.ts.snap b/src/test/store/__snapshots__/profile-view.test.ts.snap index c16fdb16e5..2edad08f1a 100644 --- a/src/test/store/__snapshots__/profile-view.test.ts.snap +++ b/src/test/store/__snapshots__/profile-view.test.ts.snap @@ -428,7 +428,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 69, + "preprocessedProfileVersion": 70, "processType": 0, "product": "Firefox", "sourceURL": "", @@ -515,6 +515,17 @@ Object { 0, ], "length": 9, + "lib": Array [ + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + ], "line": Array [ null, null, @@ -661,7 +672,6 @@ Object { "resourceTable": Object { "host": Array [], "length": 0, - "lib": Array [], "name": Array [], "type": Array [], }, @@ -2486,6 +2496,17 @@ CallTree { 0, ], "length": 9, + "lib": Int32Array [ + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + ], "line": Array [ null, null, @@ -2655,7 +2676,6 @@ CallTree { "resourceTable": Object { "host": Array [], "length": 0, - "lib": Array [], "name": Array [], "type": Array [], }, @@ -2874,6 +2894,17 @@ Object { 0, ], "length": 9, + "lib": Int32Array [ + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + ], "line": Array [ null, null, @@ -3043,7 +3074,6 @@ Object { "resourceTable": Object { "host": Array [], "length": 0, - "lib": Array [], "name": Array [], "type": Array [], }, @@ -3342,6 +3372,17 @@ Object { 0, ], "length": 9, + "lib": Int32Array [ + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + ], "line": Array [ null, null, @@ -3511,7 +3552,6 @@ Object { "resourceTable": Object { "host": Array [], "length": 0, - "lib": Array [], "name": Array [], "type": Array [], }, @@ -3716,6 +3756,17 @@ Object { 0, ], "length": 9, + "lib": Int32Array [ + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + ], "line": Array [ null, null, @@ -3885,7 +3936,6 @@ Object { "resourceTable": Object { "host": Array [], "length": 0, - "lib": Array [], "name": Array [], "type": Array [], }, @@ -4102,6 +4152,17 @@ Object { 0, ], "length": 9, + "lib": Int32Array [ + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + ], "line": Array [ null, null, @@ -4271,7 +4332,6 @@ Object { "resourceTable": Object { "host": Array [], "length": 0, - "lib": Array [], "name": Array [], "type": Array [], }, diff --git a/src/test/store/symbolication.test.ts b/src/test/store/symbolication.test.ts index 81771af3ea..352f32a2d8 100644 --- a/src/test/store/symbolication.test.ts +++ b/src/test/store/symbolication.test.ts @@ -620,7 +620,6 @@ function _createUnsymbolicatedProfile() { profile.shared.resourceTable = { length: 1, - lib: [libIndex], name: [stringTable.indexForString('example lib')], host: [stringTable.indexForString('example host')], type: [ResourceType.Library], @@ -628,6 +627,9 @@ function _createUnsymbolicatedProfile() { for (let i = 0; i < profile.shared.funcTable.length; i++) { profile.shared.funcTable.resource[i] = 0; } + for (let i = 0; i < profile.shared.frameTable.length; i++) { + profile.shared.frameTable.lib[i] = libIndex; + } // Add a marker with a cause stack. We use the stack of the first sample. // This sample has 0x000a in its stack, which has an inlined function call, diff --git a/src/test/unit/__snapshots__/process-profile.test.ts.snap b/src/test/unit/__snapshots__/process-profile.test.ts.snap index 72c8a2406d..5f7b34f429 100644 --- a/src/test/unit/__snapshots__/process-profile.test.ts.snap +++ b/src/test/unit/__snapshots__/process-profile.test.ts.snap @@ -207,7 +207,7 @@ Array [ "host": null, "isJS": false, "lib": undefined, - "libIndex": undefined, + "libIndex": null, "lineNumber": null, "resourceIndex": -1, "resourceName": null, diff --git a/src/test/unit/__snapshots__/profile-conversion.test.ts.snap b/src/test/unit/__snapshots__/profile-conversion.test.ts.snap index a3831da47a..907fa724ce 100644 --- a/src/test/unit/__snapshots__/profile-conversion.test.ts.snap +++ b/src/test/unit/__snapshots__/profile-conversion.test.ts.snap @@ -42,7 +42,7 @@ Object { "RefreshDriverTick", "Network", ], - "preprocessedProfileVersion": 69, + "preprocessedProfileVersion": 70, "product": "ART Trace (Android)", "symbolicated": true, "version": 36, @@ -1022,7 +1022,7 @@ Object { "RefreshDriverTick", "Network", ], - "preprocessedProfileVersion": 69, + "preprocessedProfileVersion": 70, "product": "ART Trace (Android)", "symbolicated": true, "version": 36, @@ -2305,7 +2305,7 @@ Object { "markerSchemaNames": Array [ "EventDispatch", ], - "preprocessedProfileVersion": 69, + "preprocessedProfileVersion": 70, "product": "Chrome Trace", "symbolicated": true, "version": 36, @@ -2697,7 +2697,7 @@ Object { "markerSchemaNames": Array [ "EventDispatch", ], - "preprocessedProfileVersion": 69, + "preprocessedProfileVersion": 70, "product": "Chrome Trace", "symbolicated": true, "version": 36, @@ -3086,7 +3086,7 @@ Object { "markerSchemaNames": Array [ "EventDispatch", ], - "preprocessedProfileVersion": 69, + "preprocessedProfileVersion": 70, "product": "Chrome Trace", "symbolicated": true, "version": 36, @@ -3187,7 +3187,7 @@ Object { "markerSchemaNames": Array [ "EventDispatch", ], - "preprocessedProfileVersion": 69, + "preprocessedProfileVersion": 70, "product": "Chrome Trace", "symbolicated": true, "version": 36, @@ -3540,7 +3540,7 @@ Object { "markerSchemaNames": Array [ "EventDispatch", ], - "preprocessedProfileVersion": 69, + "preprocessedProfileVersion": 70, "product": "Chrome Trace", "symbolicated": true, "version": 36, @@ -3605,7 +3605,7 @@ Object { "markerSchemaNames": Array [ "EventDispatch", ], - "preprocessedProfileVersion": 69, + "preprocessedProfileVersion": 70, "product": "Chrome Trace", "symbolicated": true, "version": 36, @@ -3759,7 +3759,7 @@ Object { "markerSchemaNames": Array [ "EventDispatch", ], - "preprocessedProfileVersion": 69, + "preprocessedProfileVersion": 70, "product": "Chrome Trace", "symbolicated": true, "version": 36, @@ -3817,7 +3817,7 @@ Object { "importedFrom": undefined, "interval": 1, "markerSchemaNames": Array [], - "preprocessedProfileVersion": 69, + "preprocessedProfileVersion": 70, "product": "Firefox", "symbolicated": true, "version": 36, @@ -4207,7 +4207,7 @@ Object { "importedFrom": undefined, "interval": 1, "markerSchemaNames": Array [], - "preprocessedProfileVersion": 69, + "preprocessedProfileVersion": 70, "product": "Firefox", "symbolicated": true, "version": 36, @@ -4265,7 +4265,7 @@ Object { "importedFrom": undefined, "interval": 1, "markerSchemaNames": Array [], - "preprocessedProfileVersion": 69, + "preprocessedProfileVersion": 70, "product": "Firefox", "symbolicated": true, "version": 36, @@ -4323,7 +4323,7 @@ Object { "importedFrom": undefined, "interval": 1, "markerSchemaNames": Array [], - "preprocessedProfileVersion": 69, + "preprocessedProfileVersion": 70, "product": "Firefox", "symbolicated": true, "version": 36, @@ -4643,7 +4643,7 @@ Object { "importedFrom": "Simpleperf", "interval": 0, "markerSchemaNames": Array [], - "preprocessedProfileVersion": 69, + "preprocessedProfileVersion": 70, "product": "com.example.sampleapplication", "symbolicated": undefined, "version": 30, @@ -5019,7 +5019,7 @@ Object { "importedFrom": "Simpleperf", "interval": 0, "markerSchemaNames": Array [], - "preprocessedProfileVersion": 69, + "preprocessedProfileVersion": 70, "product": "com.example.sampleapplication", "symbolicated": undefined, "version": 30, @@ -5319,7 +5319,7 @@ Object { "importedFrom": "dhat", "interval": 1, "markerSchemaNames": Array [], - "preprocessedProfileVersion": 69, + "preprocessedProfileVersion": 70, "product": "target/debug/examples/work_log (dhat)", "symbolicated": true, "version": 36, @@ -5452,7 +5452,7 @@ Object { "importedFrom": undefined, "interval": 1, "markerSchemaNames": Array [], - "preprocessedProfileVersion": 69, + "preprocessedProfileVersion": 70, "product": "Flamegraph", "symbolicated": true, "version": 36, @@ -5510,7 +5510,7 @@ Object { "importedFrom": undefined, "interval": 1, "markerSchemaNames": Array [], - "preprocessedProfileVersion": 69, + "preprocessedProfileVersion": 70, "product": "Flamegraph", "symbolicated": true, "version": 36, diff --git a/src/test/unit/__snapshots__/profile-upgrading.test.ts.snap b/src/test/unit/__snapshots__/profile-upgrading.test.ts.snap index 394eaa6d2e..edcb4e9788 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": 69, + "preprocessedProfileVersion": 70, "processType": 0, "product": "Firefox", "sampleUnits": undefined, @@ -1287,6 +1287,210 @@ Object { 0, ], "length": 202, + "lib": Array [ + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + ], "line": Array [ null, null, @@ -2722,20 +2926,6 @@ Object { null, ], "length": 12, - "lib": Array [ - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - ], "name": Array [ 2, 5, @@ -7644,7 +7834,7 @@ Object { "misc": "rv:48.0", "oscpu": "Intel Mac OS X 10.11", "platform": "Macintosh", - "preprocessedProfileVersion": 69, + "preprocessedProfileVersion": 70, "processType": 0, "product": "Firefox", "stackwalk": 1, @@ -7793,6 +7983,29 @@ Object { 0, ], "length": 21, + "lib": Array [ + -1, + 0, + 0, + -1, + -1, + 0, + 0, + -1, + 0, + 0, + -1, + -1, + 0, + 0, + -1, + 1, + 1, + -1, + -1, + 1, + 1, + ], "line": Array [ null, null, @@ -7995,11 +8208,6 @@ Object { null, ], "length": 3, - "lib": Array [ - 0, - null, - 1, - ], "name": Array [ 11, 10, @@ -9021,7 +9229,7 @@ Object { "misc": "rv:48.0", "oscpu": "Intel Mac OS X 10.11", "platform": "Macintosh", - "preprocessedProfileVersion": 69, + "preprocessedProfileVersion": 70, "processType": 0, "product": "Firefox", "stackwalk": 1, @@ -9182,6 +9390,31 @@ Object { 0, ], "length": 23, + "lib": Array [ + -1, + 0, + 0, + -1, + -1, + 0, + 0, + -1, + 0, + 0, + -1, + -1, + 0, + 0, + -1, + 1, + 1, + -1, + -1, + -1, + -1, + 1, + 1, + ], "line": Array [ null, null, @@ -9408,11 +9641,6 @@ Object { null, ], "length": 3, - "lib": Array [ - 0, - null, - 1, - ], "name": Array [ 12, 11, @@ -10566,7 +10794,7 @@ Object { "misc": "rv:48.0", "oscpu": "Intel Mac OS X 10.11", "platform": "Macintosh", - "preprocessedProfileVersion": 69, + "preprocessedProfileVersion": 70, "processType": 0, "product": "Firefox", "stackwalk": 1, @@ -10753,6 +10981,31 @@ Object { 0, ], "length": 23, + "lib": Array [ + -1, + 0, + 0, + -1, + -1, + 0, + 0, + -1, + 0, + 0, + -1, + -1, + 0, + 0, + -1, + 1, + 1, + -1, + -1, + -1, + -1, + 1, + 1, + ], "line": Array [ null, null, @@ -10979,11 +11232,6 @@ Object { null, ], "length": 3, - "lib": Array [ - 0, - null, - 1, - ], "name": Array [ 13, 12, diff --git a/src/test/unit/merge-compare.test.ts b/src/test/unit/merge-compare.test.ts index c064168786..c96c65350d 100644 --- a/src/test/unit/merge-compare.test.ts +++ b/src/test/unit/merge-compare.test.ts @@ -18,6 +18,7 @@ import { ensureExists } from 'firefox-profiler/utils/types'; import { getTimeRangeIncludingAllThreads } from 'firefox-profiler/profile-logic/profile-data'; import { StringTable } from '../../utils/string-table'; import type { RawProfileSharedData, Profile } from 'firefox-profiler/types'; +import { ResourceType } from 'firefox-profiler/types'; import { callTreeFromProfile, formatTree } from '../fixtures/utils'; import { storeWithProfile } from '../fixtures/stores'; import { addTransformToStack } from '../../actions/profile-view'; @@ -50,8 +51,19 @@ describe('mergeProfilesForDiffing function', function () { const mergedShared = mergedProfile.shared; const mergedResources = mergedShared.resourceTable; const mergedFunctions = mergedShared.funcTable; + const mergedFrames = mergedShared.frameTable; const stringArray = mergedShared.stringArray; + // The lib lives on the frame, so map each func back to the lib of one of + // its frames. + const libForFunc = new Array(mergedFunctions.length).fill(null); + for (let frameIndex = 0; frameIndex < mergedFrames.length; frameIndex++) { + const libIndex = mergedFrames.lib[frameIndex]; + if (libIndex !== -1) { + libForFunc[mergedFrames.func[frameIndex]] = libIndex; + } + } + expect(mergedLibs).toHaveLength(3); expect(mergedResources).toHaveLength(3); expect(mergedFunctions).toHaveLength(4); @@ -72,11 +84,10 @@ describe('mergeProfilesForDiffing function', function () { if (nameIndex >= 0) { resourceName = stringArray[nameIndex]; } - - const libIndex = mergedResources.lib[resourceIndex]; - if (libIndex !== null && libIndex !== undefined && libIndex >= 0) { - libName = mergedLibs[libIndex].name; - } + } + const libIndex = libForFunc[funcIndex]; + if (libIndex !== null) { + libName = mergedLibs[libIndex].name; } /* eslint-disable jest/no-conditional-expect */ @@ -101,6 +112,49 @@ describe('mergeProfilesForDiffing function', function () { expect(resourcesForA).toEqual(['libA', 'libB']); }); + it('keeps two builds of the same library apart while sharing one resource', function () { + // This is the case that motivated moving the lib column off the + // resourceTable and onto the frameTable: comparing two builds of libxul. + // Both builds have the same name, so they share a single resource, but they + // have different breakpadIds and so must stay separate libs - symbols have + // to be looked up separately for each build. + const sampleProfileA = getProfileFromTextSamples('A[lib:libxul.so]'); + const sampleProfileB = getProfileFromTextSamples('A[lib:libxul.so]'); + sampleProfileB.profile.libs[0] = { + ...sampleProfileB.profile.libs[0], + breakpadId: 'A_DIFFERENT_BUILD', + }; + const profileState = stateFromLocation({ + pathname: '/public/fakehash1/', + search: '?thread=0&v=3', + hash: '', + }); + + const { profile: mergedProfile } = mergeProfilesForDiffing( + [sampleProfileA.profile, sampleProfileB.profile], + [profileState, profileState] + ); + const { resourceTable, frameTable, stringArray } = mergedProfile.shared; + + // The two builds are separate libs... + expect(mergedProfile.libs.map((lib) => lib.breakpadId)).toEqual([ + 'SOMETHING_FAKE', + 'A_DIFFERENT_BUILD', + ]); + + // ...but they share the one resource that names them. + const libraryResources = []; + for (let i = 0; i < resourceTable.length; i++) { + if (resourceTable.type[i] === ResourceType.Library) { + libraryResources.push(stringArray[resourceTable.name[i]]); + } + } + expect(libraryResources).toEqual(['libxul.so']); + + // The frames are what keeps the two builds apart. + expect([...frameTable.lib]).toEqual([0, 1]); + }); + it('should set interval of merged profile to minimum of all intervals', function () { const sampleProfileA = getProfileFromTextSamples('A'); const sampleProfileB = getProfileFromTextSamples('B'); diff --git a/src/test/unit/process-profile.test.ts b/src/test/unit/process-profile.test.ts index 945bfd78e6..12404111f8 100644 --- a/src/test/unit/process-profile.test.ts +++ b/src/test/unit/process-profile.test.ts @@ -110,14 +110,15 @@ describe('extract functions and resource from location strings', function () { globalDataCollector.addExtensionOrigins(extensions); it('extracts the information for all different types of locations', function () { - const { frameFuncs } = extractFuncsAndResourcesFromFrameLocations( - locationIndexes, - locationIndexes.map(() => false), - geckoThreadStringArray, - libs, - globalDataCollector, - getEmptySourceTable() - ); + const { frameFuncs, frameLibs } = + extractFuncsAndResourcesFromFrameLocations( + locationIndexes, + locationIndexes.map(() => false), + geckoThreadStringArray, + libs, + globalDataCollector, + getEmptySourceTable() + ); const { shared: { sources, funcTable, resourceTable }, @@ -139,15 +140,13 @@ describe('extract functions and resource from location strings', function () { fileNameIndex === null ? null : stringTable.getString(fileNameIndex); const lineNumber = funcTable.lineNumber[funcIndex]; const columnNumber = funcTable.columnNumber[funcIndex]; - - let libIndex, resourceName, host, resourceType; + let resourceName, host, resourceType; if (resourceIndex === -1) { resourceName = null; host = null; resourceType = null; } else { const hostStringIndex = resourceTable.host[resourceIndex]; - libIndex = resourceTable.lib[resourceIndex]; resourceName = stringTable.getString( resourceTable.name[resourceIndex] ); @@ -157,10 +156,8 @@ describe('extract functions and resource from location strings', function () { : stringTable.getString(hostStringIndex); resourceType = resourceTable.type[resourceIndex]; } - const lib = - libIndex === undefined || libIndex === null || libIndex === -1 - ? undefined - : libs[libIndex]; + const libIndex = frameLibs[locationIndex]; + const lib = libIndex === null ? undefined : libs[libIndex]; return [ locationName, diff --git a/src/test/unit/profile-query/call-tree.test.ts b/src/test/unit/profile-query/call-tree.test.ts index 41dbbf43b1..7bf2b1d1c6 100644 --- a/src/test/unit/profile-query/call-tree.test.ts +++ b/src/test/unit/profile-query/call-tree.test.ts @@ -23,10 +23,9 @@ describe('call-tree collection', function () { const state = store.getState(); const threadSelectors = getThreadSelectors(0); const callTree = threadSelectors.getCallTree(state); - const libs = profile.libs; // Collect with budget of 3 nodes - const result = collectCallTree(callTree, libs, { + const result = collectCallTree(callTree, { maxNodes: 3, }); @@ -47,10 +46,9 @@ describe('call-tree collection', function () { const state = store.getState(); const threadSelectors = getThreadSelectors(0); const callTree = threadSelectors.getCallTree(state); - const libs = profile.libs; // With small budget, should still include D (100% at depth 3) - const result = collectCallTree(callTree, libs, { + const result = collectCallTree(callTree, { maxNodes: 4, }); @@ -75,10 +73,9 @@ describe('call-tree collection', function () { const state = store.getState(); const threadSelectors = getThreadSelectors(0); const callTree = threadSelectors.getCallTree(state); - const libs = profile.libs; // With budget of 4: should get A, B (50%), D (50%), C (50%) - const result = collectCallTree(callTree, libs, { + const result = collectCallTree(callTree, { maxNodes: 4, }); @@ -99,10 +96,9 @@ describe('call-tree collection', function () { const state = store.getState(); const threadSelectors = getThreadSelectors(0); const callTree = threadSelectors.getCallTree(state); - const libs = profile.libs; // With budget of 2: A and B, should show C/D/E as elided - const result = collectCallTree(callTree, libs, { + const result = collectCallTree(callTree, { maxNodes: 2, }); @@ -125,9 +121,8 @@ describe('call-tree collection', function () { const state = store.getState(); const threadSelectors = getThreadSelectors(0); const callTree = threadSelectors.getCallTree(state); - const libs = profile.libs; - const result = collectCallTree(callTree, libs, { + const result = collectCallTree(callTree, { maxNodes: 4, scoringStrategy: 'exponential-0.9', }); @@ -149,9 +144,8 @@ describe('call-tree collection', function () { const state = store.getState(); const threadSelectors = getThreadSelectors(0); const callTree = threadSelectors.getCallTree(state); - const libs = profile.libs; - const result = collectCallTree(callTree, libs, { + const result = collectCallTree(callTree, { maxNodes: 4, scoringStrategy: 'percentage-only', }); @@ -175,9 +169,8 @@ describe('call-tree collection', function () { const state = store.getState(); const threadSelectors = getThreadSelectors(0); const callTree = threadSelectors.getCallTree(state); - const libs = profile.libs; - const result = collectCallTree(callTree, libs, { + const result = collectCallTree(callTree, { maxNodes: 10, scoringStrategy: 'exponential-0.9', }); @@ -203,10 +196,9 @@ describe('call-tree collection', function () { const state = store.getState(); const threadSelectors = getThreadSelectors(0); const callTree = threadSelectors.getCallTree(state); - const libs = profile.libs; // Small budget to force truncation - const result = collectCallTree(callTree, libs, { + const result = collectCallTree(callTree, { maxNodes: 3, }); @@ -237,7 +229,6 @@ describe('call-tree collection', function () { const state = store.getState(); const threadSelectors = getThreadSelectors(0); const callTree = threadSelectors.getCallTree(state); - const libs = profile.libs; // First verify that A has many children const roots = callTree.getRoots(); @@ -246,7 +237,7 @@ describe('call-tree collection', function () { const aChildren = callTree.getChildren(aCallNode); expect(aChildren.length).toBe(16); // B through Q - const result = collectCallTree(callTree, libs, { + const result = collectCallTree(callTree, { maxNodes: 5, // Small budget to ensure truncation maxChildrenPerNode: 10, }); @@ -272,9 +263,8 @@ describe('call-tree collection', function () { const state = store.getState(); const threadSelectors = getThreadSelectors(0); const callTree = threadSelectors.getCallTree(state); - const libs = profile.libs; - const result = collectCallTree(callTree, libs, { + const result = collectCallTree(callTree, { maxNodes: 10, }); @@ -302,9 +292,8 @@ describe('call-tree collection', function () { const state = store.getState(); const threadSelectors = getThreadSelectors(0); const callTree = threadSelectors.getCallTree(state); - const libs = profile.libs; - const result = collectCallTree(callTree, libs, { + const result = collectCallTree(callTree, { maxNodes: 8, scoringStrategy: 'exponential-0.9', }); @@ -331,9 +320,8 @@ describe('call-tree collection', function () { const state = store.getState(); const threadSelectors = getThreadSelectors(0); const callTree = threadSelectors.getCallTree(state); - const libs = profile.libs; - const result = collectCallTree(callTree, libs, { + const result = collectCallTree(callTree, { maxNodes: 100, maxDepth: 20, }); @@ -354,10 +342,9 @@ describe('call-tree collection', function () { const state = store.getState(); const threadSelectors = getThreadSelectors(0); const callTree = threadSelectors.getCallTree(state); - const libs = profile.libs; // Budget that includes A and B, but not the other children - const result = collectCallTree(callTree, libs, { + const result = collectCallTree(callTree, { maxNodes: 2, }); @@ -392,9 +379,8 @@ describe('call-tree collection', function () { const state = store.getState(); const threadSelectors = getThreadSelectors(0); const callTree = threadSelectors.getCallTree(state); - const libs = profile.libs; - const result = collectCallTree(callTree, libs, { + const result = collectCallTree(callTree, { maxNodes: 2, }); @@ -422,9 +408,8 @@ describe('call-tree collection', function () { const state = store.getState(); const threadSelectors = getThreadSelectors(0); const callTree = threadSelectors.getCallTree(state); - const libs = profile.libs; - const result = collectCallTree(callTree, libs, { + const result = collectCallTree(callTree, { maxNodes: 1000, // High budget maxDepth: 10, // But limited depth }); diff --git a/src/test/unit/profile-query/function-list.test.ts b/src/test/unit/profile-query/function-list.test.ts index d6a736d7cf..0d6ed65b72 100644 --- a/src/test/unit/profile-query/function-list.test.ts +++ b/src/test/unit/profile-query/function-list.test.ts @@ -12,7 +12,6 @@ import { type FunctionData, } from '../../../profile-query/function-list'; import { getProfileFromTextSamples } from '../../fixtures/profiles/processed-profile'; -import type { Lib } from 'firefox-profiler/types'; function createMockTree(functions: FunctionData[]) { return { @@ -24,12 +23,11 @@ function createMockTree(functions: FunctionData[]) { describe('function-list', function () { describe('extractFunctionData', function () { it('extracts function data from a tree', function () { - const { profile, derivedThreads } = getProfileFromTextSamples(` + const { derivedThreads } = getProfileFromTextSamples(` foo bar `); const [thread] = derivedThreads; - const libs: Lib[] = profile.libs; const functions: FunctionData[] = [ { @@ -51,7 +49,7 @@ describe('function-list', function () { ]; const tree = createMockTree(functions); - const result = extractFunctionData(tree, thread, libs); + const result = extractFunctionData(tree, thread); expect(result).toEqual(functions); }); diff --git a/src/test/unit/profile-tree.test.ts b/src/test/unit/profile-tree.test.ts index 90f8a59a91..64fbc98f44 100644 --- a/src/test/unit/profile-tree.test.ts +++ b/src/test/unit/profile-tree.test.ts @@ -697,7 +697,6 @@ describe('origin annotation', function () { shared.funcTable.source[funcIndex] = location ? addSourceToTable(shared.sources, stringTable.indexForString(location)) : null; - shared.resourceTable.lib.push(-1); shared.resourceTable.name.push(stringTable.indexForString(name)); shared.resourceTable.host.push( host ? stringTable.indexForString(host) : null diff --git a/src/test/unit/sanitize.test.ts b/src/test/unit/sanitize.test.ts index 8ab505fbcc..720a290a39 100644 --- a/src/test/unit/sanitize.test.ts +++ b/src/test/unit/sanitize.test.ts @@ -1660,6 +1660,30 @@ describe('sanitizePII', function () { ); }); + it('should compact the libs and translate the frameTable lib column', function () { + // Thread 0's frames are in libA, thread 1's are in libB, and thread 2's + // frames have no library at all (lib === -1). + const { profile } = getProfileFromTextSamples( + `A[lib:libA.so]`, + `B[lib:libB.so]`, + `Cjs` + ); + + expect(profile.libs.map((lib) => lib.name)).toEqual(['libA.so', 'libB.so']); + expect([...profile.shared.frameTable.lib]).toEqual([0, 1, -1]); + + // Remove thread 0, which is the only user of libA.so. + const { sanitizedProfile } = setup( + { shouldRemoveThreads: new Set([0]) }, + profile + ); + + // libA.so is gone, and the surviving frames point at the reindexed libB.so + // while the lib-less frame keeps its -1 sentinel. + expect(sanitizedProfile.libs.map((lib) => lib.name)).toEqual(['libB.so']); + expect([...sanitizedProfile.shared.frameTable.lib]).toEqual([0, -1]); + }); + it('always removes source contents even when no PII removal is requested', function () { const { profile } = getProfileFromTextSamples(`A[file:file1.js]`); // There should be only one source. diff --git a/src/types/profile-derived.ts b/src/types/profile-derived.ts index 4da76d2ba1..f4eb814fce 100644 --- a/src/types/profile-derived.ts +++ b/src/types/profile-derived.ts @@ -287,10 +287,10 @@ export type StackTable = { * * 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. + * for missing addresses), `inlineDepth` (`Uint8Array`), `func` (`Int32Array`), + * and `lib` (`Int32Array`, with `-1` as the sentinel for frames with no + * library). 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). @@ -301,6 +301,8 @@ export type FrameTable = { subcategory: (IndexIntoSubcategoryListForCategory | null)[]; // Differs from RawFrameTable: always Int32Array. func: Int32Array; + // Differs from RawFrameTable: always Int32Array (-1 sentinel preserved). + lib: Int32Array; nativeSymbol: (IndexIntoNativeSymbolTable | null)[]; innerWindowID: (InnerWindowID | null)[]; line: (number | null)[]; diff --git a/src/types/profile.ts b/src/types/profile.ts index a473c58223..8c853c307c 100644 --- a/src/types/profile.ts +++ b/src/types/profile.ts @@ -248,12 +248,30 @@ export type RawFrameTable = { // is performed at the end of profile processing. See the big comment above // nudgeReturnAddresses for more details. // - // The library which this address is relative to is given by the frame's nativeSymbol: - // frame -> nativeSymbol -> lib. + // The library which this address is relative to is given by the frame's `lib` + // column. // // Frames with no address use the sentinel value `-1`. address: Array
| Int32Array; + // The native library that this frame's code is in. Frames which aren't native + // code (JS frames, label frames), or whose library is unknown, use the + // sentinel value `-1`. + // + // This is what makes a frame's `address` meaningful: the address is an offset + // relative to this library. It is also what symbolication groups frames by. + // + // The library is stored per frame rather than being reached via the frame's + // resource (frame -> func -> resource -> lib) so that resources and libraries + // can vary independently. Multiple libraries can share one resource: a + // comparison profile of two libxul.so builds has one resource named + // "libxul.so" but a separate lib for each build, and a profile which combines + // several Firefox runs has one webhost resource per origin but a separate + // jitdump lib per run. Keeping them separate also leaves room for resources + // that describe something other than a library, such as a Rust crate, without + // breaking the frame-to-library association. + lib: 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. // The outermost frame always has depth 0. @@ -410,10 +428,13 @@ export const enum ResourceType { /** * The ResourceTable holds additional information about functions. It tends to contain * sparse arrays. Multiple functions can point to the same resource. + * + * A resource of type Library names the binary a function came from, but it does + * not identify a specific loaded library: several libs can share one resource. + * The lib for a piece of native code is on the frame, see RawFrameTable.lib. */ export type ResourceTable = { length: number; - lib: Array; name: Array; host: Array; type: ResourceType[];