Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions locales/en-US/app.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,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-js-sources = Include JavaScript source code
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.
Expand Down
94 changes: 93 additions & 1 deletion src/actions/receive-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import type {
BrowserConnectionStatus,
} from '../app-logic/browser-connection';
import type { LibSymbolicationRequest } from '../profile-logic/symbol-store';
import { getJSSourcesViaWebChannel } from '../app-logic/web-channel';

/**
* This file collects all the actions that are used for receiving the profile in the
Expand Down Expand Up @@ -212,6 +213,15 @@ export function finalizeProfileView(
);
}

// Download all JS sources from the browser and store them in the profile
let jsSourcesPromise = null;
if (browserConnection) {
jsSourcesPromise = downloadJSSourcesFromBrowser(
profile,
browserConnection
);
}

// Note we kick off symbolication only for the profiles we know for sure
// that they weren't symbolicated.
// We can skip the symbolication in tests if needed.
Expand All @@ -233,7 +243,11 @@ export function finalizeProfileView(
}
}

await Promise.all([faviconsPromise, symbolicationPromise]);
await Promise.all([
faviconsPromise,
jsSourcesPromise,
symbolicationPromise,
]);
};
}

Expand Down Expand Up @@ -741,6 +755,84 @@ export async function retrievePageFaviconsFromBrowser(
});
}

/**
* Download all JavaScript sources from the browser and store them in the profile's
* string table. This is called during profile loading to pre-fetch all JS sources.
*/
export async function downloadJSSourcesFromBrowser(
profile: Profile,
_browserConnection: BrowserConnection
): Promise<void> {
const { shared } = profile;
const { sources } = shared;

// Collect all unique source UUIDs that don't already have source code
const uuidsToFetch: string[] = [];
const sourceIndexByUuid: Map<string, number> = new Map();

for (let i = 0; i < sources.length; i++) {
const uuid = sources.uuid[i];
const sourceCode = sources.sourceCode[i];

// Only fetch if we have a UUID and don't already have the source code
if (uuid !== null && sourceCode === null) {
uuidsToFetch.push(uuid);
sourceIndexByUuid.set(uuid, i);
}
}

if (uuidsToFetch.length === 0) {
// No sources to fetch
console.log('No JS sources to download from browser');
return;
}

console.log(
`Starting to download ${uuidsToFetch.length} JS source(s) from browser...`
);

try {
// Fetch all JS sources at once using the WebChannel
const results = await getJSSourcesViaWebChannel(uuidsToFetch);

// Store the fetched sources in the string table
let successCount = 0;
let errorCount = 0;

for (let i = 0; i < results.length; i++) {
const result = results[i];
const uuid = uuidsToFetch[i];
const sourceIndex = sourceIndexByUuid.get(uuid);

if (sourceIndex === undefined) {
continue;
}

if ('error' in result) {
// Log the error but continue with other sources
console.warn(
`Failed to fetch JS source for UUID ${uuid}: ${result.error}`
);
errorCount++;
continue;
}

// Add the source code to the string table and update the sources table
const sourceCodeIndex = shared.stringArray.length;
shared.stringArray.push(result.sourceText);
sources.sourceCode[sourceIndex] = sourceCodeIndex;
successCount++;
}

console.log(
`Finished downloading JS sources: ${successCount} succeeded, ${errorCount} failed`
);
} catch (error) {
// Log the error but don't fail the entire profile load
console.error('Failed to download JS sources from browser:', error);
}
}

// From a BrowserConnectionStatus, this unwraps the included browserConnection
// when possible.
export function unwrapBrowserConnection(
Expand Down
4 changes: 4 additions & 0 deletions src/components/app/MenuButtons/Publish.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ class PublishPanelImpl extends React.PureComponent<PublishProps, {}> {
</Localized>
)
: null}
{this._renderCheckbox(
'includeJSSources',
'MenuButtons--publish--renderCheckbox-label-js-sources'
)}
</div>
{sanitizedProfileEncodingState.phase === 'ERROR' ? (
<div className="photon-message-bar photon-message-bar-error photon-message-bar-inner-content">
Expand Down
4 changes: 3 additions & 1 deletion src/components/app/SourceCodeFetcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,9 @@ class SourceCodeFetcherImpl extends React.PureComponent<Props> {
symbolServerUrl,
addressProof,
this._archiveCache,
delegate
delegate,
profile,
sourceViewSourceIndex
);

switch (fetchSourceResult.type) {
Expand Down
1 change: 1 addition & 0 deletions src/profile-logic/data-structures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ export function getEmptySourceTable(): SourceTable {
// be caught by the type system.
uuid: [],
filename: [],
sourceCode: [],
length: 0,
};
}
Expand Down
3 changes: 2 additions & 1 deletion src/profile-logic/global-data-collector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class GlobalDataCollector {
_libKeyToLibIndex: Map<string, IndexIntoLibs> = new Map();
_stringArray: string[] = [];
_stringTable: StringTable = StringTable.withBackingArray(this._stringArray);
_sources: SourceTable = { length: 0, uuid: [], filename: [] };
_sources: SourceTable = { length: 0, uuid: [], filename: [], sourceCode: [] };
_uuidToSourceIndex: Map<string, IndexIntoSourceTable> = new Map();
_filenameToSourceIndex: Map<IndexIntoStringTable, IndexIntoSourceTable> =
new Map();
Expand Down Expand Up @@ -72,6 +72,7 @@ export class GlobalDataCollector {
const filenameIndex = this._stringTable.indexForString(filename);
this._sources.uuid[index] = uuid;
this._sources.filename[index] = filenameIndex;
this._sources.sourceCode[index] = null; // Initially no source code
this._sources.length++;

if (uuid !== null) {
Expand Down
7 changes: 6 additions & 1 deletion src/profile-logic/merge-compare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,12 @@ function mergeSources(
sources: SourceTable;
translationMaps: TranslationMapForSources[];
} {
const newSources: SourceTable = { length: 0, uuid: [], filename: [] };
const newSources: SourceTable = {
length: 0,
uuid: [],
filename: [],
sourceCode: [],
};
const mapOfInsertedSources: Map<string, IndexIntoSourceTable> = new Map();

const translationMaps = sourcesPerProfile.map((sources, profileIndex) => {
Expand Down
17 changes: 17 additions & 0 deletions src/profile-logic/profile-compacting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ function _createCompactedSourceTable(
let nextIndex = 0;
const newUuid = [];
const newFilename = [];
const newSourceCode = [];

for (let i = 0; i < sourceTable.length; i++) {
if (referencedSources[i] === 0) {
Expand All @@ -418,13 +419,29 @@ function _createCompactedSourceTable(
}
newFilename[newIndex] = newFilenameIndexPlusOne - 1;

// Translate the source code string index
const oldSourceCodeIndex = sourceTable.sourceCode[i];
if (oldSourceCodeIndex !== null) {
const newSourceCodeIndexPlusOne =
oldStringToNewStringPlusOne[oldSourceCodeIndex];
if (newSourceCodeIndexPlusOne === 0) {
throw new Error(
`String index ${oldSourceCodeIndex} was not found in the translation map`
);
}
newSourceCode[newIndex] = newSourceCodeIndexPlusOne - 1;
} else {
newSourceCode[newIndex] = null;
}

oldSourceToNewSourcePlusOne[i] = newIndex + 1;
}

const newSources: SourceTable = {
length: nextIndex,
uuid: newUuid,
filename: newFilename,
sourceCode: newSourceCode,
};

return { newSources, oldSourceToNewSourcePlusOne };
Expand Down
14 changes: 13 additions & 1 deletion src/profile-logic/sanitize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,18 @@ export function sanitizePII(
}
const stringTable = StringTable.withBackingArray(stringArray);

// Handle JS source removal if requested
let sources = profile.shared.sources;
if (PIIToBeRemoved.shouldRemoveJSSources) {
// Create a new sources table with sourceCode set to null for all entries
sources = {
length: sources.length,
uuid: sources.uuid.slice(),
filename: sources.filename.slice(),
sourceCode: sources.sourceCode.map(() => null),
};
}

let removingCounters = false;
const newProfile: Profile = {
...profile,
Expand All @@ -129,7 +141,7 @@ export function sanitizePII(
pages: pages,
shared: {
stringArray,
sources: profile.shared.sources,
sources,
},
threads: profile.threads.reduce<RawThread[]>((acc, thread, threadIndex) => {
const newThread: RawThread | null = sanitizeThreadPII(
Expand Down
2 changes: 2 additions & 0 deletions src/reducers/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ function _getSanitizingSharingOptions(): CheckedSharingOptions {
includeExtension: false,
includePreferenceValues: false,
includePrivateBrowsingData: false,
includeJSSources: false,
};
}

Expand All @@ -39,6 +40,7 @@ function _getMostlyNonSanitizingSharingOptions(): CheckedSharingOptions {
includePreferenceValues: true,
// We always want to sanitize the private browsing data by default
includePrivateBrowsingData: false,
includeJSSources: true,
};
}

Expand Down
1 change: 1 addition & 0 deletions src/selectors/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ export const getRemoveProfileInformation: Selector<RemoveProfileInformation | nu
!checkedSharingOptions.includePreferenceValues,
shouldRemovePrivateBrowsingData:
!checkedSharingOptions.includePrivateBrowsingData,
shouldRemoveJSSources: !checkedSharingOptions.includeJSSources,
};
}
);
Expand Down
1 change: 1 addition & 0 deletions src/test/unit/sanitize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ describe('sanitizePII', function () {
shouldRemoveExtensions: false,
shouldRemovePreferenceValues: false,
shouldRemovePrivateBrowsingData: false,
shouldRemoveJSSources: false,
};

const PIIToRemove: RemoveProfileInformation = {
Expand Down
1 change: 1 addition & 0 deletions src/types/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export type CheckedSharingOptions = {
includeExtension: boolean;
includePreferenceValues: boolean;
includePrivateBrowsingData: boolean;
includeJSSources: boolean;
};

// This type is used when selecting tracks in the timeline. Ctrl and Meta are
Expand Down
2 changes: 2 additions & 0 deletions src/types/profile-derived.ts
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,8 @@ export type RemoveProfileInformation = {
readonly shouldRemovePreferenceValues: boolean;
// Remove the private browsing data if it's true.
readonly shouldRemovePrivateBrowsingData: boolean;
// Remove the JavaScript sources if it's true.
readonly shouldRemoveJSSources: boolean;
};

/**
Expand Down
3 changes: 3 additions & 0 deletions src/types/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,9 @@ export type SourceTable = {
length: number;
uuid: Array<string | null>;
filename: Array<IndexIntoStringTable>;
// Index into the string table for the source code content.
// Null if the source code hasn't been downloaded yet.
sourceCode: Array<IndexIntoStringTable | null>;
};

export type RawProfileSharedData = {
Expand Down
21 changes: 20 additions & 1 deletion src/utils/fetch-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import type { ExternalCommunicationDelegate } from './query-api';
import type {
SourceCodeLoadingError,
AddressProof,
Profile,
IndexIntoSourceTable,
} from 'firefox-profiler/types';

export type FetchSourceResult =
Expand All @@ -36,17 +38,34 @@ export type FetchSourceResult =
* @param archiveCache - A map which allows reusing the bytes of the archive file.
* Stores promises to the bytes of uncompressed tar files.
* @param delegate - An object which handles web requests and browser connection queries.
* @param profile - The profile, used to check if source code is already downloaded.
* @param sourceIndex - The index into the sources table, used to check if source code is already downloaded.
*/
export async function fetchSource(
file: string,
sourceUuid: string | null,
symbolServerUrl: string,
addressProof: AddressProof | null,
archiveCache: Map<string, Promise<Uint8Array>>,
delegate: ExternalCommunicationDelegate
delegate: ExternalCommunicationDelegate,
profile: Profile | null = null,
sourceIndex: IndexIntoSourceTable | null = null
): Promise<FetchSourceResult> {
const errors: SourceCodeLoadingError[] = [];

// First, check if we already have the source code in the profile
if (
profile !== null &&
sourceIndex !== null &&
sourceIndex < profile.shared.sources.length
) {
const sourceCodeIndex = profile.shared.sources.sourceCode[sourceIndex];
if (sourceCodeIndex !== null) {
const sourceCode = profile.shared.stringArray[sourceCodeIndex];
return { type: 'SUCCESS', source: sourceCode };
}
}

if (addressProof !== null) {
// Prepare a request to /source/v1. The API format for this endpoint is documented
// at https://github.com/mstange/profiler-get-symbols/blob/master/API.md#sourcev1
Expand Down