Skip to content
Open
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
22 changes: 22 additions & 0 deletions locales/en-US/app.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,28 @@ MenuButtons--metaInfo--resymbolicate-profile = Re-symbolicate profile
MenuButtons--metaInfo--symbolicate-profile = Symbolicate profile
MenuButtons--metaInfo--attempting-resymbolicate = Attempting to re-symbolicate profile
MenuButtons--metaInfo--currently-symbolicating = Currently symbolicating profile
MenuButtons--metaInfo--source-maps = Source maps:
# The trailing ellipsis indicates that clicking the button opens a file picker.
MenuButtons--metaInfo--apply-source-map = Apply source map…
.title = Load a .map file from disk to symbolicate a minified JavaScript bundle, to recover original function names and source locations.
# Shown when the uploaded map could match more than one source and the user has
# to choose which one it applies to.
MenuButtons--metaInfo--source-map-choose-bundle = Choose which bundle this source map applies to:
# Button to confirm the chosen source and apply the source map to it.
MenuButtons--metaInfo--source-map-apply = Apply
# Button to dismiss the source chooser without symbolicating.
MenuButtons--metaInfo--source-map-cancel = Cancel
# Shown after symbolication finished and original sources were resolved.
# Variable:
# $filename (String) - The bundle source the source map was applied to.
MenuButtons--metaInfo--source-map-success = Original sources resolved for { $filename }.
# Shown after symbolication finished but no stack positions matched the map.
# Variable:
# $filename (String) - The bundle source the source map was applied to.
MenuButtons--metaInfo--source-map-no-match = No stack positions in { $filename } matched this source map.
MenuButtons--metaInfo--source-map-error-invalid = The selected file is not a valid source map.
MenuButtons--metaInfo--source-map-error-no-eligible = This profile has no JS bundles with source map URLs.
MenuButtons--metaInfo--source-map-error-failed = Could not apply this source map to the profile.
MenuButtons--metaInfo--cpu-model = CPU model:
MenuButtons--metaInfo--cpu-cores = CPU cores:
MenuButtons--metaInfo--main-memory = Main memory:
Expand Down
6 changes: 4 additions & 2 deletions src/actions/receive-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import {
hasUsefulSamples,
} from 'firefox-profiler/profile-logic/profile-data';
import { doSourceMapSymbolication } from './source-map-symbolication';
import type { SourceMapSymbolicationResult } from './source-map-symbolication';

import type { RawSourceMap } from 'source-map';
import type {
Expand Down Expand Up @@ -254,7 +255,8 @@ export function finalizeProfileView(
// funcs aren't in those sets), and the JS apply step reads current
// shared state at dispatch time so it composes with whatever native
// has committed by then. Requires WebChannel version 7+.
let sourceMapSymbolicationPromise: Promise<void> | null = null;
let sourceMapSymbolicationPromise: Promise<SourceMapSymbolicationResult> | null =
null;
if (browserConnection !== null && browserConnection.supportsGetSourceMap) {
sourceMapSymbolicationPromise = doResolveSourceMaps(
profile,
Expand Down Expand Up @@ -1156,7 +1158,7 @@ export function waitingForProfileFromFile(): Action {
};
}

function _fileReader(input: File) {
export function _fileReader(input: File) {
const reader = new FileReader();
const promise = new Promise((resolve, reject) => {
// Flow's definition for FileReader doesn't handle the polymorphic nature of
Expand Down
111 changes: 105 additions & 6 deletions src/actions/source-map-symbolication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@

import { getRawProfileSharedData } from 'firefox-profiler/selectors';
import { applySourceMapSymbolicationResponse } from 'firefox-profiler/profile-logic/source-map-symbolication';
import {
getSourcesWithSourceMapURL,
parseSourceMapFileContents,
matchSourceMapToSource,
} from 'firefox-profiler/profile-logic/source-map-matching';

import type {
WorkerInput,
WorkerOutput,
} from 'firefox-profiler/profile-logic/source-map-worker-types';
import type { IndexIntoSourceTable, ThunkAction } from 'firefox-profiler/types';
import type { EligibleSource } from 'firefox-profiler/profile-logic/source-map-matching';
import type { RawSourceMap } from 'source-map';
import { assertExhaustiveCheck } from 'firefox-profiler/utils/types';

Expand All @@ -26,13 +32,15 @@ import { assertExhaustiveCheck } from 'firefox-profiler/utils/types';
* text (fetched alongside the source maps). Used for scope-tree-based function
* name resolution.
*/
export type SourceMapSymbolicationResult = 'applied' | 'no-match' | 'error';

export function doSourceMapSymbolication(
resolvedSourceMaps: Map<IndexIntoSourceTable, RawSourceMap>,
compiledSources: Map<IndexIntoSourceTable, string>
): ThunkAction<Promise<void>> {
): ThunkAction<Promise<SourceMapSymbolicationResult>> {
return async (dispatch, getState) => {
if (resolvedSourceMaps.size === 0) {
return;
return 'no-match';
}

const shared = getRawProfileSharedData(getState());
Expand Down Expand Up @@ -61,7 +69,7 @@ export function doSourceMapSymbolication(
);
if (applied === null) {
dispatch({ type: 'SOURCE_MAP_SYMBOLICATION_FAILED' });
break;
return 'no-match';
}
dispatch({
type: 'BULK_SOURCE_MAP_SYMBOLICATION',
Expand All @@ -71,21 +79,112 @@ export function doSourceMapSymbolication(
newSources: applied.newSources,
newStringArray: applied.newStringArray,
});
break;
return 'applied';
}
case 'error':
console.warn('Source map worker error:', result.message);
dispatch({ type: 'SOURCE_MAP_SYMBOLICATION_FAILED' });
break;
return 'error';
case 'no-op':
dispatch({ type: 'SOURCE_MAP_SYMBOLICATION_FAILED' });
break;
return 'no-match';
default:
throw assertExhaustiveCheck(result);
}
};
}

/**
* The ways applying a user-supplied `.map` file can fail.
*/
export type ApplySourceMapError =
| 'invalid-source-map'
| 'no-eligible-sources'
| 'symbolication-failed';

/**
* The outcome of applying a user-supplied `.map` file to the profile.
*/
export type ApplySourceMapFileResult =
// `filename` is the resolved bundle source the map was applied to, so the UI
// can show which source was matched (auto-matched or user-picked).
| { type: 'applied'; filename: string }
| { type: 'no-match'; filename: string }
| { type: 'ambiguous'; candidates: EligibleSource[] }
| { type: 'error'; error: ApplySourceMapError };

/**
* Apply a source map file that the user selected from disk. Parses the file,
* auto-matches it to a bundle source (unless `sourceIndex` is provided, e.g.
* after the user disambiguated in the picker), and feeds it into the existing
* source map symbolication pipeline.
*/
export function applySourceMapFile(
fileName: string,
fileContents: string,
// Set when the user picked a bundle in the picker; skips auto-matching.
sourceIndex?: IndexIntoSourceTable
): ThunkAction<Promise<ApplySourceMapFileResult>> {
return async (dispatch, getState) => {
const map = parseSourceMapFileContents(fileContents);
if (map === null) {
return { type: 'error', error: 'invalid-source-map' };
}

const shared = getRawProfileSharedData(getState());

let targetSourceIndex: IndexIntoSourceTable;
if (sourceIndex !== undefined) {
targetSourceIndex = sourceIndex;
} else {
const eligible = getSourcesWithSourceMapURL(
shared.sources,
shared.stringArray
);
const result = matchSourceMapToSource(map, fileName, eligible);
switch (result.type) {
case 'no-eligible-sources':
return { type: 'error', error: 'no-eligible-sources' };
case 'ambiguous':
return { type: 'ambiguous', candidates: result.candidates };
case 'match':
targetSourceIndex = result.sourceIndex;
break;
default:
throw assertExhaustiveCheck(result);
}
}

const filename =
shared.stringArray[shared.sources.filename[targetSourceIndex]];

// Feed the bundle's stored content as the compiled source when we have it,
// which enables full scope-tree name resolution.
const compiledContent = shared.sources.content[targetSourceIndex];
const compiledSources =
compiledContent !== null
? new Map([[targetSourceIndex, compiledContent]])
: new Map<IndexIntoSourceTable, string>();

const outcome = await dispatch(
doSourceMapSymbolication(
new Map([[targetSourceIndex, map]]),
compiledSources
)
);
switch (outcome) {
case 'applied':
return { type: 'applied', filename };
case 'no-match':
return { type: 'no-match', filename };
case 'error':
return { type: 'error', error: 'symbolication-failed' };
default:
throw assertExhaustiveCheck(outcome);
}
};
}

/**
* Spawn a one-shot source map worker, send it the input, and return the
* output. The worker is terminated once a response is received or an error
Expand Down
Loading
Loading