Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3106131
Extract the GlobalDataCollector class to its own file
canova Sep 17, 2025
377859a
Add sources table to the gecko and processed profile formats
canova Sep 17, 2025
0a6b001
Add source table to the derived thread
canova Sep 19, 2025
bc3505e
Change funcTable fileName array into sources array
canova Sep 17, 2025
cd6b096
Update the test snapshots after the new sources table and funcTable c…
canova Sep 17, 2025
aac2c13
Add a processed profile upgrader after the `profile.shared.sources` a…
canova Sep 17, 2025
7afe9d6
Bump gecko profile format version for the new frameTable location format
canova Aug 5, 2025
d200dbc
Add tests for the source table processing
canova Sep 18, 2025
ec7ba3a
Add tests for GlobalDataCollector for sources
canova Sep 18, 2025
0ec9366
Add tests for merge-compare code with the source table
canova Sep 18, 2025
dd00c8a
Store sourceIndex instead of sourceFile in url state and source view …
canova Sep 17, 2025
0d758c1
Add a url upgrader for the changed sourceView query parameter
canova Sep 18, 2025
f455e83
Simplify the getStackLineInfo code by passing sourceIndex instead of …
canova Sep 18, 2025
0f897f0
Add tests for the source code cache
canova Sep 18, 2025
3721104
Pass sourceUuid through the source fetching pipeline
canova Sep 17, 2025
1237329
Add WebChannel API for JS source code fetching
canova Sep 17, 2025
a705106
Fetch the JS source from the browser
canova Jun 28, 2025
059ccc2
Add tests for JavaScript source fetching
canova Jul 3, 2025
06d187d
Change the response type of GET_JS_SOURCE and print the error to the …
canova Sep 22, 2025
722ff87
Implement compacting for the shared source table
canova Sep 22, 2025
a4b90ed
Remove _processSourceTable and only add the sources while we are proc…
canova Sep 22, 2025
9f0aefb
Make the source code cache tests use the proper actions
canova Sep 22, 2025
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
8 changes: 8 additions & 0 deletions docs-developer/CHANGELOG-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ Note that this is not an exhaustive list. Processed profile format upgraders can

## Processed profile format

### Version 58

A new `SourceTable` has been added to `profile.shared.sources` to centralize all source file information. The `FuncTable.fileName` field has been replaced with `FuncTable.source`, which references indices in the shared sources table. This change allows storing a UUID per JS source, which will be used for fetching sources.

### Version 57

The `searchable` property in marker schemas, originally added in version 44, is now removed again. Now all marker fields are searchable.
Expand Down Expand Up @@ -122,6 +126,10 @@ Older versions are not documented in this changelog but can be found in [process

## Gecko profile format

### Version 32

`frameTable` `location` string field was changed to include an optional `sourceIndex` at the end of the string inside brackets for JS sources. For example, new JS frames look like this: `functionName (http://script.url/:1234:1234)[1234]` with the last number being its `sourceIndex`. This index references entries in the shared `SourceTable` in `profile.sources` (added in in the same version) which centralizes all source file information.

### Version 31

Two new marker schema field format types have been added: `flow-id` and `terminating-flow-id`, with string index values (like `unique-string`).
Expand Down
9 changes: 9 additions & 0 deletions locales/en-US/app.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1243,6 +1243,15 @@ SourceView--not-in-archive-error-when-obtaining-source =
SourceView--archive-parsing-error-when-obtaining-source =
The archive at { $url } could not be parsed: { $parsingErrorMessage }

# Displayed below SourceView--cannot-obtain-source, if a JS file could not be found in
# the browser.
# Variables:
# $url (String) - The URL of the JS source file.
# $sourceUuid (number) - The UUID of the JS source file.
# $errorMessage (String) - The raw internal error message, not localized
SourceView--not-in-browser-error-when-obtaining-js-source =
The browser was unable to obtain the source file for { $url } with sourceUuid { $sourceUuid }: { $errorMessage }.

## Toggle buttons in the top right corner of the bottom box

# The toggle button for the assembly view, while the assembly view is hidden.
Expand Down
20 changes: 12 additions & 8 deletions src/actions/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,34 @@ import type {
SourceCodeLoadingError,
ApiQueryError,
DecodedInstruction,
IndexIntoSourceTable,
} from 'firefox-profiler/types';

export function beginLoadingSourceCodeFromUrl(
file: string,
sourceIndex: IndexIntoSourceTable,
url: string
): Action {
return { type: 'SOURCE_CODE_LOADING_BEGIN_URL', file, url };
return { type: 'SOURCE_CODE_LOADING_BEGIN_URL', sourceIndex, url };
}

export function beginLoadingSourceCodeFromBrowserConnection(
file: string
sourceIndex: IndexIntoSourceTable
): Action {
return { type: 'SOURCE_CODE_LOADING_BEGIN_BROWSER_CONNECTION', file };
return { type: 'SOURCE_CODE_LOADING_BEGIN_BROWSER_CONNECTION', sourceIndex };
}

export function finishLoadingSourceCode(file: string, code: string): Action {
return { type: 'SOURCE_CODE_LOADING_SUCCESS', file, code };
export function finishLoadingSourceCode(
sourceIndex: IndexIntoSourceTable,
code: string
): Action {
return { type: 'SOURCE_CODE_LOADING_SUCCESS', sourceIndex, code };
}

export function failLoadingSourceCode(
file: string,
sourceIndex: IndexIntoSourceTable,
errors: SourceCodeLoadingError[]
): Action {
return { type: 'SOURCE_CODE_LOADING_ERROR', file, errors };
return { type: 'SOURCE_CODE_LOADING_ERROR', sourceIndex, errors };
}

export function beginLoadingAssemblyCodeFromUrl(
Expand Down
8 changes: 4 additions & 4 deletions src/actions/profile-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1917,7 +1917,7 @@ export function changeTableViewOptions(

export function updateBottomBoxContentsAndMaybeOpen(
currentTab: TabSlug,
{ libIndex, sourceFile, nativeSymbols }: BottomBoxInfo
{ libIndex, sourceIndex, nativeSymbols }: BottomBoxInfo
): Action {
// TODO: If the set has more than one element, pick the native symbol with
// the highest total sample count
Expand All @@ -1926,12 +1926,12 @@ export function updateBottomBoxContentsAndMaybeOpen(
return {
type: 'UPDATE_BOTTOM_BOX',
libIndex,
sourceFile,
sourceIndex,
nativeSymbol,
allNativeSymbolsForInitiatingCallNode: nativeSymbols,
currentTab,
shouldOpenBottomBox: sourceFile !== null || nativeSymbol !== null,
shouldOpenAssemblyView: sourceFile === null && nativeSymbol !== null,
shouldOpenBottomBox: sourceIndex !== null || nativeSymbol !== null,
shouldOpenAssemblyView: sourceIndex === null && nativeSymbol !== null,
};
}

Expand Down
30 changes: 30 additions & 0 deletions src/app-logic/browser-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
querySymbolicationApiViaWebChannel,
getPageFaviconsViaWebChannel,
showFunctionInDevtoolsViaWebChannel,
getJSSourcesViaWebChannel,
} from './web-channel';
import type {
Milliseconds,
Expand Down Expand Up @@ -83,6 +84,8 @@ export interface BrowserConnection {
line: number | null,
column: number | null
): Promise<void>;

getJSSource(sourceUuid: string): Promise<string>;
}

/**
Expand All @@ -98,6 +101,7 @@ class BrowserConnectionImpl implements BrowserConnection {
_webChannelSupportsGetExternalMarkers: boolean;
_webChannelSupportsGetPageFavicons: boolean;
_webChannelSupportsOpenDebuggerInTab: boolean;
_webChannelSupportsGetJSSource: boolean;
_geckoProfiler: $GeckoProfiler | undefined;

constructor(webChannelVersion: number) {
Expand All @@ -106,6 +110,7 @@ class BrowserConnectionImpl implements BrowserConnection {
this._webChannelSupportsGetExternalMarkers = webChannelVersion >= 3;
this._webChannelSupportsGetPageFavicons = webChannelVersion >= 4;
this._webChannelSupportsOpenDebuggerInTab = webChannelVersion >= 5;
this._webChannelSupportsGetJSSource = webChannelVersion >= 6;
}

// Only called when we must obtain the profile from the browser, i.e. if we
Expand Down Expand Up @@ -226,6 +231,31 @@ class BrowserConnectionImpl implements BrowserConnection {

return [];
}

/**
* Fetches JavaScript source code from the browser using the source UUID.
* This method requires WebChannel version 6 or higher (Firefox 145+).
*/
async getJSSource(sourceUuid: string): Promise<string> {
if (!this._webChannelSupportsGetJSSource) {
throw new Error(
"Can't use getJSSource in Firefox versions with the old WebChannel."
);
}

// Even though the WebChannel request for fetching JS sources supports
// fetching multiple sources, we only fetch one at a time currently.
// TODO: Change this to fetch multiple JS sources at the load time or while
// we share the profile.
return getJSSourcesViaWebChannel([sourceUuid]).then((sources) => {
const source = sources[0];
if ('error' in source) {
throw new Error(source.error);
}

return source.sourceText;
});
}
}

// Should work with:
Expand Down
4 changes: 2 additions & 2 deletions src/app-logic/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import type { MarkerPhase } from 'firefox-profiler/types';
// The current version of the Gecko profile format.
// Please don't forget to update the gecko profile format changelog in
// `docs-developer/CHANGELOG-formats.md`.
export const GECKO_PROFILE_VERSION = 31;
export const GECKO_PROFILE_VERSION = 32;

// The current version of the "processed" profile format.
// Please don't forget to update the processed profile format changelog in
// `docs-developer/CHANGELOG-formats.md`.
export const PROCESSED_PROFILE_VERSION = 57;
export const PROCESSED_PROFILE_VERSION = 58;

// The following are the margin sizes for the left and right of the timeline. Independent
// components need to share these values.
Expand Down
46 changes: 38 additions & 8 deletions src/app-logic/url-handling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ import {
encodeUintSetForUrlComponent,
} from '../utils/uintarray-encoding';
import { tabSlugs } from '../app-logic/tabs-handling';
import { StringTable } from 'firefox-profiler/utils/string-table';

export const CURRENT_URL_VERSION = 11;
export const CURRENT_URL_VERSION = 12;

/**
* This static piece of state might look like an anti-pattern, but it's a relatively
Expand Down Expand Up @@ -171,7 +172,7 @@ type BaseQuery = {
view: string;
implementation: string;
timelineType: string;
sourceView: string;
sourceViewIndex: number;
assemblyView: string;
};

Expand Down Expand Up @@ -208,7 +209,7 @@ type Query = BaseQuery & {
invertCallstack?: null | undefined;
ctSummary?: string;
transforms?: string;
sourceView?: string;
sourceViewIndex?: number;
assemblyView?: string;

// StackChart specific
Expand Down Expand Up @@ -349,8 +350,8 @@ export function getQueryStringFromUrlState(urlState: UrlState): string {
urlState.profileSpecific;

if (isBottomBoxOpenPerPanel[selectedTab]) {
if (sourceView.sourceFile !== null) {
query.sourceView = sourceView.sourceFile;
if (sourceView.sourceIndex !== null) {
query.sourceViewIndex = sourceView.sourceIndex;
}
if (assemblyView.isOpen && assemblyView.nativeSymbol !== null) {
query.assemblyView = stringifyAssemblyViewSymbol(
Expand Down Expand Up @@ -508,7 +509,7 @@ export function stateFromLocation(
const sourceView: SourceViewState = {
scrollGeneration: 0,
libIndex: null,
sourceFile: null,
sourceIndex: null,
};
const assemblyView: AssemblyViewState = {
isOpen: false,
Expand All @@ -518,8 +519,8 @@ export function stateFromLocation(
};
const isBottomBoxOpenPerPanel: any = {};
tabSlugs.forEach((tabSlug) => (isBottomBoxOpenPerPanel[tabSlug] = false));
if (query.sourceView) {
sourceView.sourceFile = query.sourceView;
if (query.sourceViewIndex !== undefined) {
sourceView.sourceIndex = Number(query.sourceViewIndex);
isBottomBoxOpenPerPanel[selectedTab] = true;
}
if (query.assemblyView) {
Expand Down Expand Up @@ -1162,6 +1163,35 @@ const _upgraders: {
processedLocation.query = {};
}
},
[12]: (
processedLocation: ProcessedLocationBeforeUpgrade,
profile?: Profile
) => {
Comment on lines +1166 to +1169
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh this is actually simpler than I feared. The URL upgrader just gets the upgraded profile? Neat. And in this case profile upgrading didn't discard any information that we might need to upgrade the URL.

(I'm only thinking about this because I'm not looking forward to upgrading funcIndexes in the URL inside transforms, once we move to a shared funcTable...)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it was quite simple! We get the upgraded profile here.

(I'm only thinking about this because I'm not looking forward to upgrading funcIndexes in the URL inside transforms, once we move to a shared funcTable...)

Oh, that's a good point. I guess there won't be a "old funcTable index -> new funcTable index" map when we upgrade the profiles. (but maybe we can for the upgraded ones?)

// This version changes the source view parameter from 'sourceView' (filename
// string) to 'sourceViewIndex' (IndexIntoSourceTable). If we can't convert
// the filename to a source index, we just remove the sourceView parameter.
const { query } = processedLocation;
if (!('sourceView' in query) || !profile || !profile.shared.sources) {
return;
}

// Try to find the source index for the given filename
const filename = query.sourceView;
const { sources, stringArray } = profile.shared;
const stringTable = StringTable.withBackingArray(stringArray);

// Find the filename string index
const filenameStringIndex = stringTable.indexForString(filename);
if (filenameStringIndex !== -1) {
// Find the source index with this filename
const sourceIndex = sources.filename.indexOf(filenameStringIndex);
if (sourceIndex !== -1) {
query.sourceViewIndex = sourceIndex;
}
}
// Remove the old sourceView parameter regardless of whether we found a match
delete query.sourceView;
},
};

for (let destVersion = 1; destVersion <= CURRENT_URL_VERSION; destVersion++) {
Expand Down
26 changes: 25 additions & 1 deletion src/app-logic/web-channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export type Request =
| GetSymbolTableRequest
| QuerySymbolicationApiRequest
| GetPageFaviconsRequest
| OpenScriptInTabDebuggerRequest;
| OpenScriptInTabDebuggerRequest
| GetJSSourcesRequest;

type StatusQueryRequest = { type: 'STATUS_QUERY' };
type EnableMenuButtonRequest = { type: 'ENABLE_MENU_BUTTON' };
Expand Down Expand Up @@ -64,6 +65,10 @@ type OpenScriptInTabDebuggerRequest = {
line: number | null;
column: number | null;
};
type GetJSSourcesRequest = {
type: 'GET_JS_SOURCES';
sourceUuids: Array<string>;
};

export type MessageFromBrowser<R extends ResponseFromBrowser> =
| OutOfBandErrorMessageFromBrowser
Expand Down Expand Up @@ -128,6 +133,10 @@ type StatusQueryResponse = {
// Shipped in Firefox 136.
// Adds support for showing the JS script in DevTools debugger.
// - OPEN_SCRIPT_IN_DEBUGGER
// Version 6:
// Shipped in Firefox 145.
// Adds support for fetching JS sources.
// - GET_JS_SOURCES
version?: number;
};
type EnableMenuButtonResponse = void;
Expand All @@ -138,6 +147,8 @@ type GetSymbolTableResponse = SymbolTableAsTuple;
type QuerySymbolicationApiResponse = string;
type GetPageFaviconsResponse = Array<FaviconData | null>;
type OpenScriptInTabDebuggerResponse = void;
type GetJSSourceReponseItem = { sourceText: string } | { error: string };
type GetJSSourcesResponse = Array<GetJSSourceReponseItem>;

// TypeScript function overloads for request/response pairs.
function _sendMessageWithResponse(
Expand Down Expand Up @@ -167,6 +178,10 @@ function _sendMessageWithResponse(
function _sendMessageWithResponse(
request: OpenScriptInTabDebuggerRequest
): Promise<OpenScriptInTabDebuggerResponse>;
function _sendMessageWithResponse(
request: GetJSSourcesRequest
): Promise<GetJSSourcesResponse>;

function _sendMessageWithResponse(request: Request): Promise<any> {
const requestId = _requestId++;
const type = request.type;
Expand Down Expand Up @@ -371,6 +386,15 @@ export async function showFunctionInDevtoolsViaWebChannel(
});
}

export async function getJSSourcesViaWebChannel(
sourceUuids: Array<string>
): Promise<Array<GetJSSourceReponseItem>> {
return _sendMessageWithResponse({
type: 'GET_JS_SOURCES',
sourceUuids,
});
}

/**
* -----------------------------------------------------------------------------
*
Expand Down
6 changes: 4 additions & 2 deletions src/components/app/BottomBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { IonGraphView } from '../shared/IonGraphView';
import { CodeLoadingOverlay } from './CodeLoadingOverlay';
import { CodeErrorOverlay } from './CodeErrorOverlay';
import {
getSourceViewFile,
getSourceViewScrollGeneration,
getAssemblyViewIsOpen,
getAssemblyViewNativeSymbol,
Expand All @@ -29,7 +28,10 @@ import {
getSourceViewCode,
getAssemblyViewCode,
} from 'firefox-profiler/selectors/code';
import { getPreviewSelectionIsBeingModified } from 'firefox-profiler/selectors/profile';
import {
getPreviewSelectionIsBeingModified,
getSourceViewFile,
} from 'firefox-profiler/selectors/profile';
import explicitConnect from 'firefox-profiler/utils/connect';

import type { ConnectedProps } from 'firefox-profiler/utils/connect';
Expand Down
11 changes: 11 additions & 0 deletions src/components/app/CodeErrorOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,17 @@ export function CodeErrorOverlay({ errors }: CodeErrorOverlayProps) {
</Localized>
);
}
case 'NOT_PRESENT_IN_BROWSER': {
const { sourceUuid, url, errorMessage } = error;
return (
<Localized
id="SourceView--not-in-browser-error-when-obtaining-js-source"
vars={{ url, sourceUuid, errorMessage }}
>
<li>{`The browser was unable to obtain the source file for ${url} with sourceUuid ${sourceUuid}: ${errorMessage}`}</li>
</Localized>
);
}
default:
throw assertExhaustiveCheck(error);
}
Expand Down
Loading