diff --git a/locales/en-US/app.ftl b/locales/en-US/app.ftl index 9475a20d4c..c6ce3b67ad 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 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..1375a1b99d 100644 --- a/src/components/app/MenuButtons/Publish.tsx +++ b/src/components/app/MenuButtons/Publish.tsx @@ -16,6 +16,7 @@ import { getProfileRootRange, getHasPreferenceMarkers, getContainsPrivateBrowsingInformation, + getHasArgumentValues, } from 'firefox-profiler/selectors/profile'; import { getAbortFunction, @@ -58,6 +59,7 @@ type StateProps = { readonly rootRange: StartEndRange; readonly shouldShowPreferenceOption: boolean; readonly profileContainsPrivateBrowsingInformation: boolean; + readonly profileHasArgumentValues: boolean; readonly checkedSharingOptions: CheckedSharingOptions; readonly sanitizedProfileEncodingState: SanitizedProfileEncodingState; readonly downloadFileName: string; @@ -123,6 +125,7 @@ class PublishPanelImpl extends React.PureComponent { const { shouldShowPreferenceOption, profileContainsPrivateBrowsingInformation, + profileHasArgumentValues, sanitizedProfileEncodingState, downloadFileName, shouldSanitizeByDefault, @@ -210,6 +213,12 @@ class PublishPanelImpl extends React.PureComponent { ) : null} + {profileHasArgumentValues + ? 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), + profileHasArgumentValues: getHasArgumentValues(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 49a521c9f1..aa95549ebe 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. @@ -2263,14 +2265,46 @@ export function filterCounterSamplesToRange( number: samples.number ? samples.number.slice(beginSampleIndex, endSampleIndex) : undefined, - argumentValues: samples.argumentValues - ? samples.argumentValues.slice(beginSampleIndex, endSampleIndex) - : 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.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..1d9d10ce26 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 getHasArgumentValues: 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..24f05ca8fa 100644 --- a/src/selectors/publish.ts +++ b/src/selectors/publish.ts @@ -13,6 +13,7 @@ import { getLocalTracksByPid, getHasPreferenceMarkers, getContainsPrivateBrowsingInformation, + getHasArgumentValues, 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/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; }; 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"