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[];