From b32ae2a911315538be0c2e14fd288f1443a75459 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Fri, 2 Jan 2026 20:34:33 +0100 Subject: [PATCH 1/7] Change the order in which we consult the various symbol sources: Check cached symbol tables only after API requests come back with no info. --- src/profile-logic/symbol-store.ts | 150 ++++++++++++++--------------- src/test/unit/symbol-store.test.ts | 11 ++- 2 files changed, 79 insertions(+), 82 deletions(-) diff --git a/src/profile-logic/symbol-store.ts b/src/profile-logic/symbol-store.ts index 55def604d7..7b6af786a6 100644 --- a/src/profile-logic/symbol-store.ts +++ b/src/profile-logic/symbol-store.ts @@ -281,10 +281,14 @@ export class SymbolStore { ): Promise { // For each library, we have three options to obtain symbol information for // it. We try all options in order, advancing to the next option on failure. - // Option 1: Symbol tables cached in the database, this._db. - // Option 2: Obtain symbols from the symbol server. - // Option 3: Obtain symbols from the browser, using the symbolication API. + // Option 1: Obtain symbols from the symbol server, using the symbolication API. + // Option 2: Obtain symbols from the browser, using the symbolication API. + // Option 3: Symbol tables cached in the database, this._db. // Option 4: Obtain symbols from the browser, via individual symbol tables. + // + // Symbol tables provide less information than the symbolication API (no inlines + // and no file / line information) so we try the API before we check for cached + // symbol tables. // Check requests for validity first. requests = requests.filter((request) => { @@ -303,48 +307,8 @@ export class SymbolStore { return true; }); - // First, try option 1 for all libraries and partition them by whether it - // was successful. - const requestsForNonCachedLibs: LibSymbolicationRequest[] = []; - const resultsForCachedLibs: Array<{ - lib: RequestedLib; - addresses: Set; - symbolTable: SymbolTableAsTuple; - }> = []; - if (ignoreCache) { - requestsForNonCachedLibs.push(...requests); - } else { - await Promise.all( - requests.map(async (request) => { - const { lib, addresses } = request; - const { debugName, breakpadId } = lib; - try { - // Try to get the symbol table from the database. - // This call will throw if the symbol table is not present. - const symbolTable = await this._db.getSymbolTable( - debugName, - breakpadId - ); - - // Did not throw, option 1 was successful! - resultsForCachedLibs.push({ - lib, - addresses, - symbolTable, - }); - } catch (e) { - if (!(e instanceof SymbolsNotFoundError)) { - // rethrow JavaScript programming error - throw e; - } - requestsForNonCachedLibs.push(request); - } - }) - ); - } - - // First phase of option 2: - // Try to service requestsForNonCachedLibs using the symbolication API, + // Option 1: + // Try to service requests using the symbolication API, // requesting chunks of max 10000 addresses each. In reality, this usually // means that all addresses for libxul get processed in one chunk, and the // addresses from all other libraries get processed in a second chunk. @@ -354,7 +318,7 @@ export class SymbolStore { // latency also suffers because of per-request overhead and pipelining limits. // We also limit each chunk to at most MAX_JOB_COUNT_PER_CHUNK libraries. const chunks = _partitionIntoChunksOfMaxValue( - requestsForNonCachedLibs, + requests, 10000, MAX_JOB_COUNT_PER_CHUNK, ({ addresses }) => addresses.size @@ -369,68 +333,100 @@ export class SymbolStore { this._symbolProvider.requestSymbolsFromServer(requests), ]); - // Finalize requests for which option 1 was successful: - // Now that the requests to the server have been kicked off, process - // symbolication for the libraries for which we found symbol tables in the - // database. This is delayed until after the request has been kicked off - // because it can take some time. - - // We also need a demangling function for this, which is in an async module. - const demangleCallback = await _getDemangleCallback(); - - for (const { lib, addresses, symbolTable } of resultsForCachedLibs) { - successCb( - lib, - readSymbolsFromSymbolTable(addresses, symbolTable, demangleCallback) - ); - } - // Process the results from the symbolication API request, as they arrive. // For unsuccessful requests, fall back to _getSymbolsFromBrowser. await Promise.all( symbolicationApiRequestsAndResponsesPerChunk.map( - async ([option2Requests, option2ResponsePromise]) => { + async ([chunkRequests, chunkResponsePromise]) => { // Store any errors encountered along the way in this map. // We will report them if all avenues fail. const errorMap: Map = new Map( requests.map((r): [LibSymbolicationRequest, Error[]] => [r, []]) ); - // Process the results of option 2: The response from the Mozilla symbolication APi. - const option3Requests = + // Process the results of option 1: The response from the Mozilla symbolication APi. + const requestsRemainingAfterServerAPI = await this._processSuccessfulResponsesAndReturnRemaining( - option2Requests, - option2ResponsePromise, + chunkRequests, + chunkResponsePromise, errorMap, successCb ); - if (option3Requests.length === 0) { + if (requestsRemainingAfterServerAPI.length === 0) { // Done! return; } - // Some libraries are still remaining, try option 3 for them. - // Option 3 is symbolProvider.requestSymbolsFromBrowser. - const option3ResponsePromise = - this._symbolProvider.requestSymbolsFromBrowser(option3Requests); - const option4Requests = + // Some libraries are still remaining, try option 2 for them. + // Option 2 is symbolProvider.requestSymbolsFromBrowser. + const browserAPIResponsePromise = + this._symbolProvider.requestSymbolsFromBrowser( + requestsRemainingAfterServerAPI + ); + const requestsRemainingAfterBrowserAPI = await this._processSuccessfulResponsesAndReturnRemaining( - option3Requests, - option3ResponsePromise, + requestsRemainingAfterServerAPI, + browserAPIResponsePromise, errorMap, successCb ); - if (option4Requests.length === 0) { + if (requestsRemainingAfterBrowserAPI.length === 0) { // Done! return; } + // Now we've reached the stage where we check for symbol tables. + // Check this._db first, and then call this._getSymbolTablesFromBrowser + // for the remainder. + + // We also need a demangling function for this, which is in an async module. + const demangleCallback = await _getDemangleCallback(); + + const requestsRemainingAfterCacheCheck: LibSymbolicationRequest[] = + []; + if (ignoreCache) { + requestsRemainingAfterCacheCheck.push( + ...requestsRemainingAfterBrowserAPI + ); + } else { + await Promise.all( + requestsRemainingAfterBrowserAPI.map(async (request) => { + const { lib, addresses } = request; + const { debugName, breakpadId } = lib; + try { + // Try to get the symbol table from the database. + // This call will throw if the symbol table is not present. + const symbolTable = await this._db.getSymbolTable( + debugName, + breakpadId + ); + + // Did not throw, option 3 was successful! + successCb( + lib, + readSymbolsFromSymbolTable( + addresses, + symbolTable, + demangleCallback + ) + ); + } catch (e) { + if (!(e instanceof SymbolsNotFoundError)) { + // rethrow JavaScript programming error + throw e; + } + requestsRemainingAfterCacheCheck.push(request); + } + }) + ); + } + // Some libraries are still remaining, try option 4 for them. // Option 4 is symbolProvider.requestSymbolTableFromBrowser. await this._getSymbolTablesFromBrowser( - option4Requests, + requestsRemainingAfterCacheCheck, errorMap, demangleCallback, successCb, diff --git a/src/test/unit/symbol-store.test.ts b/src/test/unit/symbol-store.test.ts index 52aeedc00c..78a3ace1f6 100644 --- a/src/test/unit/symbol-store.test.ts +++ b/src/test/unit/symbol-store.test.ts @@ -326,13 +326,14 @@ describe('SymbolStore', function () { ); // The symbolStore should already have a cached symbol table for lib2 now, - // so requestSymbolsFromServer should only have been called for one request. + // but the cache is only checked after the API requests, so both libraries + // are sent to requestSymbolsFromServer. expect(symbolProvider.requestSymbolsFromServer).toHaveBeenCalledTimes(2); - expect(symbolsForAddressesRequestCount).toEqual(3); + expect(symbolsForAddressesRequestCount).toEqual(4); expect(errorCallback).not.toHaveBeenCalled(); - // requestSymbolsFromServer should have succeeded for that one request, - // so requestSymbolTableFromBrowser should not have been called again. + // requestSymbolsFromServer should have succeeded for lib1 and failed for lib2. + // lib2 is then found in the cache, so requestSymbolTableFromBrowser is not called again. expect(symbolProvider.requestSymbolTableFromBrowser).toHaveBeenCalledTimes( 1 ); @@ -452,7 +453,7 @@ describe('SymbolStore', function () { // Empty debugNames or breakpadIds should cause errors. And if symbols are // not available from any source, all errors along the way should be included // in the reported error. - expect(failedLibs.size).toBe(3); + expect([...failedLibs]).toBeArrayOfSize(3); expect(failedLibs.get('empty-breakpadid')).toEqual( expect.objectContaining({ message: expect.stringContaining('Invalid debugName or breakpadId'), From 0d48bc0905f5b234718f228a6d35488be53f6faf Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Mon, 5 Jan 2026 19:09:24 +0100 Subject: [PATCH 2/7] Make option 4 work more like options 1 and 2. --- src/profile-logic/symbol-store.ts | 104 +++++++++++++++++------------- 1 file changed, 59 insertions(+), 45 deletions(-) diff --git a/src/profile-logic/symbol-store.ts b/src/profile-logic/symbol-store.ts index 7b6af786a6..5c83108e51 100644 --- a/src/profile-logic/symbol-store.ts +++ b/src/profile-logic/symbol-store.ts @@ -378,7 +378,7 @@ export class SymbolStore { } // Now we've reached the stage where we check for symbol tables. - // Check this._db first, and then call this._getSymbolTablesFromBrowser + // Check this._db first, and then call this._getSymbolsViaBrowserSymbolTables // for the remainder. // We also need a demangling function for this, which is in an async module. @@ -424,14 +424,39 @@ export class SymbolStore { } // Some libraries are still remaining, try option 4 for them. - // Option 4 is symbolProvider.requestSymbolTableFromBrowser. - await this._getSymbolTablesFromBrowser( - requestsRemainingAfterCacheCheck, - errorMap, - demangleCallback, - successCb, - errorCb - ); + // Option 4: Request a symbol table from the browser. + const responsePromiseViaSymbolTable = + this._getSymbolsViaBrowserSymbolTables( + requestsRemainingAfterCacheCheck, + demangleCallback + ); + + const failedRequests = + await this._processSuccessfulResponsesAndReturnRemaining( + requestsRemainingAfterCacheCheck, + responsePromiseViaSymbolTable, + errorMap, + successCb + ); + + if (failedRequests.length === 0) { + // Done! + return; + } + + for (const request of failedRequests) { + const { lib } = request; + // None of the symbolication methods were successful. + // Call the error callback with all accumulated errors. + errorCb( + request, + new SymbolsNotFoundError( + `Could not obtain symbols for ${lib.debugName}/${lib.breakpadId}.`, + lib, + ...(errorMap.get(request) ?? []) + ) + ); + } } ) ); @@ -485,42 +510,31 @@ export class SymbolStore { // 2. Android system libraries, even in modern versions of Firefox. We don't // support querySymbolicationApi for them yet, see // https://bugzilla.mozilla.org/show_bug.cgi?id=1735897 - async _getSymbolTablesFromBrowser( + async _getSymbolsViaBrowserSymbolTables( requests: LibSymbolicationRequest[], - errorMap: Map, - demangleCallback: DemangleFunction, - successCb: (lib: RequestedLib, results: Map) => void, - errorCb: (request: LibSymbolicationRequest, error: Error) => void - ): Promise { - for (const request of requests) { - const { lib, addresses } = request; - try { - // Option 4: Request a symbol table from the browser. - // This call will throw if the browser cannot obtain the symbol table. - const symbolTable = - await this._symbolProvider.requestSymbolTableFromBrowser(lib); - - // Did not throw, option 4 was successful! - successCb( - lib, - readSymbolsFromSymbolTable(addresses, symbolTable, demangleCallback) - ); - - // Store the symbol table in the database. - await this._storeSymbolTableInDB(lib, symbolTable); - } catch (lastError) { - // None of the symbolication methods were successful. - // Call the error callback with all accumulated errors. - errorCb( - request, - new SymbolsNotFoundError( - `Could not obtain symbols for ${lib.debugName}/${lib.breakpadId}.`, - lib, - ...(errorMap.get(request) ?? []), - lastError - ) - ); - } - } + demangleCallback: DemangleFunction + ): Promise { + return Promise.all( + requests.map(async (request) => { + const { lib, addresses } = request; + try { + // This call will throw if the browser cannot obtain the symbol table. + const symbolTable = + await this._symbolProvider.requestSymbolTableFromBrowser(lib); + + // Store the symbol table in the database. + await this._storeSymbolTableInDB(lib, symbolTable); + + const results = readSymbolsFromSymbolTable( + addresses, + symbolTable, + demangleCallback + ); + return { type: 'SUCCESS', lib, results }; + } catch (error) { + return { type: 'ERROR', request, error }; + } + }) + ); } } From 9b62de6c080cc3b908bfdf68f0533170568d8c58 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sun, 4 Jan 2026 19:50:35 +0100 Subject: [PATCH 3/7] Create a proper class for the symbol provider that getSymbolStore passes to the SymbolStore it creates. --- src/actions/receive-profile.ts | 157 ++++++++++++++++++++------------- 1 file changed, 97 insertions(+), 60 deletions(-) diff --git a/src/actions/receive-profile.ts b/src/actions/receive-profile.ts index bfca2b3ba0..809723214a 100644 --- a/src/actions/receive-profile.ts +++ b/src/actions/receive-profile.ts @@ -94,7 +94,12 @@ import type { BrowserConnection, BrowserConnectionStatus, } from '../app-logic/browser-connection'; -import type { LibSymbolicationRequest } from '../profile-logic/symbol-store'; +import type { + LibSymbolicationRequest, + LibSymbolicationResponse, + SymbolProvider, +} from '../profile-logic/symbol-store'; +import type { SymbolTableAsTuple } from 'firefox-profiler/profile-logic/symbol-store-db'; /** * This file collects all the actions that are used for receiving the profile in the @@ -569,13 +574,44 @@ function getSymbolStore( return null; } - async function requestSymbolsWithCallback( + const symbolProvider = new RegularSymbolProvider( + dispatch, + symbolServerUrl, + browserConnection + ); + + // Note, the database name still references the old project name, "perf.html". It was + // left the same as to not invalidate user's information. + return new SymbolStore('perf-html-async-storage', symbolProvider); +} + +/** + * The regular implementation of the SymbolProvider interface: Symbols are requested + * from a server via fetch, and from the browser via a BrowserConnection (if present). + * State changes are notified via redux actions to the provided `dispatch` function. + */ +class RegularSymbolProvider implements SymbolProvider { + _dispatch: Dispatch; + _symbolServerUrl: string; + _browserConnection: BrowserConnection | null; + + constructor( + dispatch: Dispatch, + symbolServerUrl: string, + browserConnection: BrowserConnection | null + ) { + this._dispatch = dispatch; + this._symbolServerUrl = symbolServerUrl; + this._browserConnection = browserConnection; + } + + async _makeSymbolicationAPIRequestWithCallback( symbolSupplierName: string, requests: LibSymbolicationRequest[], callback: (path: string, requestJson: string) => Promise ) { for (const { lib } of requests) { - dispatch(requestingSymbolTable(lib)); + this._dispatch(requestingSymbolTable(lib)); } try { return await MozillaSymbolicationAPI.requestSymbols( @@ -589,73 +625,74 @@ function getSymbolStore( ); } finally { for (const { lib } of requests) { - dispatch(receivedSymbolTableReply(lib)); + this._dispatch(receivedSymbolTableReply(lib)); } } } - // Note, the database name still references the old project name, "perf.html". It was - // left the same as to not invalidate user's information. - const symbolStore = new SymbolStore('perf-html-async-storage', { - requestSymbolsFromServer: (requests) => - requestSymbolsWithCallback( - 'symbol server', - requests, - async (path, json) => { - const response = await fetch(symbolServerUrl + path, { - body: json, - method: 'POST', - mode: 'cors', - // Use a profiler-specific user agent, so that the symbolication server knows - // what's making this request. - headers: new Headers({ - 'User-Agent': `FirefoxProfiler/1.0 (+${location.origin})`, - }), - }); - return response.json(); - } - ), - - requestSymbolsFromBrowser: async (requests) => { - if (browserConnection === null) { - throw new Error( - 'No connection to the browser, cannot run querySymbolicationApi' - ); + requestSymbolsFromServer( + requests: LibSymbolicationRequest[] + ): Promise { + return this._makeSymbolicationAPIRequestWithCallback( + 'symbol server', + requests, + async (path, json) => { + const response = await fetch(this._symbolServerUrl + path, { + body: json, + method: 'POST', + mode: 'cors', + // Use a profiler-specific user agent, so that the symbolication server knows + // what's making this request. + headers: new Headers({ + 'User-Agent': `FirefoxProfiler/1.0 (+${location.origin})`, + }), + }); + return response.json(); } + ); + } - const bc = browserConnection; - return requestSymbolsWithCallback( - 'browser', - requests, - async (path, json) => - JSON.parse(await bc.querySymbolicationApi(path, json)) + async requestSymbolsFromBrowser( + requests: LibSymbolicationRequest[] + ): Promise { + if (this._browserConnection === null) { + throw new Error( + 'No connection to the browser, cannot run querySymbolicationApi' ); - }, + } - requestSymbolTableFromBrowser: async (lib) => { - if (browserConnection === null) { - throw new Error( - 'No connection to the browser, cannot obtain symbol tables' - ); - } + const bc = this._browserConnection; + return this._makeSymbolicationAPIRequestWithCallback( + 'browser', + requests, + async (path, json) => + JSON.parse(await bc.querySymbolicationApi(path, json)) + ); + } - const { debugName, breakpadId } = lib; - dispatch(requestingSymbolTable(lib)); - try { - const symbolTable = await browserConnection.getSymbolTable( - debugName, - breakpadId - ); - dispatch(receivedSymbolTableReply(lib)); - return symbolTable; - } catch (error) { - dispatch(receivedSymbolTableReply(lib)); - throw error; - } - }, - }); + async requestSymbolTableFromBrowser( + lib: RequestedLib + ): Promise { + if (this._browserConnection === null) { + throw new Error( + 'No connection to the browser, cannot obtain symbol tables' + ); + } - return symbolStore; + const { debugName, breakpadId } = lib; + this._dispatch(requestingSymbolTable(lib)); + try { + const symbolTable = await this._browserConnection.getSymbolTable( + debugName, + breakpadId + ); + this._dispatch(receivedSymbolTableReply(lib)); + return symbolTable; + } catch (error) { + this._dispatch(receivedSymbolTableReply(lib)); + throw error; + } + } } export async function doSymbolicateProfile( From 2b6c4861d20616f8f4b691eb3d2f28e4baa8b05b Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Tue, 6 Jan 2026 09:18:26 +0100 Subject: [PATCH 4/7] Make SymbolStoreDB.getSymbolTable return null if the symbol table isn't in the database. --- src/profile-logic/symbol-store-db.ts | 21 ++++++---------- src/profile-logic/symbol-store.ts | 33 ++++++++++---------------- src/symbolicator-cli/index.ts | 12 ++-------- src/test/store/receive-profile.test.ts | 10 +------- src/test/unit/symbol-store-db.test.ts | 8 +++---- src/test/unit/symbolicator-cli.test.ts | 6 ++--- src/types/symbolication.ts | 12 +++++----- 7 files changed, 33 insertions(+), 69 deletions(-) diff --git a/src/profile-logic/symbol-store-db.ts b/src/profile-logic/symbol-store-db.ts index 835db72568..dd12b8ee03 100644 --- a/src/profile-logic/symbol-store-db.ts +++ b/src/profile-logic/symbol-store-db.ts @@ -2,8 +2,6 @@ * 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 { SymbolsNotFoundError } from './errors'; - // Contains a symbol table, which can be used to map addresses to strings. // Symbol tables of this format are created within Firefox's implementation of // the geckoProfiler WebExtension API, at @@ -182,16 +180,16 @@ export default class SymbolStoreDB { /** * Retrieve the symbol table for the given library. - * @param {string} The debugName of the library. - * @param {string} The breakpadId of the library. - * @return A promise that resolves with the symbol table (in - * SymbolTableAsTuple format), or fails if we couldn't - * find a symbol table for the requested library. + * @param {string} debugName The debugName of the library. + * @param {string} breakpadId The breakpadId of the library. + * @return A promise that resolves with the symbol table (in + * SymbolTableAsTuple format), with null if we couldn't + * find a symbol table for the requested library. */ getSymbolTable( debugName: string, breakpadId: string - ): Promise { + ): Promise { return this._getDB().then((db) => { return new Promise((resolve, reject) => { const transaction = db.transaction('symbol-tables', 'readwrite'); @@ -207,12 +205,7 @@ export default class SymbolStoreDB { const { addrs, index, buffer } = value; updateDateReq.onsuccess = () => resolve([addrs, index, buffer]); } else { - reject( - new SymbolsNotFoundError( - 'The requested library does not exist in the database.', - { debugName, breakpadId } - ) - ); + resolve(null); } }; }); diff --git a/src/profile-logic/symbol-store.ts b/src/profile-logic/symbol-store.ts index 5c83108e51..fcb605fbf3 100644 --- a/src/profile-logic/symbol-store.ts +++ b/src/profile-logic/symbol-store.ts @@ -395,28 +395,19 @@ export class SymbolStore { requestsRemainingAfterBrowserAPI.map(async (request) => { const { lib, addresses } = request; const { debugName, breakpadId } = lib; - try { - // Try to get the symbol table from the database. - // This call will throw if the symbol table is not present. - const symbolTable = await this._db.getSymbolTable( - debugName, - breakpadId + const symbolTable = await this._db.getSymbolTable( + debugName, + breakpadId + ); + if (symbolTable !== null) { + // Option 3 was successful! + const results = readSymbolsFromSymbolTable( + addresses, + symbolTable, + demangleCallback ); - - // Did not throw, option 3 was successful! - successCb( - lib, - readSymbolsFromSymbolTable( - addresses, - symbolTable, - demangleCallback - ) - ); - } catch (e) { - if (!(e instanceof SymbolsNotFoundError)) { - // rethrow JavaScript programming error - throw e; - } + successCb(lib, results); + } else { requestsRemainingAfterCacheCheck.push(request); } }) diff --git a/src/symbolicator-cli/index.ts b/src/symbolicator-cli/index.ts index 2e917c7a52..f86db56998 100644 --- a/src/symbolicator-cli/index.ts +++ b/src/symbolicator-cli/index.ts @@ -25,7 +25,6 @@ import { import type { SymbolicationStepInfo } from '../profile-logic/symbolication'; import type { SymbolTableAsTuple } from '../profile-logic/symbol-store-db'; import * as MozillaSymbolicationAPI from '../profile-logic/mozilla-symbolication-api'; -import { SymbolsNotFoundError } from '../profile-logic/errors'; import type { ThreadIndex } from '../types'; /** @@ -56,16 +55,9 @@ export class InMemorySymbolDB { async getSymbolTable( debugName: string, breakpadId: string - ): Promise { + ): Promise { const key = this._makeKey(debugName, breakpadId); - const value = this._store.get(key); - if (typeof value !== 'undefined') { - return value; - } - throw new SymbolsNotFoundError( - 'The requested library does not exist in the database.', - { debugName, breakpadId } - ); + return this._store.get(key) ?? null; } async close(): Promise {} diff --git a/src/test/store/receive-profile.test.ts b/src/test/store/receive-profile.test.ts index 13a5618f8b..392fb2e833 100644 --- a/src/test/store/receive-profile.test.ts +++ b/src/test/store/receive-profile.test.ts @@ -84,14 +84,7 @@ function simulateSymbolStoreHasNoCache() { (SymbolStoreDB as any).mockImplementation(() => ({ getSymbolTable: jest .fn() - .mockImplementation((debugName, breakpadId) => - Promise.reject( - new SymbolsNotFoundError( - 'The requested library does not exist in the database.', - { debugName, breakpadId } - ) - ) - ), + .mockImplementation((_debugName, _breakpadId) => Promise.resolve(null)), })); } @@ -781,7 +774,6 @@ describe('actions/receive-profile', function () { const browserConnectionStatus = await createBrowserConnection('Firefox/123.0'); await dispatch(retrieveProfileFromBrowser(browserConnectionStatus)); - expect(console.warn).toHaveBeenCalledTimes(2); const state = getState(); expect(getView(state)).toEqual({ phase: 'DATA_LOADED' }); diff --git a/src/test/unit/symbol-store-db.test.ts b/src/test/unit/symbol-store-db.test.ts index 65c84247ff..22c954028f 100644 --- a/src/test/unit/symbol-store-db.test.ts +++ b/src/test/unit/symbol-store-db.test.ts @@ -41,7 +41,7 @@ describe('SymbolStoreDB', function () { for (let i = 0; i < 5; i++) { await expect( symbolStoreDB.getSymbolTable(libs[i].debugName, libs[i].breakpadId) - ).rejects.toBeInstanceOf(Error); + ).resolves.toBeNull(); // .rejects.toMatch('does not exist in the database'); // TODO Some future verison of jest should make this work } @@ -61,8 +61,7 @@ describe('SymbolStoreDB', function () { for (let i = 0; i < 5; i++) { await expect( symbolStoreDB.getSymbolTable(libs[i].debugName, libs[i].breakpadId) - ).rejects.toBeInstanceOf(Error); - // .rejects.toMatch('does not exist in the database'); // TODO Some future verison of jest should make this work + ).resolves.toBeNull(); } for (let i = 5; i < 10; i++) { @@ -85,8 +84,7 @@ describe('SymbolStoreDB', function () { for (let i = 0; i < 10; i++) { await expect( symbolStoreDB.getSymbolTable(libs[i].debugName, libs[i].breakpadId) - ).rejects.toBeInstanceOf(Error); - // .rejects.toMatch('does not exist in the database'); // TODO Some future verison of jest should make this work + ).resolves.toBeNull(); } await symbolStoreDB.close(); diff --git a/src/test/unit/symbolicator-cli.test.ts b/src/test/unit/symbolicator-cli.test.ts index 84b5bb3174..50bac0c519 100644 --- a/src/test/unit/symbolicator-cli.test.ts +++ b/src/test/unit/symbolicator-cli.test.ts @@ -3,7 +3,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { InMemorySymbolDB, makeOptionsFromArgv } from '../../symbolicator-cli'; import { completeSymbolTableAsTuple } from '../fixtures/example-symbol-table'; -import { SymbolsNotFoundError } from '../../profile-logic/errors'; describe('makeOptionsFromArgv', function () { const commonArgs = ['/path/to/node', '/path/to/symbolicator-cli.js']; @@ -120,8 +119,7 @@ describe('InMemorySymbolDB', function () { it('should throw when getting a SymbolTable that was not set', async function () { const db = new InMemorySymbolDB(); - await expect(async () => { - await db.getSymbolTable(debugName, breakpadId); - }).rejects.toThrow(SymbolsNotFoundError); + const tableOrNull = await db.getSymbolTable(debugName, breakpadId); + await expect(tableOrNull).toBeNull(); }); }); diff --git a/src/types/symbolication.ts b/src/types/symbolication.ts index 5935020119..4162e488e6 100644 --- a/src/types/symbolication.ts +++ b/src/types/symbolication.ts @@ -19,16 +19,16 @@ export interface ISymbolStoreDB { /** * Retrieve the symbol table for the given library. - * @param {string} The debugName of the library. - * @param {string} The breakpadId of the library. - * @return A promise that resolves with the symbol table (in - * SymbolTableAsTuple format), or fails if we couldn't - * find a symbol table for the requested library. + * @param {string} debugName The debugName of the library. + * @param {string} breakpadId The breakpadId of the library. + * @return A promise that resolves with the symbol table (in + * SymbolTableAsTuple format), with null if we couldn't + * find a symbol table for the requested library. */ getSymbolTable( debugName: string, breakpadId: string - ): Promise; + ): Promise; close(): Promise; } From f53bdfae279973936843034e551e4788d1635992 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Tue, 16 Dec 2025 08:59:08 -0500 Subject: [PATCH 5/7] Move symbol table caching and demangling into RegularSymbolProvider. This removes the demangling dependency from symbol-store.ts. It also lets us remove the unused "database" from symbolicator-cli. The cache only exists so that we don't give the browser unnecessary work to recompute the symbol table. So we only need to access the cache if we're about to ask the browser for a full symbol table. --- src/actions/receive-profile.ts | 115 ++++++++++++++- src/profile-logic/symbol-store.ts | 166 +++------------------ src/symbolicator-cli/index.ts | 43 +----- src/test/store/symbolication.test.ts | 86 +++-------- src/test/unit/symbol-store.test.ts | 194 +++++++------------------ src/test/unit/symbolicator-cli.test.ts | 28 +--- 6 files changed, 210 insertions(+), 422 deletions(-) diff --git a/src/actions/receive-profile.ts b/src/actions/receive-profile.ts index 809723214a..474d86914e 100644 --- a/src/actions/receive-profile.ts +++ b/src/actions/receive-profile.ts @@ -10,7 +10,10 @@ import { processGeckoProfile, unserializeProfileOfArbitraryFormat, } from 'firefox-profiler/profile-logic/process-profile'; -import { SymbolStore } from 'firefox-profiler/profile-logic/symbol-store'; +import { + readSymbolsFromSymbolTable, + SymbolStore, +} from 'firefox-profiler/profile-logic/symbol-store'; import { symbolicateProfile, applySymbolicationSteps, @@ -82,6 +85,7 @@ import type { TabID, PageList, MixedObject, + ISymbolStoreDB, } from 'firefox-profiler/types'; import type { @@ -95,11 +99,13 @@ import type { BrowserConnectionStatus, } from '../app-logic/browser-connection'; import type { + AddressResult, LibSymbolicationRequest, LibSymbolicationResponse, SymbolProvider, } from '../profile-logic/symbol-store'; import type { SymbolTableAsTuple } from 'firefox-profiler/profile-logic/symbol-store-db'; +import SymbolStoreDB from 'firefox-profiler/profile-logic/symbol-store-db'; /** * This file collects all the actions that are used for receiving the profile in the @@ -574,17 +580,19 @@ function getSymbolStore( return null; } + // Note, the database name still references the old project name, "perf.html". It was + // left the same as to not invalidate user's information. const symbolProvider = new RegularSymbolProvider( + 'perf-html-async-storage', dispatch, symbolServerUrl, browserConnection ); - - // Note, the database name still references the old project name, "perf.html". It was - // left the same as to not invalidate user's information. - return new SymbolStore('perf-html-async-storage', symbolProvider); + return new SymbolStore(symbolProvider); } +type DemangleFunction = (name: string) => string; + /** * The regular implementation of the SymbolProvider interface: Symbols are requested * from a server via fetch, and from the browser via a BrowserConnection (if present). @@ -594,8 +602,11 @@ class RegularSymbolProvider implements SymbolProvider { _dispatch: Dispatch; _symbolServerUrl: string; _browserConnection: BrowserConnection | null; + _symbolDb: ISymbolStoreDB; + _demangleCallback: DemangleFunction | null = null; constructor( + dbNamePrefixOrDB: string | ISymbolStoreDB, dispatch: Dispatch, symbolServerUrl: string, browserConnection: BrowserConnection | null @@ -603,6 +614,12 @@ class RegularSymbolProvider implements SymbolProvider { this._dispatch = dispatch; this._symbolServerUrl = symbolServerUrl; this._browserConnection = browserConnection; + + if (typeof dbNamePrefixOrDB === 'string') { + this._symbolDb = new SymbolStoreDB(`${dbNamePrefixOrDB}-symbol-tables`); + } else { + this._symbolDb = dbNamePrefixOrDB; + } } async _makeSymbolicationAPIRequestWithCallback( @@ -670,7 +687,70 @@ class RegularSymbolProvider implements SymbolProvider { ); } - async requestSymbolTableFromBrowser( + /** + * This function returns a function that can demangle function name using a + * WebAssembly module, but falls back on the identity function if the + * WebAssembly module isn't available for some reason. + */ + async _createDemangleCallback(): Promise { + try { + // When this module imports some WebAssembly module, Webpack's mechanism + // invokes the WebAssembly object which might be absent in some browsers, + // therefore `import` can throw. Also some browsers might refuse to load a + // wasm module because of our CSP. + // See webpack bug https://github.com/webpack/webpack/issues/8517 + const { demangle_any } = await import('gecko-profiler-demangle'); + return demangle_any; + } catch (error) { + // Module loading can fail (for example in browsers without WebAssembly + // support, or due to bad server configuration), so we will fall back + // to a pass-through function if that happens. + console.error('Demangling module could not be imported.', error); + return (mangledString) => mangledString; + } + } + + async _getDemangleCallback(): Promise { + return (this._demangleCallback ??= await this._createDemangleCallback()); + } + + // Try to get individual symbol tables from the browser, for any libraries + // which couldn't be symbolicated with the symbolication API. + // This is needed for two cases: + // 1. Firefox 95 and earlier, which didn't have a querySymbolicationApi + // WebChannel access point, and only supports symbol tables. + // 2. Android system libraries, even in modern versions of Firefox. We don't + // support querySymbolicationApi for them yet, see + // https://bugzilla.mozilla.org/show_bug.cgi?id=1735897 + async requestSymbolsViaSymbolTableFromBrowser( + request: LibSymbolicationRequest, + ignoreCache: boolean + ): Promise> { + // Check this._symbolDb first, and then call this._getSymbolTablesFromBrowser + // if we couldn't find the table in the cache. + + // We also need a demangling function for this, which is in an async module. + const demangleCallback = await this._getDemangleCallback(); + + const { lib, addresses } = request; + const { debugName, breakpadId } = lib; + let symbolTable = null; + + if (!ignoreCache) { + // Try to get the symbol table from the database. + // This call will return null if the symbol table is not present. + symbolTable = await this._symbolDb.getSymbolTable(debugName, breakpadId); + } + + if (symbolTable === null) { + symbolTable = await this._requestSymbolTableFromBrowser(lib); + this._storeSymbolTableInDB(lib, symbolTable); + } + + return readSymbolsFromSymbolTable(addresses, symbolTable, demangleCallback); + } + + async _requestSymbolTableFromBrowser( lib: RequestedLib ): Promise { if (this._browserConnection === null) { @@ -693,6 +773,29 @@ class RegularSymbolProvider implements SymbolProvider { throw error; } } + + // Store a symbol table in the database. This is only used for symbol tables + // and not for partial symbol results. Symbol tables are obtained from the + // browser via the geckoProfiler object which is defined in a frame script: + // https://searchfox.org/mozilla-central/rev/a9db89754fb507254cb8422e5a00af7c10d98264/devtools/client/performance-new/frame-script.js#67-81 + // + // We do not store results from the Mozilla symbolication API, because those + // only contain the symbols we requested and not all the symbols of a given + // library. + async _storeSymbolTableInDB( + lib: RequestedLib, + symbolTable: SymbolTableAsTuple + ): Promise { + const { debugName, breakpadId } = lib; + try { + await this._symbolDb.storeSymbolTable(debugName, breakpadId, symbolTable); + } catch (error) { + console.log( + `Failed to store the symbol table for ${debugName} in the database:`, + error + ); + } + } } export async function doSymbolicateProfile( diff --git a/src/profile-logic/symbol-store.ts b/src/profile-logic/symbol-store.ts index fcb605fbf3..df39c76a30 100644 --- a/src/profile-logic/symbol-store.ts +++ b/src/profile-logic/symbol-store.ts @@ -2,10 +2,9 @@ * 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 SymbolStoreDB from './symbol-store-db'; import { SymbolsNotFoundError } from './errors'; -import type { RequestedLib, ISymbolStoreDB } from 'firefox-profiler/types'; +import type { RequestedLib } from 'firefox-profiler/types'; import type { SymbolTableAsTuple } from './symbol-store-db'; import { ensureExists } from '../utils/types'; @@ -72,7 +71,10 @@ export interface SymbolProvider { // Expensive, should be called if the other two options were unsuccessful. // Does not carry file + line + inlines information. - requestSymbolTableFromBrowser(lib: RequestedLib): Promise; + requestSymbolsViaSymbolTableFromBrowser( + request: LibSymbolicationRequest, + ignoreCache: boolean + ): Promise>; } export interface AbstractSymbolStore { @@ -193,31 +195,6 @@ function _partitionIntoChunksOfMaxValue( return chunks.map(({ elements }) => elements); } -type DemangleFunction = (name: string) => string; - -/** - * This function returns a function that can demangle function name using a - * WebAssembly module, but falls back on the identity function if the - * WebAssembly module isn't available for some reason. - */ -async function _getDemangleCallback(): Promise { - try { - // When this module imports some WebAssembly module, Webpack's mechanism - // invokes the WebAssembly object which might be absent in some browsers, - // therefore `import` can throw. Also some browsers might refuse to load a - // wasm module because of our CSP. - // See webpack bug https://github.com/webpack/webpack/issues/8517 - const { demangle_any } = await import('gecko-profiler-demangle'); - return demangle_any; - } catch (error) { - // Module loading can fail (for example in browsers without WebAssembly - // support, or due to bad server configuration), so we will fall back - // to a pass-through function if that happens. - console.error('Demangling module could not be imported.', error); - return (mangledString) => mangledString; - } -} - /** * The SymbolStore implements efficient lookup of symbols for a set of addresses. * It consults multiple sources of symbol information and caches some results. @@ -226,44 +203,9 @@ async function _getDemangleCallback(): Promise { */ export class SymbolStore { _symbolProvider: SymbolProvider; - _db: ISymbolStoreDB; - constructor( - dbNamePrefixOrDB: string | ISymbolStoreDB, - symbolProvider: SymbolProvider - ) { + constructor(symbolProvider: SymbolProvider) { this._symbolProvider = symbolProvider; - if (typeof dbNamePrefixOrDB === 'string') { - this._db = new SymbolStoreDB(`${dbNamePrefixOrDB}-symbol-tables`); - } else { - this._db = dbNamePrefixOrDB; - } - } - - async closeDb() { - await this._db.close(); - } - - // Store a symbol table in the database. This is only used for symbol tables - // and not for partial symbol results. Symbol tables are obtained from the - // browser via the geckoProfiler object which is defined in a frame script: - // https://searchfox.org/mozilla-central/rev/a9db89754fb507254cb8422e5a00af7c10d98264/devtools/client/performance-new/frame-script.js#67-81 - // - // We do not store results from the Mozilla symbolication API, because those - // only contain the symbols we requested and not all the symbols of a given - // library. - _storeSymbolTableInDB( - lib: RequestedLib, - symbolTable: SymbolTableAsTuple - ): Promise { - return this._db - .storeSymbolTable(lib.debugName, lib.breakpadId, symbolTable) - .catch((error) => { - console.log( - `Failed to store the symbol table for ${lib.debugName} in the database:`, - error - ); - }); } /** @@ -283,8 +225,7 @@ export class SymbolStore { // it. We try all options in order, advancing to the next option on failure. // Option 1: Obtain symbols from the symbol server, using the symbolication API. // Option 2: Obtain symbols from the browser, using the symbolication API. - // Option 3: Symbol tables cached in the database, this._db. - // Option 4: Obtain symbols from the browser, via individual symbol tables. + // Option 3: Obtain symbols from the browser, via individual symbol tables. // // Symbol tables provide less information than the symbolication API (no inlines // and no file / line information) so we try the API before we check for cached @@ -377,55 +318,30 @@ export class SymbolStore { return; } + // Some libraries are still remaining, try option 3 for them. + // Option 3: Request a symbol table from the browser. // Now we've reached the stage where we check for symbol tables. - // Check this._db first, and then call this._getSymbolsViaBrowserSymbolTables - // for the remainder. - - // We also need a demangling function for this, which is in an async module. - const demangleCallback = await _getDemangleCallback(); - - const requestsRemainingAfterCacheCheck: LibSymbolicationRequest[] = - []; - if (ignoreCache) { - requestsRemainingAfterCacheCheck.push( - ...requestsRemainingAfterBrowserAPI - ); - } else { - await Promise.all( + const responsesPromise: Promise = + Promise.all( requestsRemainingAfterBrowserAPI.map(async (request) => { - const { lib, addresses } = request; - const { debugName, breakpadId } = lib; - const symbolTable = await this._db.getSymbolTable( - debugName, - breakpadId - ); - if (symbolTable !== null) { - // Option 3 was successful! - const results = readSymbolsFromSymbolTable( - addresses, - symbolTable, - demangleCallback - ); - successCb(lib, results); - } else { - requestsRemainingAfterCacheCheck.push(request); + try { + const { lib } = request; + const results = + await this._symbolProvider.requestSymbolsViaSymbolTableFromBrowser( + request, + ignoreCache + ); + return { type: 'SUCCESS', lib, results }; + } catch (error) { + return { type: 'ERROR', request, error }; } }) ); - } - - // Some libraries are still remaining, try option 4 for them. - // Option 4: Request a symbol table from the browser. - const responsePromiseViaSymbolTable = - this._getSymbolsViaBrowserSymbolTables( - requestsRemainingAfterCacheCheck, - demangleCallback - ); const failedRequests = await this._processSuccessfulResponsesAndReturnRemaining( - requestsRemainingAfterCacheCheck, - responsePromiseViaSymbolTable, + requestsRemainingAfterBrowserAPI, + responsesPromise, errorMap, successCb ); @@ -492,40 +408,4 @@ export class SymbolStore { return requests; } } - - // Try to get individual symbol tables from the browser, for any libraries - // which couldn't be symbolicated with the symbolication API. - // This is needed for two cases: - // 1. Firefox 95 and earlier, which didn't have a querySymbolicationApi - // WebChannel access point, and only supports symbol tables. - // 2. Android system libraries, even in modern versions of Firefox. We don't - // support querySymbolicationApi for them yet, see - // https://bugzilla.mozilla.org/show_bug.cgi?id=1735897 - async _getSymbolsViaBrowserSymbolTables( - requests: LibSymbolicationRequest[], - demangleCallback: DemangleFunction - ): Promise { - return Promise.all( - requests.map(async (request) => { - const { lib, addresses } = request; - try { - // This call will throw if the browser cannot obtain the symbol table. - const symbolTable = - await this._symbolProvider.requestSymbolTableFromBrowser(lib); - - // Store the symbol table in the database. - await this._storeSymbolTableInDB(lib, symbolTable); - - const results = readSymbolsFromSymbolTable( - addresses, - symbolTable, - demangleCallback - ); - return { type: 'SUCCESS', lib, results }; - } catch (error) { - return { type: 'ERROR', request, error }; - } - }) - ); - } } diff --git a/src/symbolicator-cli/index.ts b/src/symbolicator-cli/index.ts index f86db56998..797c4674b8 100644 --- a/src/symbolicator-cli/index.ts +++ b/src/symbolicator-cli/index.ts @@ -23,46 +23,9 @@ import { applySymbolicationSteps, } from '../profile-logic/symbolication'; import type { SymbolicationStepInfo } from '../profile-logic/symbolication'; -import type { SymbolTableAsTuple } from '../profile-logic/symbol-store-db'; import * as MozillaSymbolicationAPI from '../profile-logic/mozilla-symbolication-api'; import type { ThreadIndex } from '../types'; -/** - * Simple 'in-memory' symbol DB that conforms to the same interface as SymbolStoreDB but - * just stores everything in a simple dictionary instead of IndexedDB. The composite key - * [debugName, breakpadId] is flattened to a string "debugName:breakpadId" to use as the - * map key. - */ -export class InMemorySymbolDB { - _store: Map; - - constructor() { - this._store = new Map(); - } - - _makeKey(debugName: string, breakpadId: string): string { - return `${debugName}:${breakpadId}`; - } - - async storeSymbolTable( - debugName: string, - breakpadId: string, - symbolTable: SymbolTableAsTuple - ): Promise { - this._store.set(this._makeKey(debugName, breakpadId), symbolTable); - } - - async getSymbolTable( - debugName: string, - breakpadId: string - ): Promise { - const key = this._makeKey(debugName, breakpadId); - return this._store.get(key) ?? null; - } - - async close(): Promise {} -} - export interface CliOptions { input: string; output: string; @@ -83,14 +46,12 @@ export async function run(options: CliOptions) { throw new Error('Unable to parse the profile.'); } - const symbolStoreDB = new InMemorySymbolDB(); - /** * SymbolStore implementation which just forwards everything to the symbol server in * MozillaSymbolicationAPI format. No support for getting symbols from 'the browser' as * there is no browser in this context. */ - const symbolStore = new SymbolStore(symbolStoreDB, { + const symbolStore = new SymbolStore({ requestSymbolsFromServer: async (requests) => { for (const { lib } of requests) { console.log(` Loading symbols for ${lib.debugName}`); @@ -118,7 +79,7 @@ export async function run(options: CliOptions) { return []; }, - requestSymbolTableFromBrowser: async () => { + requestSymbolsViaSymbolTableFromBrowser: async () => { throw new Error('Not supported in this context'); }, }); diff --git a/src/test/store/symbolication.test.ts b/src/test/store/symbolication.test.ts index 8f7065ec8b..1eba0dfed1 100644 --- a/src/test/store/symbolication.test.ts +++ b/src/test/store/symbolication.test.ts @@ -9,14 +9,17 @@ import { partialSymbolTable, } from '../fixtures/example-symbol-table'; import type { ExampleSymbolTable } from '../fixtures/example-symbol-table'; -import type { MarkerPayload, RequestedLib } from 'firefox-profiler/types'; +import type { MarkerPayload } from 'firefox-profiler/types'; import type { AddressResult, LibSymbolicationRequest, LibSymbolicationResponse, SymbolProvider, } from '../../profile-logic/symbol-store'; -import { SymbolStore } from '../../profile-logic/symbol-store'; +import { + readSymbolsFromSymbolTable, + SymbolStore, +} from '../../profile-logic/symbol-store'; import * as ProfileViewSelectors from '../../selectors/profile'; import { selectedThreadSelectors } from '../../selectors/per-thread'; import { INTERVAL } from 'firefox-profiler/app-logic/constants'; @@ -34,10 +37,6 @@ import { assertSetContainsOnly } from '../fixtures/custom-assertions'; import { StringTable } from '../../utils/string-table'; import { ensureExists } from 'firefox-profiler/utils/types'; -// fake-indexeddb no longer includes a structuredClone polyfill, so we need to -// import it explicitly. -import 'core-js/stable/structured-clone'; -import { indexedDB, IDBKeyRange } from 'fake-indexeddb'; import { stripIndent } from 'common-tags'; import { SymbolsNotFoundError } from '../../profile-logic/errors'; @@ -46,21 +45,6 @@ import { SymbolsNotFoundError } from '../../profile-logic/errors'; * its own file. */ describe('doSymbolicateProfile', function () { - const symbolStoreName = 'test-db'; - beforeAll(function () { - // The SymbolStore requires IndexedDB, otherwise symbolication will be skipped. - window.indexedDB = indexedDB; - window.IDBKeyRange = IDBKeyRange; - }); - - afterAll(async function () { - // @ts-expect-error property must be optional - delete window.indexedDB; - // @ts-expect-error property must be optional - delete window.IDBKeyRange; - await _deleteDatabase(`${symbolStoreName}-symbol-tables`); - }); - // Initialize a store, an unsymbolicated profile, and helper functions. function init() { // The rejection in `requestSymbolsFromServer` outputs an error log, let's @@ -101,7 +85,7 @@ describe('doSymbolicateProfile', function () { type: 'ERROR' as const, request, error: new SymbolsNotFoundError( - 'Not in from-server mode, try requestSymbolTableFromBrowser.', + 'Not in from-server mode, try requestSymbolsViaSymbolTableFromBrowser.', lib ), }; @@ -123,7 +107,11 @@ describe('doSymbolicateProfile', function () { throw new Error('requestSymbolsFromBrowser unsupported in this test'); }, - requestSymbolTableFromBrowser: async (lib: RequestedLib) => { + requestSymbolsViaSymbolTableFromBrowser: async ( + request: LibSymbolicationRequest, + _ignoreCache: boolean + ) => { + const { lib, addresses } = request; if (lib.debugName !== 'firefox.pdb') { throw new SymbolsNotFoundError( 'Should only have libs called firefox.pdb', @@ -132,11 +120,14 @@ describe('doSymbolicateProfile', function () { } if (symbolicationProviderMode !== 'from-browser') { throw new Error( - 'should not call requestSymbolTableFromBrowser if requestSymbolsFromServer is successful' + 'should not call requestSymbolsViaSymbolTableFromBrowser if requestSymbolsFromServer is successful' ); } - - return symbolTable.asTuple; + return readSymbolsFromSymbolTable( + addresses, + symbolTable.asTuple, + (s: string) => s + ); }, }; @@ -155,7 +146,7 @@ describe('doSymbolicateProfile', function () { }), switchSymbolTable, switchSymbolProviderMode, - symbolStore: new SymbolStore(symbolStoreName, symbolProvider), + symbolStore: new SymbolStore(symbolProvider), }; } @@ -193,8 +184,7 @@ describe('doSymbolicateProfile', function () { ]); }); - it('uses the cache when available', async () => { - // This reuses the db from the previous test + it('does different merging depending on available symbols', async () => { const { store: { dispatch, getState }, profile, @@ -202,10 +192,6 @@ describe('doSymbolicateProfile', function () { switchSymbolTable, } = init(); - // This partial symbol table should not be used, because the db cache is - // used instead. - switchSymbolTable(partialSymbolTable); - await doSymbolicateProfile(dispatch, profile, symbolStore); expect(formatTree(getCallTree(getState()))).toEqual([ // 0x0000 and 0x000a get merged together. @@ -215,7 +201,8 @@ describe('doSymbolicateProfile', function () { '- second symbol (total: 1, self: 1)', ]); - // But the partial symbol table should be used when ignoring the cache. + // Now re-symbolicate with the partial symbol table. + switchSymbolTable(partialSymbolTable); await doSymbolicateProfile( dispatch, profile, @@ -226,25 +213,9 @@ describe('doSymbolicateProfile', function () { '- overencompassing first symbol (total: 4, self: 2)', ' - last symbol (total: 2, self: 2)', ]); - - // And then the cache should have been overwritten, let's check this by - // switching the symbol table again. - switchSymbolTable(completeSymbolTable); - // This time do not ignore the cache. - await doSymbolicateProfile(dispatch, profile, symbolStore); - // The result should be the same despite that the complete symbol table - // has been configured, this means the incomplete symbol table is in the - // DB cache. - expect(formatTree(getCallTree(getState()))).toEqual([ - '- overencompassing first symbol (total: 4, self: 2)', - ' - last symbol (total: 2, self: 2)', - ]); }); it('can symbolicate a profile when symbols come from-server', async () => { - // Get rid of any cached symbol tables from the previous test. - await _deleteDatabase(`${symbolStoreName}-symbol-tables`); - const { store: { dispatch, getState }, profile, @@ -402,7 +373,6 @@ describe('doSymbolicateProfile', function () { describe('merging of functions with different memory addresses, but in the same function', () => { it('starts with expanded call nodes of multiple memory addresses', async function () { - // Don't use the mocks on this test, as no SymbolStore database is needed. const { store: { dispatch, getState }, funcNamesToFuncIndexes, @@ -465,9 +435,6 @@ describe('doSymbolicateProfile', function () { }); it('can symbolicate a profile with a partial symbol table and re-symbolicate it with a complete symbol table', async () => { - // Get rid of any cached symbol tables from the previous tests. - await _deleteDatabase(`${symbolStoreName}-symbol-tables`); - const { store: { dispatch, getState }, symbolStore, @@ -514,9 +481,6 @@ describe('doSymbolicateProfile', function () { }); it('can re-symbolicate a partially-symbolicated profile even if it needs to add funcs to the funcTable', async () => { - // Get rid of any cached symbol tables from the previous tests. - await _deleteDatabase(`${symbolStoreName}-symbol-tables`); - const { store: { dispatch, getState }, symbolStore, @@ -661,11 +625,3 @@ function _createUnsymbolicatedProfile() { return profile; } - -function _deleteDatabase(dbName: string) { - return new Promise((resolve, reject) => { - const req = indexedDB.deleteDatabase(dbName); - req.onsuccess = () => resolve(undefined); - req.onerror = () => reject(req.error); - }); -} diff --git a/src/test/unit/symbol-store.test.ts b/src/test/unit/symbol-store.test.ts index 78a3ace1f6..4b88f7aa7d 100644 --- a/src/test/unit/symbol-store.test.ts +++ b/src/test/unit/symbol-store.test.ts @@ -3,16 +3,16 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import type { AddressResult, + LibSymbolicationRequest, LibSymbolicationResponse, SymbolProvider, } from '../../profile-logic/symbol-store'; -import { SymbolStore } from '../../profile-logic/symbol-store'; +import { + readSymbolsFromSymbolTable, + SymbolStore, +} from '../../profile-logic/symbol-store'; import { SymbolsNotFoundError } from '../../profile-logic/errors'; import { completeSymbolTableAsTuple } from '../fixtures/example-symbol-table'; -// fake-indexeddb no longer includes a structuredClone polyfill, so we need to -// import it explicitly. -import 'core-js/stable/structured-clone'; -import { indexedDB, IDBKeyRange } from 'fake-indexeddb'; import { FakeSymbolStore } from '../fixtures/fake-symbol-store'; import { ensureExists } from '../../utils/types'; import type { RequestedLib } from 'firefox-profiler/types'; @@ -21,38 +21,6 @@ describe('SymbolStore', function () { let symbolProvider: SymbolProvider | null = null; let symbolStore: SymbolStore | null = null; - function deleteDatabase() { - return new Promise((resolve, reject) => { - const req = indexedDB.deleteDatabase( - 'profiler-async-storage-symbol-tables' - ); - req.onsuccess = () => resolve(undefined); - req.onerror = () => reject(req.error); - }); - } - - beforeAll(function () { - // The SymbolStore requires IndexedDB, otherwise symbolication will be skipped. - window.indexedDB = indexedDB; - window.IDBKeyRange = IDBKeyRange; - }); - - afterAll(function () { - // @ts-expect-error - Check tsconfig DOM stuff in tests directory - delete window.indexedDB; - // @ts-expect-error - Check tsconfig DOM stuff in tests directory - delete window.IDBKeyRange; - - symbolStore = null; - }); - - afterEach(async function () { - if (symbolStore) { - await symbolStore.closeDb().catch(() => {}); - } - await deleteDatabase(); - }); - it('should only request symbols from the symbol provider once per library', async function () { symbolProvider = { requestSymbolsFromServer: jest.fn((requests) => @@ -70,13 +38,22 @@ describe('SymbolStore', function () { requestSymbolsFromBrowser: async (_requests) => { throw new Error('requestSymbolsFromBrowser unsupported in this test'); }, - requestSymbolTableFromBrowser: jest.fn(() => - Promise.resolve(completeSymbolTableAsTuple) + requestSymbolsViaSymbolTableFromBrowser: jest.fn( + (request: LibSymbolicationRequest, _ignoreCache: boolean) => { + const results = readSymbolsFromSymbolTable( + request.addresses, + completeSymbolTableAsTuple, + (s: string) => s + ); + return Promise.resolve(results); + } ), }; - symbolStore = new SymbolStore('profiler-async-storage', symbolProvider); + symbolStore = new SymbolStore(symbolProvider); - expect(symbolProvider.requestSymbolTableFromBrowser).not.toHaveBeenCalled(); + expect( + symbolProvider.requestSymbolsViaSymbolTableFromBrowser + ).not.toHaveBeenCalled(); const lib1 = { debugName: 'firefox', breakpadId: 'dont-care' }; let secondAndThirdSymbol = new Map(); @@ -89,9 +66,9 @@ describe('SymbolStore', function () { errorCallback ); expect(symbolProvider.requestSymbolsFromServer).toHaveBeenCalledTimes(1); - expect(symbolProvider.requestSymbolTableFromBrowser).toHaveBeenCalledTimes( - 1 - ); + expect( + symbolProvider.requestSymbolsViaSymbolTableFromBrowser + ).toHaveBeenCalledTimes(1); expect(errorCallback).not.toHaveBeenCalled(); expect(secondAndThirdSymbol.get(0xf01)).toEqual({ name: 'second symbol', @@ -114,9 +91,9 @@ describe('SymbolStore', function () { errorCallback ); expect(symbolProvider.requestSymbolsFromServer).toHaveBeenCalledTimes(2); - expect(symbolProvider.requestSymbolTableFromBrowser).toHaveBeenCalledTimes( - 2 - ); + expect( + symbolProvider.requestSymbolsViaSymbolTableFromBrowser + ).toHaveBeenCalledTimes(2); expect(errorCallback).not.toHaveBeenCalled(); expect(firstAndLastSymbol.get(0x33)).toEqual({ name: 'first symbol', @@ -138,9 +115,9 @@ describe('SymbolStore', function () { errorCallback ); expect(symbolProvider.requestSymbolsFromServer).toHaveBeenCalledTimes(2); - expect(symbolProvider.requestSymbolTableFromBrowser).toHaveBeenCalledTimes( - 2 - ); + expect( + symbolProvider.requestSymbolsViaSymbolTableFromBrowser + ).toHaveBeenCalledTimes(2); // Empty breakpadIds should result in an error. expect(errorCallback).toHaveBeenCalledWith( @@ -153,55 +130,6 @@ describe('SymbolStore', function () { ); }); - it('should persist in DB', async function () { - symbolProvider = { - requestSymbolsFromServer: jest.fn((requests) => - Promise.resolve( - requests.map((request) => ({ - type: 'ERROR', - request, - error: new Error('this example only supports symbol tables'), - })) - ) - ), - requestSymbolsFromBrowser: jest.fn((_requests) => - Promise.reject( - new Error('requestSymbolsFromBrowser unsupported in this test') - ) - ), - requestSymbolTableFromBrowser: jest.fn(() => - Promise.resolve(completeSymbolTableAsTuple) - ), - }; - symbolStore = new SymbolStore('profiler-async-storage', symbolProvider); - - const lib = { debugName: 'firefox', breakpadId: 'dont-care' }; - const errorCallback = jest.fn((_request, _error) => {}); - await symbolStore.getSymbols( - [{ lib: lib, addresses: new Set([0]) }], - (_request, _results) => {}, - errorCallback - ); - expect(errorCallback).not.toHaveBeenCalled(); - - // Using another symbol store simulates a page reload - // Due to https://github.com/dumbmatter/fakeIndexedDB/issues/22 we need to - // take care to sequence the DB open requests. - await symbolStore.closeDb().catch(() => {}); - symbolStore = new SymbolStore('profiler-async-storage', symbolProvider); - - await symbolStore.getSymbols( - [{ lib: lib, addresses: new Set([0x1]) }], - (_request, _results) => {}, - errorCallback - ); - - expect(symbolProvider.requestSymbolTableFromBrowser).toHaveBeenCalledTimes( - 1 - ); - expect(errorCallback).not.toHaveBeenCalled(); - }); - it('should call requestSymbolsFromServer first', async function () { const symbolTable = new Map([ [0, 'first symbol'], @@ -245,11 +173,18 @@ describe('SymbolStore', function () { new Error('requestSymbolsFromBrowser unsupported in this test') ) ), - requestSymbolTableFromBrowser: jest - .fn() - .mockResolvedValue(completeSymbolTableAsTuple), + requestSymbolsViaSymbolTableFromBrowser: jest.fn( + (request: LibSymbolicationRequest, _ignoreCache: boolean) => { + const results = readSymbolsFromSymbolTable( + request.addresses, + completeSymbolTableAsTuple, + (s: string) => s + ); + return Promise.resolve(results); + } + ), }; - symbolStore = new SymbolStore('profiler-async-storage', symbolProvider); + symbolStore = new SymbolStore(symbolProvider); const lib1 = { debugName: 'available-for-addresses', @@ -283,10 +218,10 @@ describe('SymbolStore', function () { expect(errorCallback).not.toHaveBeenCalled(); // requestSymbolsFromServer should have failed for lib2, so - // requestSymbolTableFromBrowser should have been called for it, once. - expect(symbolProvider.requestSymbolTableFromBrowser).toHaveBeenCalledTimes( - 1 - ); + // requestSymbolsViaSymbolTableFromBrowser should have been called for it, once. + expect( + symbolProvider.requestSymbolsViaSymbolTableFromBrowser + ).toHaveBeenCalledTimes(1); const lib1Symbols = ensureExists(symbolsPerLibrary.get(lib1)); const lib2Symbols = ensureExists(symbolsPerLibrary.get(lib2)); @@ -307,36 +242,6 @@ describe('SymbolStore', function () { name: 'last symbol', symbolAddress: 0x2000, }); - - // Using another symbol store simulates a page reload - // Due to https://github.com/dumbmatter/fakeIndexedDB/issues/22 we need to - // take care to sequence the DB open requests. - const symbolStore2 = new SymbolStore( - 'profiler-async-storage', - symbolProvider - ); - - await symbolStore2.getSymbols( - [ - { lib: lib1, addresses: new Set([0xf01, 0x1a50]) }, - { lib: lib2, addresses: new Set([0x33, 0x2000]) }, - ], - (_request, _results) => {}, - errorCallback - ); - - // The symbolStore should already have a cached symbol table for lib2 now, - // but the cache is only checked after the API requests, so both libraries - // are sent to requestSymbolsFromServer. - expect(symbolProvider.requestSymbolsFromServer).toHaveBeenCalledTimes(2); - expect(symbolsForAddressesRequestCount).toEqual(4); - expect(errorCallback).not.toHaveBeenCalled(); - - // requestSymbolsFromServer should have succeeded for lib1 and failed for lib2. - // lib2 is then found in the cache, so requestSymbolTableFromBrowser is not called again. - expect(symbolProvider.requestSymbolTableFromBrowser).toHaveBeenCalledTimes( - 1 - ); }); it('should should report the right errors', async function () { @@ -411,19 +316,28 @@ describe('SymbolStore', function () { new Error('requestSymbolsFromBrowser unsupported in this test') ) ), - requestSymbolTableFromBrowser: async ({ debugName, breakpadId }) => { + requestSymbolsViaSymbolTableFromBrowser: async ( + request, + _ignoreCache + ) => { + const { lib, addresses } = request; + const { debugName, breakpadId } = lib; expect(debugName).not.toEqual(''); expect(breakpadId).not.toEqual(''); if ( debugName === 'available-from-browser' || debugName === 'available-from-both-server-and-browser' ) { - return completeSymbolTableAsTuple; + return readSymbolsFromSymbolTable( + addresses, + completeSymbolTableAsTuple, + (s: string) => s + ); } throw new Error('The browser does not have symbols for this library.'); }, }; - symbolStore = new SymbolStore('profiler-async-storage', symbolProvider); + symbolStore = new SymbolStore(symbolProvider!); const addresses = new Set([0xf01, 0x1a50]); const succeededLibs = new Set(); diff --git a/src/test/unit/symbolicator-cli.test.ts b/src/test/unit/symbolicator-cli.test.ts index 50bac0c519..59138c332b 100644 --- a/src/test/unit/symbolicator-cli.test.ts +++ b/src/test/unit/symbolicator-cli.test.ts @@ -1,8 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { InMemorySymbolDB, makeOptionsFromArgv } from '../../symbolicator-cli'; -import { completeSymbolTableAsTuple } from '../fixtures/example-symbol-table'; +import { makeOptionsFromArgv } from '../../symbolicator-cli'; describe('makeOptionsFromArgv', function () { const commonArgs = ['/path/to/node', '/path/to/symbolicator-cli.js']; @@ -98,28 +97,3 @@ describe('makeOptionsFromArgv', function () { ).toThrow(); }); }); - -describe('InMemorySymbolDB', function () { - const debugName = 'debugName'; - const breakpadId = 'breakpadId'; - - it('should get a SymbolTable that was set', async function () { - const db = new InMemorySymbolDB(); - - await db.storeSymbolTable( - debugName, - breakpadId, - completeSymbolTableAsTuple - ); - - const table = await db.getSymbolTable(debugName, breakpadId); - expect(table).toEqual(completeSymbolTableAsTuple); - }); - - it('should throw when getting a SymbolTable that was not set', async function () { - const db = new InMemorySymbolDB(); - - const tableOrNull = await db.getSymbolTable(debugName, breakpadId); - await expect(tableOrNull).toBeNull(); - }); -}); From 6cae060db0c52ea77e63b293092dce0bdca397bf Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Fri, 9 Jan 2026 17:21:23 +0100 Subject: [PATCH 6/7] Remove unused ISymbolStoreDB interface. --- src/actions/receive-profile.ts | 12 +++--------- src/types/symbolication.ts | 31 ------------------------------- 2 files changed, 3 insertions(+), 40 deletions(-) diff --git a/src/actions/receive-profile.ts b/src/actions/receive-profile.ts index 474d86914e..2444aa731d 100644 --- a/src/actions/receive-profile.ts +++ b/src/actions/receive-profile.ts @@ -85,7 +85,6 @@ import type { TabID, PageList, MixedObject, - ISymbolStoreDB, } from 'firefox-profiler/types'; import type { @@ -602,11 +601,11 @@ class RegularSymbolProvider implements SymbolProvider { _dispatch: Dispatch; _symbolServerUrl: string; _browserConnection: BrowserConnection | null; - _symbolDb: ISymbolStoreDB; + _symbolDb: SymbolStoreDB; _demangleCallback: DemangleFunction | null = null; constructor( - dbNamePrefixOrDB: string | ISymbolStoreDB, + dbNamePrefix: string, dispatch: Dispatch, symbolServerUrl: string, browserConnection: BrowserConnection | null @@ -614,12 +613,7 @@ class RegularSymbolProvider implements SymbolProvider { this._dispatch = dispatch; this._symbolServerUrl = symbolServerUrl; this._browserConnection = browserConnection; - - if (typeof dbNamePrefixOrDB === 'string') { - this._symbolDb = new SymbolStoreDB(`${dbNamePrefixOrDB}-symbol-tables`); - } else { - this._symbolDb = dbNamePrefixOrDB; - } + this._symbolDb = new SymbolStoreDB(`${dbNamePrefix}-symbol-tables`); } async _makeSymbolicationAPIRequestWithCallback( diff --git a/src/types/symbolication.ts b/src/types/symbolication.ts index 4162e488e6..5c0bd5b8d1 100644 --- a/src/types/symbolication.ts +++ b/src/types/symbolication.ts @@ -2,37 +2,6 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -export interface ISymbolStoreDB { - /** - * Store the symbol table for a given library. - * @param {string} The debugName of the library. - * @param {string} The breakpadId of the library. - * @param {symbolTable} The symbol table, in SymbolTableAsTuple format. - * @return A promise that resolves (with nothing) once storage - * has succeeded. - */ - storeSymbolTable( - debugName: string, - breakpadId: string, - symbolTable: SymbolTableAsTuple - ): Promise; - - /** - * Retrieve the symbol table for the given library. - * @param {string} debugName The debugName of the library. - * @param {string} breakpadId The breakpadId of the library. - * @return A promise that resolves with the symbol table (in - * SymbolTableAsTuple format), with null if we couldn't - * find a symbol table for the requested library. - */ - getSymbolTable( - debugName: string, - breakpadId: string - ): Promise; - - close(): Promise; -} - export type SymbolTableAsTuple = [ Uint32Array, // addrs Uint32Array, // index From 3f43d129aefecc92498900f5ad1e0726d65b4a36 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Tue, 6 Jan 2026 10:34:26 +0100 Subject: [PATCH 7/7] Disable async chunks in the symbolicator-cli build. --- src/symbolicator-cli/webpack.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/symbolicator-cli/webpack.config.js b/src/symbolicator-cli/webpack.config.js index 6e43c9b4f8..944b25a84b 100644 --- a/src/symbolicator-cli/webpack.config.js +++ b/src/symbolicator-cli/webpack.config.js @@ -12,6 +12,7 @@ module.exports = { output: { path: path.resolve(projectRoot, 'dist'), filename: 'symbolicator-cli.js', + asyncChunks: false, }, entry: './src/symbolicator-cli/index.ts', module: {