diff --git a/src/profile-logic/marker-data.ts b/src/profile-logic/marker-data.ts index 6861df3d86..5c196abd14 100644 --- a/src/profile-logic/marker-data.ts +++ b/src/profile-logic/marker-data.ts @@ -23,6 +23,7 @@ import { } from 'firefox-profiler/app-logic/constants'; import { getSchemaFromMarker, + isStringIndexMarkerField, markerPayloadMatchesSearch, markerSchemaFrontEndOnly, } from './marker-schema'; @@ -1468,15 +1469,56 @@ export function removePrefMarkerPreferenceValues( } /** - * Sanitize Text marker's name property for potential URLs. + * Apply a transformation to a Text marker's text. The schema tells us whether the + * payload holds the text inline or as a string table index. In the latter case the + * result is interned as a new string, as other markers and frames may share that + * entry. + */ +function _updateTextMarkerText( + payload: TextMarkerPayload, + stringIndexMarkerFieldsByDataType: Map, + stringTable: StringTable, + transform: (text: string) => string +): TextMarkerPayload { + // The casts below follow the storage layout the schema declares, which + // TypeScript can't verify from the payload type alone. + if ( + !isStringIndexMarkerField( + stringIndexMarkerFieldsByDataType, + payload.type, + 'name' + ) + ) { + return { ...payload, name: transform(payload.name as string) }; + } + + const nameIndex = payload.name as IndexIntoStringTable; + if (!stringTable.hasIndex(nameIndex)) { + return payload; + } + const text = stringTable.getString(nameIndex); + const newText = transform(text); + if (newText === text) { + return payload; + } + return { ...payload, name: stringTable.indexForString(newText) }; +} + +/** + * Sanitize Text marker's name property for potential URLs. Only for payloads + * holding their text inline, as the string table is sanitized as a whole. */ export function sanitizeTextMarker( - payload: TextMarkerPayload + payload: TextMarkerPayload, + stringIndexMarkerFieldsByDataType: Map, + stringTable: StringTable ): TextMarkerPayload { - return { - ...payload, - name: removeURLs(payload.name), - }; + return _updateTextMarkerText( + payload, + stringIndexMarkerFieldsByDataType, + stringTable, + removeURLs + ); } /** @@ -1484,20 +1526,26 @@ export function sanitizeTextMarker( */ export function sanitizeExtensionTextMarker( markerName: string, - payload: TextMarkerPayload + payload: TextMarkerPayload, + stringIndexMarkerFieldsByDataType: Map, + stringTable: StringTable ): TextMarkerPayload { if (['ExtensionParent', 'ExtensionChild'].includes(markerName)) { - return { - ...payload, - name: payload.name.replace(/^.*, (api_(call|event): )/, '$1'), - }; + return _updateTextMarkerText( + payload, + stringIndexMarkerFieldsByDataType, + stringTable, + (text) => text.replace(/^.*, (api_(call|event): )/, '$1') + ); } if (markerName === 'Extension Suspend') { - return { - ...payload, - name: payload.name.replace(/ by .*$/, ''), - }; + return _updateTextMarkerText( + payload, + stringIndexMarkerFieldsByDataType, + stringTable, + (text) => text.replace(/ by .*$/, '') + ); } return payload; @@ -1731,6 +1779,31 @@ export function formatLogTimestamp(absoluteMs: number): string { ); } +/** + * Resolve a new format Log marker's message. The schema tells us whether the + * payload holds the message inline or as an index into the string table, which + * is what newer profiles do. + */ +export function resolveLogMarkerMessage( + message: string | IndexIntoStringTable, + stringArray: string[], + stringIndexMarkerFieldsByDataType: Map +): string { + // The casts below follow the storage layout the schema declares, which + // TypeScript can't verify from the payload type alone. + if ( + !isStringIndexMarkerField( + stringIndexMarkerFieldsByDataType, + 'Log', + 'message' + ) + ) { + return message as string; + } + + return stringArray[message as IndexIntoStringTable] ?? ''; +} + /** * Format a Log marker payload into a MOZ_LOG canonical line. * @@ -1751,15 +1824,21 @@ export function formatLogStatement( threadName: string, data: LogMarkerPayload, moduleName: string, - stringArray: string[] + stringArray: string[], + stringIndexMarkerFieldsByDataType: Map ): string | null { if ('message' in data) { - if (!data.message) { + const message = resolveLogMarkerMessage( + data.message, + stringArray, + stringIndexMarkerFieldsByDataType + ); + if (!message) { return null; } const levelStr = stringArray[data.level] ?? ''; const levelLetter = LOG_LEVEL_STRING_TO_LETTER[levelStr] ?? 'D'; - return `${timestampStr} - [${processName} ${pid}: ${threadName}]: ${levelLetter}/${moduleName} ${data.message.trim()}`; + return `${timestampStr} - [${processName} ${pid}: ${threadName}]: ${levelLetter}/${moduleName} ${message.trim()}`; } if (!data.name) { return null; diff --git a/src/profile-logic/marker-schema.ts b/src/profile-logic/marker-schema.ts index 59454a280d..bb56860d7d 100644 --- a/src/profile-logic/marker-schema.ts +++ b/src/profile-logic/marker-schema.ts @@ -642,11 +642,7 @@ export function markerPayloadMatchesSearch( continue; } - if ( - payloadField.format === 'unique-string' || - payloadField.format === 'flow-id' || - payloadField.format === 'terminating-flow-id' - ) { + if (isStringIndexFormat(payloadField.format)) { if (typeof value !== 'number') { console.warn( `In marker ${marker.name}, the key ${payloadField.key} has an invalid value "${value}" as a unique string, it isn't a number.` @@ -671,6 +667,19 @@ export function markerPayloadMatchesSearch( return false; } +/** + * Whether this field format means the payload holds a string table index. + */ +export function isStringIndexFormat( + format: MarkerFormatType | undefined +): boolean { + return ( + format === 'unique-string' || + format === 'flow-id' || + format === 'terminating-flow-id' + ); +} + /** * Returns a map of marker schema name -> array of field keys, listing any fields * that contain indexes into the string table. If a marker schema has no such @@ -689,12 +698,7 @@ export function computeStringIndexMarkerFieldsByDataType( const { name, fields } = schema; const stringIndexFields = []; for (const field of fields) { - if ( - (field.format === 'unique-string' || - field.format === 'flow-id' || - field.format === 'terminating-flow-id') && - field.key - ) { + if (isStringIndexFormat(field.format) && field.key) { stringIndexFields.push(field.key); } } @@ -704,3 +708,18 @@ export function computeStringIndexMarkerFieldsByDataType( } return stringIndexMarkerFieldsByDataType; } + +/** + * Whether the schema of this marker data type declares the given payload field + * as a string table index. Takes the map computed by + * `computeStringIndexMarkerFieldsByDataType`. + */ +export function isStringIndexMarkerField( + stringIndexMarkerFieldsByDataType: Map, + dataType: string, + fieldKey: string +): boolean { + return ( + stringIndexMarkerFieldsByDataType.get(dataType)?.includes(fieldKey) ?? false + ); +} diff --git a/src/profile-logic/sanitize.ts b/src/profile-logic/sanitize.ts index d03304c11b..9bba0aedc4 100644 --- a/src/profile-logic/sanitize.ts +++ b/src/profile-logic/sanitize.ts @@ -18,7 +18,11 @@ import { sanitizeTextMarker, sanitizeFromMarkerSchema, } from './marker-data'; -import { getSchemaFromMarker } from './marker-schema'; +import { + computeStringIndexMarkerFieldsByDataType, + getSchemaFromMarker, + isStringIndexMarkerField, +} from './marker-schema'; import { filterRawThreadSamplesToRange, filterCounterSamplesToRange, @@ -141,6 +145,11 @@ export function sanitizePII( stringArray, }; + // Precompute the payload fields that hold string table indexes, so that the + // marker loop below doesn't have to walk the schema fields for every marker. + const stringIndexMarkerFieldsByDataType = + computeStringIndexMarkerFieldsByDataType(Object.values(markerSchemaByName)); + let stackFlags: Uint8Array | null = null; if (windowIdFromPrivateBrowsing.size > 0) { @@ -326,6 +335,7 @@ export function sanitizePII( PIIToBeRemoved, windowIdFromPrivateBrowsing, markerSchemaByName, + stringIndexMarkerFieldsByDataType, stackFlags ); @@ -441,6 +451,7 @@ function sanitizeThreadPII( PIIToBeRemoved: RemoveProfileInformation, windowIdFromPrivateBrowsing: Set, markerSchemaByName: MarkerSchemaByName, + stringIndexMarkerFieldsByDataType: Map, stackFlags: Uint8Array | null ): RawThread | null { if (PIIToBeRemoved.shouldRemoveThreads.has(threadIndex)) { @@ -509,9 +520,21 @@ function sanitizeThreadPII( markerTable.name[i] = stringTable.indexForString(sanitizedRequestStr); } - if (currentMarker.type === 'Text') { + if ( + currentMarker.type === 'Text' && + !isStringIndexMarkerField( + stringIndexMarkerFieldsByDataType, + 'Text', + 'name' + ) + ) { // Sanitize all the name fields of text markers in case they contain URLs. - markerTable.data[i] = sanitizeTextMarker(currentMarker); + // Newer profiles hold the text in the string table, sanitized above. + markerTable.data[i] = sanitizeTextMarker( + currentMarker, + stringIndexMarkerFieldsByDataType, + stringTable + ); // Re-assign the value of currentMarker as the marker may be // sanitized again to remove extension ids. currentMarker = markerTable.data[i]; @@ -524,10 +547,13 @@ function sanitizeThreadPII( currentMarker.type === 'Text' ) { const markerName = stringTable.getString(markerTable.name[i]); - // Sanitize extension ids out of known extension markers. + // Sanitize extension ids out of known extension markers. Unlike URLs, + // these aren't removed from the string table as a whole. markerTable.data[i] = sanitizeExtensionTextMarker( markerName, - currentMarker + currentMarker, + stringIndexMarkerFieldsByDataType, + stringTable ); } diff --git a/src/profile-query/formatters/marker-info.ts b/src/profile-query/formatters/marker-info.ts index ce1c2614fa..a28b166313 100644 --- a/src/profile-query/formatters/marker-info.ts +++ b/src/profile-query/formatters/marker-info.ts @@ -9,6 +9,7 @@ import { import { getProfile, getCategories, + getMarkerSchema, getMarkerSchemaByName, getStringTable, getCommittedRange, @@ -23,6 +24,7 @@ import { } from '../network-summary'; import { getThreadSelectors } from 'firefox-profiler/selectors/per-thread'; import { + computeStringIndexMarkerFieldsByDataType, formatFromMarkerSchema, getLabelGetter, } from 'firefox-profiler/profile-logic/marker-schema'; @@ -61,6 +63,7 @@ import { LOG_LETTER_TO_LEVEL, formatLogTimestamp, formatLogStatement, + resolveLogMarkerMessage, } from 'firefox-profiler/profile-logic/marker-data'; import { formatFunctionNameWithLibrary } from '../function-list'; import type { @@ -1369,6 +1372,9 @@ export function collectProfileLogs( const profile = getProfile(state); const profileStartTime = profile.meta.startTime; const stringArray = profile.shared.stringArray; + // The schema tells us which payload fields hold string table indexes. + const stringIndexMarkerFieldsByDataType = + computeStringIndexMarkerFieldsByDataType(getMarkerSchema(state)); // Resolve which thread indexes to include. const threadIndexes: Set | null = @@ -1423,13 +1429,18 @@ export function collectProfileLogs( let levelLetter: string; if ('message' in logData) { - if (!logData.message) { + const rawMessage = resolveLogMarkerMessage( + logData.message, + stringArray, + stringIndexMarkerFieldsByDataType + ); + if (!rawMessage) { continue; } moduleName = stringArray[markers.name[i]] ?? ''; const levelStr = stringArray[logData.level] ?? ''; levelLetter = LOG_LEVEL_STRING_TO_LETTER[levelStr] ?? 'D'; - message = logData.message.trim(); + message = rawMessage.trim(); } else { if (!logData.name) { continue; @@ -1473,7 +1484,8 @@ export function collectProfileLogs( threadName, logData, moduleName, - stringArray + stringArray, + stringIndexMarkerFieldsByDataType ); if (formatted !== null) { entries.push(formatted); diff --git a/src/test/fixtures/profiles/marker-schema.ts b/src/test/fixtures/profiles/marker-schema.ts index d5f2f61bfa..00dc94843b 100644 --- a/src/test/fixtures/profiles/marker-schema.ts +++ b/src/test/fixtures/profiles/marker-schema.ts @@ -114,8 +114,10 @@ export const markerSchemaForTests: MarkerSchema[] = [ fields: [ { key: 'module', label: 'Module', format: 'string' }, { key: 'name', label: 'Name', format: 'string' }, - // New format: level is a string table index ("Error"/"Warning"/"Info"/"Debug"/"Verbose"). + // New format: level ("Error"/"Warning"/"Info"/"Debug"/"Verbose") and + // message are string table indexes. { key: 'level', label: 'Level', format: 'unique-string' }, + { key: 'message', label: 'Message', format: 'unique-string' }, ], }, { diff --git a/src/test/store/__snapshots__/profile-view.test.ts.snap b/src/test/store/__snapshots__/profile-view.test.ts.snap index 50a5655d83..a088d078fb 100644 --- a/src/test/store/__snapshots__/profile-view.test.ts.snap +++ b/src/test/store/__snapshots__/profile-view.test.ts.snap @@ -283,6 +283,11 @@ Object { "key": "level", "label": "Level", }, + Object { + "format": "unique-string", + "key": "message", + "label": "Message", + }, ], "name": "Log", "tableLabel": "({marker.data.module}) {marker.data.name}", diff --git a/src/test/unit/marker-data.test.ts b/src/test/unit/marker-data.test.ts index 80c8c41ab0..9a5d06beff 100644 --- a/src/test/unit/marker-data.test.ts +++ b/src/test/unit/marker-data.test.ts @@ -15,7 +15,9 @@ import { processGeckoProfile } from '../../profile-logic/process-profile'; import { filterRawMarkerTableToRange, filterRawMarkerTableToRangeWithMarkersToDelete, + formatLogStatement, } from '../../profile-logic/marker-data'; +import { computeStringIndexMarkerFieldsByDataType } from '../../profile-logic/marker-schema'; import { createGeckoProfile, @@ -37,6 +39,9 @@ import { getEmptySharedData } from '../../profile-logic/data-structures'; import type { IndexIntoRawMarkerTable, + LogMarkerPayload, + MarkerFormatType, + MarkerSchema, Milliseconds, NetworkPayload, ScreenshotPayload, @@ -1392,3 +1397,65 @@ describe('filterRawMarkerTableToRangeWithMarkersToDelete', () => { expect(markerNames).toEqual(['A', 'B', 'E', 'G']); }); }); + +describe('formatLogStatement', function () { + const timestamp = '1970-01-01 00:00:00.170000000 UTC'; + const stringArray = ['', 'Error', 'the log message\n']; + + // The schema decides whether the payload holds the message inline or as an + // index into the string table. + function logSchema(messageFormat: MarkerFormatType): MarkerSchema { + return { + name: 'Log', + display: ['marker-chart', 'marker-table'], + fields: [ + { key: 'level', label: 'Level', format: 'unique-string' }, + { key: 'message', label: 'Message', format: messageFormat }, + ], + }; + } + + function format(data: LogMarkerPayload, messageFormat: MarkerFormatType) { + return formatLogStatement( + timestamp, + 'GeckoMain', + 1234, + 'GeckoMain', + data, + 'nsHttp', + stringArray, + computeStringIndexMarkerFieldsByDataType([logSchema(messageFormat)]) + ); + } + + it('formats a message held as an index into the string table', function () { + expect(format({ type: 'Log', level: 1, message: 2 }, 'unique-string')).toBe( + `${timestamp} - [GeckoMain 1234: GeckoMain]: E/nsHttp the log message` + ); + }); + + it('formats a message held inline, as older profiles do', function () { + expect( + format({ type: 'Log', level: 1, message: 'the log message\n' }, 'string') + ).toBe( + `${timestamp} - [GeckoMain 1234: GeckoMain]: E/nsHttp the log message` + ); + }); + + it('skips a marker whose message index resolves to an empty string', function () { + expect(format({ type: 'Log', level: 1, message: 0 }, 'unique-string')).toBe( + null + ); + }); + + it('formats a legacy message held in the name field', function () { + expect( + format( + { type: 'Log', module: 'D/nsHttp', name: 'the log message\n' }, + 'unique-string' + ) + ).toBe( + `${timestamp} - [GeckoMain 1234: GeckoMain]: D/nsHttp the log message` + ); + }); +}); diff --git a/src/test/unit/profile-query/marker-utils.test.ts b/src/test/unit/profile-query/marker-utils.test.ts index 763cb817ee..19e2acb5a2 100644 --- a/src/test/unit/profile-query/marker-utils.test.ts +++ b/src/test/unit/profile-query/marker-utils.test.ts @@ -7,6 +7,7 @@ import { computeRateStats, collectMarkerInfo, collectMarkerStack, + collectProfileLogs, collectThreadMarkers, collectThreadNetwork, } from 'firefox-profiler/profile-query/formatters/marker-info'; @@ -1237,3 +1238,32 @@ describe('collectThreadNetwork', function () { expect(result.requests[0].markerHandle).toBe('m-1'); }); }); + +describe('collectProfileLogs', function () { + it('filters on the text of a message held as a string table index', function () { + // The fixtures intern `level` and `message`, so these payloads hold string + // table indexes like the ones Firefox emits now. The search filter runs + // against the message, so it has to be resolved first. + const { store, threadMap } = setupWithMarkers([ + [ + 'nsHttp', + 170, + null, + { type: 'Log', level: 'Error', message: 'ParentChannelListener' }, + ], + [ + 'nsJarProtocol', + 190, + null, + { type: 'Log', level: 'Debug', message: 'nsJARChannel::nsJARChannel' }, + ], + ]); + + const { entries } = collectProfileLogs(store, threadMap, { + search: 'nsJARChannel', + }); + expect(entries).toEqual([ + '1970-01-01 00:00:00.190000000 UTC - [Unknown Process 0: Empty]: D/nsJarProtocol nsJARChannel::nsJARChannel', + ]); + }); +}); diff --git a/src/test/unit/sanitize.test.ts b/src/test/unit/sanitize.test.ts index b7937b9b46..8ab505fbcc 100644 --- a/src/test/unit/sanitize.test.ts +++ b/src/test/unit/sanitize.test.ts @@ -31,6 +31,7 @@ import { bytesToBase64, } from 'firefox-profiler/utils/base64'; import { ValueSummaryReader } from 'devtools-reps'; +import { StringTable } from 'firefox-profiler/utils/string-table'; import type { MarkerSchemaByName, RawThread, @@ -40,7 +41,8 @@ import type { describe('sanitizePII', function () { function setup( piiConfig: Partial, - originalProfile = processGeckoProfile(createGeckoProfile()) + originalProfile = processGeckoProfile(createGeckoProfile()), + extraMarkerSchemas: MarkerSchemaByName = {} ) { const defaultsPii: RemoveProfileInformation = { shouldRemoveThreads: new Set(), @@ -136,6 +138,7 @@ describe('sanitizePII', function () { }, ], }, + ...extraMarkerSchemas, }; // Mirror what the `getTracedValuesBuffer` selector hands to `sanitizePII` @@ -160,6 +163,136 @@ describe('sanitizePII', function () { }; } + // Mirrors what Firefox emits now: the schema declares `name` as a unique + // string, so the payload holds a string table index. + const uniqueStringTextSchema: MarkerSchemaByName = { + Text: { + name: 'Text', + tableLabel: '{marker.name} — {marker.data.name}', + display: ['marker-chart', 'marker-table'], + fields: [{ key: 'name', label: 'Details', format: 'unique-string' }], + }, + }; + + function setupWithUniqueStringTextMarkers( + piiConfig: Partial, + markers: Array<[string, string]> + ) { + const profile = getProfileWithMarkers( + markers.map(([markerName, text]) => [ + markerName, + 0, + 1, + { type: 'Text', name: text }, + ]) + ); + // The fixtures use their own schema, where Text holds its text inline, so + // both the schema and the payloads are replaced here. + profile.meta.markerSchema = [uniqueStringTextSchema.Text]; + const stringTable = StringTable.withBackingArray( + profile.shared.stringArray + ); + profile.threads[0].markers.data = markers.map(([, text]) => ({ + type: 'Text', + name: stringTable.indexForString(text), + })); + + const { sanitizedProfile } = setup( + piiConfig, + profile, + uniqueStringTextSchema + ); + const { stringArray } = sanitizedProfile.shared; + return sanitizedProfile.threads[0].markers.data.map((data) => { + if (!data || data.type !== 'Text') { + throw new Error('Expected a Text marker'); + } + if (typeof data.name !== 'number') { + throw new Error( + 'Expected the text to be an index into the string table' + ); + } + return stringArray[data.name]; + }); + } + + it('should sanitize the URLs inside text markers holding a unique string', function () { + // URLs are removed from the whole string table, so nothing else is needed. + expect( + setupWithUniqueStringTextMarkers({ shouldRemoveUrls: true }, [ + [ + 'Extension Suspend', + 'onBeforeRequest https://profiler.firefox.com/ by extension', + ], + ]) + ).toEqual(['onBeforeRequest https:// by extension']); + }); + + it('should sanitize extension ids inside text markers holding a unique string', function () { + expect( + setupWithUniqueStringTextMarkers({ shouldRemoveExtensions: true }, [ + [ + 'ExtensionParent', + 'formautofill@mozilla.org, api_call: runtime.onUpdateAvailable.addListener', + ], + [ + 'ExtensionChild', + 'formautofill@mozilla.org, api_call: runtime.onUpdateAvailable.addListener', + ], + ]) + ).toEqual([ + 'api_call: runtime.onUpdateAvailable.addListener', + 'api_call: runtime.onUpdateAvailable.addListener', + ]); + }); + + it('should sanitize both URLs and extension ids inside text markers holding a unique string', function () { + expect( + setupWithUniqueStringTextMarkers( + { shouldRemoveUrls: true, shouldRemoveExtensions: true }, + [ + [ + 'Extension Suspend', + 'onBeforeRequest https://profiler.firefox.com/ by extension', + ], + ] + ) + ).toEqual(['onBeforeRequest https://']); + }); + + it('should not alter other strings when sanitizing a shared text marker string', function () { + // The string table is shared, so the sanitized text has to be interned as a + // new string instead of replacing an entry others may point to. + const text = + 'formautofill@mozilla.org, api_call: runtime.onUpdateAvailable.addListener'; + const profile = getProfileWithMarkers([ + ['ExtensionParent', 0, 1, { type: 'Text', name: text }], + ['SomeOtherMarker', 0, 1, { type: 'Text', name: text }], + ]); + profile.meta.markerSchema = [uniqueStringTextSchema.Text]; + const stringTable = StringTable.withBackingArray( + profile.shared.stringArray + ); + const textIndex = stringTable.indexForString(text); + profile.threads[0].markers.data = [ + { type: 'Text', name: textIndex }, + { type: 'Text', name: textIndex }, + ]; + + const { sanitizedProfile } = setup( + { shouldRemoveExtensions: true }, + profile, + uniqueStringTextSchema + ); + + const { stringArray } = sanitizedProfile.shared; + const [sanitized, untouched] = sanitizedProfile.threads[0].markers.data.map( + (data) => stringArray[(data as any).name] + ); + expect(sanitized).toBe('api_call: runtime.onUpdateAvailable.addListener'); + expect(untouched).toBe(text); + }); + it('should sanitize the threads if they are provided', function () { const { originalProfile, sanitizedProfile } = setup({ shouldRemoveThreads: new Set([0, 2]), diff --git a/src/types/markers.ts b/src/types/markers.ts index e3eb9217b5..25cac35a6a 100644 --- a/src/types/markers.ts +++ b/src/types/markers.ts @@ -624,7 +624,8 @@ export type UserTimingMarkerPayload = { export type TextMarkerPayload = { type: 'Text'; - name: string; + // A string table index in newer profiles, the text itself in older ones. + name: string | IndexIntoStringTable; cause?: CauseBacktrace; innerWindowID?: number; }; @@ -658,7 +659,8 @@ export type LogMarkerPayload = type: 'Log'; // String table index resolving to "Error", "Warning", "Info", "Debug", or "Verbose". level: number; - message: string; + // A string table index in newer profiles, the message itself in older ones. + message: string | IndexIntoStringTable; color?: string; }; diff --git a/src/utils/window-console.ts b/src/utils/window-console.ts index e88a025c4f..ac749ab519 100644 --- a/src/utils/window-console.ts +++ b/src/utils/window-console.ts @@ -28,6 +28,7 @@ import { formatLogTimestamp, formatLogStatement, } from 'firefox-profiler/profile-logic/marker-data'; +import { computeStringIndexMarkerFieldsByDataType } from 'firefox-profiler/profile-logic/marker-schema'; // Despite providing a good libdef for Object.defineProperty, Flow still // special-cases the `value` property: if it's missing it throws an error. Using @@ -287,6 +288,11 @@ export function addDataToWindowObject( const profile = selectorsForConsole.profile.getProfile(getState()); const range = selectorsForConsole.profile.getPreviewSelectionRange(getState()); + // The schema tells us which payload fields hold string table indexes. + const stringIndexMarkerFieldsByDataType = + computeStringIndexMarkerFieldsByDataType( + selectorsForConsole.profile.getMarkerSchema(getState()) + ); for (const thread of profile.threads) { const { markers } = thread; @@ -315,7 +321,8 @@ export function addDataToWindowObject( thread.name, data, moduleName, - stringArray + stringArray, + stringIndexMarkerFieldsByDataType ); if (statement !== null) { logs.push(statement);