From eeb50fa9ef68ef54bc85360def9e804ba8f19b77 Mon Sep 17 00:00:00 2001 From: Alex Thayer Date: Thu, 26 Feb 2026 12:26:50 -0800 Subject: [PATCH 1/2] Remove argumentValues from RawCounterSamplesTable I'm not actually sure why I initially put this in here - I think it was just overzealous flow type fixing that I then ported blindly to typescript when I had to update the patches. --- src/profile-logic/profile-data.ts | 5 +---- src/types/profile.ts | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/profile-logic/profile-data.ts b/src/profile-logic/profile-data.ts index 49a521c9f1..27537e11d5 100644 --- a/src/profile-logic/profile-data.ts +++ b/src/profile-logic/profile-data.ts @@ -2262,10 +2262,7 @@ export function filterCounterSamplesToRange( count: samples.count.slice(beginSampleIndex, endSampleIndex), number: samples.number ? samples.number.slice(beginSampleIndex, endSampleIndex) - : undefined, - argumentValues: samples.argumentValues - ? samples.argumentValues.slice(beginSampleIndex, endSampleIndex) - : undefined, + : undefined }; return newCounter; diff --git a/src/types/profile.ts b/src/types/profile.ts index 90486b7ecd..3333b4f90e 100644 --- a/src/types/profile.ts +++ b/src/types/profile.ts @@ -513,7 +513,6 @@ export type RawCounterSamplesTable = { number?: number[]; // The count of the data, for instance for memory this would be bytes. count: number[]; - argumentValues?: Array; length: number; }; From 47ee093f60a1a64aecc66637cc0b4d6341d3954f Mon Sep 17 00:00:00 2001 From: Alex Thayer Date: Thu, 26 Mar 2026 09:37:58 -0700 Subject: [PATCH 2/2] Allow exporting argument values in profiles This adds a checkbox which will allow the user to include argument values in exported profiles. The export also respects the checkbox to only export the specified timerange by reconstructing the values buffer to only include arguments referenced by included samples. --- locales/en-US/app.ftl | 1 + package.json | 2 +- src/components/app/MenuButtons/Publish.tsx | 10 ++++++ src/profile-logic/profile-data.ts | 42 +++++++++++++++++++++- src/profile-logic/sanitize.ts | 22 ++++++++++-- src/reducers/publish.ts | 3 ++ src/selectors/profile.ts | 7 ++++ src/selectors/publish.ts | 38 +++++++++++++++++--- src/test/store/publish.test.ts | 2 ++ src/test/unit/sanitize.test.ts | 4 +++ src/types/@types/devtools-reps/index.d.ts | 9 +++++ src/types/actions.ts | 1 + src/types/profile-derived.ts | 2 ++ src/utils/base64.ts | 18 ++++++++++ yarn.lock | 8 ++--- 15 files changed, 156 insertions(+), 13 deletions(-) diff --git a/locales/en-US/app.ftl b/locales/en-US/app.ftl index 9475a20d4c..60a3e4b951 100644 --- a/locales/en-US/app.ftl +++ b/locales/en-US/app.ftl @@ -724,6 +724,7 @@ MenuButtons--publish--renderCheckbox-label-preference = Include preference value MenuButtons--publish--renderCheckbox-label-private-browsing = Include the data from private browsing windows MenuButtons--publish--renderCheckbox-label-private-browsing-warning-image = .title = This profile contains private browsing data +MenuButtons--publish--renderCheckbox-label-argument-values = Include JavaScript execution tracing function argument values MenuButtons--publish--reupload-performance-profile = Re-upload Performance Profile MenuButtons--publish--share-performance-profile = Share Performance Profile MenuButtons--publish--info-description = Upload your profile and make it accessible to anyone with the link. diff --git a/package.json b/package.json index fd532010ed..e434916df8 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "common-tags": "^1.8.2", "copy-to-clipboard": "^3.3.3", "core-js": "^3.48.0", - "devtools-reps": "^0.27.4", + "devtools-reps": "^0.27.6", "escape-string-regexp": "^4.0.0", "gecko-profiler-demangle": "^0.4.0", "idb": "^8.0.3", diff --git a/src/components/app/MenuButtons/Publish.tsx b/src/components/app/MenuButtons/Publish.tsx index b464465a33..ea9eb3b255 100644 --- a/src/components/app/MenuButtons/Publish.tsx +++ b/src/components/app/MenuButtons/Publish.tsx @@ -16,6 +16,7 @@ import { getProfileRootRange, getHasPreferenceMarkers, getContainsPrivateBrowsingInformation, + getHasJSTracingArgumentValues, } from 'firefox-profiler/selectors/profile'; import { getAbortFunction, @@ -58,6 +59,7 @@ type StateProps = { readonly rootRange: StartEndRange; readonly shouldShowPreferenceOption: boolean; readonly profileContainsPrivateBrowsingInformation: boolean; + readonly profileHasJSTracingArgumentValues: boolean; readonly checkedSharingOptions: CheckedSharingOptions; readonly sanitizedProfileEncodingState: SanitizedProfileEncodingState; readonly downloadFileName: string; @@ -123,6 +125,7 @@ class PublishPanelImpl extends React.PureComponent { const { shouldShowPreferenceOption, profileContainsPrivateBrowsingInformation, + profileHasJSTracingArgumentValues, sanitizedProfileEncodingState, downloadFileName, shouldSanitizeByDefault, @@ -210,6 +213,12 @@ class PublishPanelImpl extends React.PureComponent { ) : null} + {profileHasJSTracingArgumentValues + ? this._renderCheckbox( + 'includeArgumentValues', + 'MenuButtons--publish--renderCheckbox-label-argument-values' + ) + : null} {sanitizedProfileEncodingState.phase === 'ERROR' ? (
@@ -361,6 +370,7 @@ export const PublishPanel = explicitConnect< shouldShowPreferenceOption: getHasPreferenceMarkers(state), profileContainsPrivateBrowsingInformation: getContainsPrivateBrowsingInformation(state), + profileHasJSTracingArgumentValues: getHasJSTracingArgumentValues(state), checkedSharingOptions: getCheckedSharingOptions(state), downloadFileName: getFilenameString(state), sanitizedProfileEncodingState: getSanitizedProfileEncodingState(state), diff --git a/src/profile-logic/profile-data.ts b/src/profile-logic/profile-data.ts index 27537e11d5..df2ac5619e 100644 --- a/src/profile-logic/profile-data.ts +++ b/src/profile-logic/profile-data.ts @@ -102,6 +102,8 @@ import type { } from 'firefox-profiler/types'; import { SelectedState, ResourceType } from 'firefox-profiler/types'; import type { CallNodeInfo, SuffixOrderIndex } from './call-node-info'; +import { bytesToBase64 } from 'firefox-profiler/utils/base64'; +import { ValueSummaryReader } from 'devtools-reps'; /** * Various helpers for dealing with the profile as a data structure. @@ -2262,12 +2264,50 @@ export function filterCounterSamplesToRange( count: samples.count.slice(beginSampleIndex, endSampleIndex), number: samples.number ? samples.number.slice(beginSampleIndex, endSampleIndex) - : undefined + : undefined, }; return newCounter; } +/** + * Filter a traced values buffer to only include entries that are referenced + * by the given argument values array. This is used during sanitization when + * filtering to a committed time range. + */ +export function filterTracedValuesBufferToEntries( + tracedValuesBuffer: ArrayBuffer, + thread: RawThread +): RawThread { + if ( + !thread.samples.argumentValues || + !thread.tracedValuesBuffer || + !thread.tracedObjectShapes + ) { + throw new Error( + 'filterTracedValuesBufferToEntries should only be called with JS Execution Tracer profiles' + ); + } + + const newThread: RawThread = { ...thread }; + const argumentValues: Array = [ + ...thread.samples.argumentValues, + ]; + + const filtered = ValueSummaryReader.filterValuesBufferToEntries( + tracedValuesBuffer, + argumentValues + ); + + newThread.tracedValuesBuffer = bytesToBase64(filtered.valuesBuffer); + newThread.samples = { + ...newThread.samples, + argumentValues: filtered.entryIndices, + }; + + return newThread; +} + /** * Process the samples in the counter. */ diff --git a/src/profile-logic/sanitize.ts b/src/profile-logic/sanitize.ts index dec2160f55..aa900aa7f0 100644 --- a/src/profile-logic/sanitize.ts +++ b/src/profile-logic/sanitize.ts @@ -22,6 +22,7 @@ import { getSchemaFromMarker } from './marker-schema'; import { filterRawThreadSamplesToRange, filterCounterSamplesToRange, + filterTracedValuesBufferToEntries, } from './profile-data'; import type { Profile, @@ -58,6 +59,7 @@ const PRIVATE_BROWSING_STACK = 1; export function sanitizePII( profile: Profile, derivedMarkerInfoForAllThreads: DerivedMarkerInfo[], + tracedValuesBuffers: Array, maybePIIToBeRemoved: RemoveProfileInformation | null, markerSchemaByName: MarkerSchemaByName ): SanitizeProfileResult { @@ -306,6 +308,7 @@ export function sanitizePII( thread, stringTable, derivedMarkerInfoForAllThreads[threadIndex], + tracedValuesBuffers[threadIndex], threadIndex, PIIToBeRemoved, windowIdFromPrivateBrowsing, @@ -420,6 +423,7 @@ function sanitizeThreadPII( thread: RawThread, stringTable: StringTable, derivedMarkerInfo: DerivedMarkerInfo, + tracedValuesBuffer: ArrayBuffer | undefined, threadIndex: number, PIIToBeRemoved: RemoveProfileInformation, windowIdFromPrivateBrowsing: Set, @@ -592,8 +596,22 @@ function sanitizeThreadPII( delete newThread['eTLD+1']; } - delete newThread.tracedValuesBuffer; - delete newThread.tracedObjectShapes; + if ( + newThread.samples.argumentValues && + tracedValuesBuffer && + newThread.tracedObjectShapes && + !PIIToBeRemoved.shouldRemoveArgumentValues + ) { + newThread = filterTracedValuesBufferToEntries( + tracedValuesBuffer, + newThread + ); + } else { + delete newThread.tracedValuesBuffer; + delete newThread.tracedObjectShapes; + newThread.samples = { ...newThread.samples }; + delete newThread.samples.argumentValues; + } const { samples } = newThread; if (stackFlags !== null && windowIdFromPrivateBrowsing.size > 0) { diff --git a/src/reducers/publish.ts b/src/reducers/publish.ts index d3c515ce9e..2d527341b2 100644 --- a/src/reducers/publish.ts +++ b/src/reducers/publish.ts @@ -25,6 +25,7 @@ function _getSanitizingSharingOptions(): CheckedSharingOptions { includeExtension: false, includePreferenceValues: false, includePrivateBrowsingData: false, + includeArgumentValues: false, }; } @@ -39,6 +40,8 @@ function _getMostlyNonSanitizingSharingOptions(): CheckedSharingOptions { includePreferenceValues: true, // We always want to sanitize the private browsing data by default includePrivateBrowsingData: false, + // We always want to sanitize the argument values by default since they may contain PII + includeArgumentValues: false, }; } diff --git a/src/selectors/profile.ts b/src/selectors/profile.ts index 7ac0ef4f2c..8eefbd3808 100644 --- a/src/selectors/profile.ts +++ b/src/selectors/profile.ts @@ -912,6 +912,13 @@ export const getContainsPrivateBrowsingInformation: Selector = return hasPrivateThreads; }); +// Gets whether this profile contains argument values from JS execution tracing. +export const getHasJSTracingArgumentValues: Selector = createSelector( + getThreads, + (threads) => + threads.some((thread) => thread.samples.argumentValues !== undefined) +); + /** * Returns the TIDs of the threads that are profiled. */ diff --git a/src/selectors/publish.ts b/src/selectors/publish.ts index 04397c601d..98b5fe6352 100644 --- a/src/selectors/publish.ts +++ b/src/selectors/publish.ts @@ -13,6 +13,7 @@ import { getLocalTracksByPid, getHasPreferenceMarkers, getContainsPrivateBrowsingInformation, + getHasJSTracingArgumentValues, getThreads, getMarkerSchemaByName, } from './profile'; @@ -80,6 +81,7 @@ export const getRemoveProfileInformation: Selector { let isIncludingEverything = true; for (const [prop, value] of Object.entries(checkedSharingOptions)) { - // Do not include preference values or private browsing checkboxes if - // they're hidden. Even though `includePreferenceValues` is not taken - // into account, it is false, if the profile updateChannel is not - // nightly or custom build. + // Do not include preference values, private browsing, or argument + // values checkboxes if they're hidden. Even though + // `includePreferenceValues` is not taken into account, it is false, if + // the profile updateChannel is not nightly or custom build. if (prop === 'includePreferenceValues' && !hasPreferenceMarkers) { continue; } @@ -106,6 +109,9 @@ export const getRemoveProfileInformation: Selector | null = null; +function getTracedValuesBuffersForAllThreads( + state: State +): Array { + const threads = getThreads(state); + if (_threadsForBuffers !== threads || _tracedValuesBuffers === null) { + _threadsForBuffers = threads; + _tracedValuesBuffers = threads.map((_: any, threadIndex: ThreadIndex) => + getThreadSelectors(threadIndex).getTracedValuesBuffer(state) + ); + } + return _tracedValuesBuffers; +} + /** * Run the profile sanitization step, and also get information about how any * UrlState needs to be updated, with things like mapping thread indexes, @@ -211,6 +238,7 @@ export const getSanitizedProfile: Selector = createSelector( getProfile, getDerivedMarkerInfoForAllThreads, + getTracedValuesBuffersForAllThreads, getRemoveProfileInformation, getMarkerSchemaByName, sanitizePII diff --git a/src/test/store/publish.test.ts b/src/test/store/publish.test.ts index 1b9f9f5c74..8d01d91b7f 100644 --- a/src/test/store/publish.test.ts +++ b/src/test/store/publish.test.ts @@ -73,6 +73,7 @@ describe('getCheckedSharingOptions', function () { describe('default filtering by channel', function () { const isFiltering = { includeExtension: false, + includeArgumentValues: false, includeFullTimeRange: false, includeHiddenThreads: false, includeAllTabs: false, @@ -83,6 +84,7 @@ describe('getCheckedSharingOptions', function () { }; const isNotFiltering = { includeExtension: true, + includeArgumentValues: false, includeFullTimeRange: true, includeHiddenThreads: true, includeAllTabs: true, diff --git a/src/test/unit/sanitize.test.ts b/src/test/unit/sanitize.test.ts index 70d43c9ba6..055c128a70 100644 --- a/src/test/unit/sanitize.test.ts +++ b/src/test/unit/sanitize.test.ts @@ -46,6 +46,7 @@ describe('sanitizePII', function () { shouldRemoveExtensions: false, shouldRemovePreferenceValues: false, shouldRemovePrivateBrowsingData: false, + shouldRemoveArgumentValues: false, }; const PIIToRemove: RemoveProfileInformation = { @@ -132,9 +133,12 @@ describe('sanitizePII', function () { }, }; + const tracedValuesBuffers = originalProfile.threads.map(() => undefined); + const sanitizedProfile = sanitizePII( originalProfile, derivedMarkerInfoForAllThreads, + tracedValuesBuffers, PIIToRemove, markerSchemaByName ).profile; diff --git a/src/types/@types/devtools-reps/index.d.ts b/src/types/@types/devtools-reps/index.d.ts index c7c11e7682..4d8861150f 100644 --- a/src/types/@types/devtools-reps/index.d.ts +++ b/src/types/@types/devtools-reps/index.d.ts @@ -18,11 +18,20 @@ declare module 'devtools-reps' { export function maybeEscapePropertyName(name: string): string; export function getGripPreviewItems(grip: any): any[]; + type FilterValuesBufferResult = { + valuesBuffer: ArrayBuffer; + entryIndices: Array; + }; + export const ValueSummaryReader: { getArgumentSummaries: ( valuesBuffer: ArrayBuffer, shapes: Array, valuesBufferIndex: number ) => Array | string; + filterValuesBufferToEntries: ( + srcBuffer: ArrayBuffer, + entryIndices: Array + ) => FilterValuesBufferResult; }; } diff --git a/src/types/actions.ts b/src/types/actions.ts index b3dccd0f99..878fbcccf9 100644 --- a/src/types/actions.ts +++ b/src/types/actions.ts @@ -151,6 +151,7 @@ export type CheckedSharingOptions = { includeExtension: boolean; includePreferenceValues: boolean; includePrivateBrowsingData: boolean; + includeArgumentValues: boolean; }; // This type is used when selecting tracks in the timeline. Ctrl and Meta are diff --git a/src/types/profile-derived.ts b/src/types/profile-derived.ts index f52454fd9e..164e1e8fae 100644 --- a/src/types/profile-derived.ts +++ b/src/types/profile-derived.ts @@ -676,6 +676,8 @@ export type RemoveProfileInformation = { readonly shouldRemovePreferenceValues: boolean; // Remove the private browsing data if it's true. readonly shouldRemovePrivateBrowsingData: boolean; + // Remove the argument values captured by the JS execution tracer if it's true. + readonly shouldRemoveArgumentValues: boolean; }; /** diff --git a/src/utils/base64.ts b/src/utils/base64.ts index 4dbff80a3a..0948a9a25c 100644 --- a/src/utils/base64.ts +++ b/src/utils/base64.ts @@ -35,6 +35,15 @@ function base64StringToBytesFallback(base64: string): ArrayBuffer { return bytes.buffer; } +function bytesToBase64Fallback(bytes: ArrayBuffer): string { + let byteStr = ''; + const u8array = new Uint8Array(bytes); + for (let i = 0; i < u8array.byteLength; i++) { + byteStr += String.fromCharCode(u8array[i]); + } + return btoa(byteStr); +} + export function base64StringToBytes(base64: string): ArrayBuffer { if ('fromBase64' in Uint8Array) { // @ts-expect-error Uint8Array.fromBase64 is a relatively new API @@ -43,3 +52,12 @@ export function base64StringToBytes(base64: string): ArrayBuffer { return base64StringToBytesFallback(base64); } + +export function bytesToBase64(bytes: ArrayBuffer): string { + if ('toBase64' in Uint8Array) { + // @ts-expect-error Uint8Array.toBase64 is a relatively new API + return new Uint8Array(bytes).toBase64(); + } + + return bytesToBase64Fallback(bytes); +} diff --git a/yarn.lock b/yarn.lock index ba33b7dd6c..eaef604a32 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4195,10 +4195,10 @@ devtools-license-check@^0.9.0: dependencies: license-checker "^9.0.3" -devtools-reps@^0.27.4: - version "0.27.4" - resolved "https://registry.yarnpkg.com/devtools-reps/-/devtools-reps-0.27.4.tgz#71cd2e595a1fd51164b18e2bbf15d6f83e747d8b" - integrity sha512-YQJy8Quz6H3BNcUYgmjPWYX736owTIU6MKE/k1WuArPtzkyWbP9EEQFgir48GSqc/w5QGuDDmBF4LhM9AS7mcg== +devtools-reps@^0.27.6: + version "0.27.6" + resolved "https://registry.yarnpkg.com/devtools-reps/-/devtools-reps-0.27.6.tgz#26aa682cfd058e171064bc86da6590e80785933f" + integrity sha512-ukQco/6e3nmTOk/qDnW7VuMga6XAlshJ/aBsCfq6ZTmaqJG2YybK7STV6oEiy2vi1U/YGQpg46mhPNx4fKytzw== dependencies: prop-types "^15.7.2" react "^16.8.6"