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 jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const browserEnvConfig = {
globals: {
AVAILABLE_STAGING_LOCALES: null,
SOURCE_MAP_WORKER_PATH: 'src/test/fixtures/source-map.worker.stub.js',
FORMAT_WORKER_PATH: 'src/test/fixtures/format.worker.stub.js',
},

snapshotFormat: {
Expand Down
10 changes: 10 additions & 0 deletions locales/en-US/app.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1420,6 +1420,16 @@ SourceView--not-in-browser-error-when-obtaining-js-source =

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

# The {} toggle button for pretty-printing the JavaScript code in the source
# view, while the code is shown as it is. Pretty-printing adds line breaks and
# indentation to make minified code easier to read.
SourceView--pretty-print-show-button =
.title = Pretty-print the code

# The {} toggle button for pretty-printing, while the code is pretty-printed.
SourceView--pretty-print-hide-button =
.title = Show the code without pretty-printing

# The toggle button for the assembly view, while the assembly view is hidden.
# Assembly refers to the low-level programming language.
AssemblyView--show-button =
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
"@fluent/bundle": "^0.19.1",
"@fluent/langneg": "^0.7.0",
"@fluent/react": "^0.15.2",
"@lezer/common": "^1.5.0",
"@lezer/highlight": "^1.2.5",
"@lezer/javascript": "^1.5.6",
"@streamparser/json": "^0.0.26",
Expand Down
6 changes: 4 additions & 2 deletions scripts/build-profiler-cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ const profilerCliConfig = {
__BUILD_HASH__: JSON.stringify(BUILD_HASH),
__PACKAGE_NAME__: JSON.stringify(name),
__VERSION__: JSON.stringify(version),
// SOURCE_MAP_WORKER_PATH is injected by the browser build. The CLI doesn't
// use source map workers but the shared code references this constant.
// SOURCE_MAP_WORKER_PATH and FORMAT_WORKER_PATH are injected by the
// browser build. The CLI doesn't use these workers but the shared code
// references these constants.
SOURCE_MAP_WORKER_PATH: JSON.stringify('/source-map.worker.js'),
FORMAT_WORKER_PATH: JSON.stringify('/format.worker.js'),
},
external: [...nodeBaseConfig.external, 'gecko-profiler-demangle'],
};
Expand Down
23 changes: 15 additions & 8 deletions scripts/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,23 @@ import {
mainBundleConfig,
sourceMapWorkerConfig,
getSourceMapWorkerPath,
formatWorkerConfig,
getFormatWorkerPath,
} from './lib/esbuild-configs.mjs';
import { cleanDist, saveMetafile } from './lib/build-utils.mjs';

async function build() {
cleanDist();

// Build the worker first so we can read its output path from the metafile
// and inject it into the main bundle via SOURCE_MAP_WORKER_PATH.
const workerResult = await esbuild.build({
...sourceMapWorkerConfig,
entryNames:
process.env.NODE_ENV === 'production' ? '[name]-[hash]' : '[name]',
});
// Build the workers first so we can read their output paths from the
// metafiles and inject them into the main bundle via SOURCE_MAP_WORKER_PATH
// and FORMAT_WORKER_PATH.
const workerEntryNames =
process.env.NODE_ENV === 'production' ? '[name]-[hash]' : '[name]';
const [workerResult, formatWorkerResult] = await Promise.all([
esbuild.build({ ...sourceMapWorkerConfig, entryNames: workerEntryNames }),
esbuild.build({ ...formatWorkerConfig, entryNames: workerEntryNames }),
]);

const buildResult = await esbuild.build({
...mainBundleConfig,
Expand All @@ -28,11 +32,14 @@ async function build() {
SOURCE_MAP_WORKER_PATH: JSON.stringify(
getSourceMapWorkerPath(workerResult.metafile)
),
FORMAT_WORKER_PATH: JSON.stringify(
getFormatWorkerPath(formatWorkerResult.metafile)
),
},
});

saveMetafile(buildResult);
console.log('✅ Main browser build and source map worker completed');
console.log('✅ Main browser build and workers completed');
}

build().catch(console.error);
28 changes: 26 additions & 2 deletions scripts/lib/esbuild-configs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export const mainBundleConfig = {
// In dev, the worker is not hashed so the path is predictable.
// In production, build.mjs overrides this after building the worker first.
SOURCE_MAP_WORKER_PATH: JSON.stringify('/source-map.worker.js'),
FORMAT_WORKER_PATH: JSON.stringify('/format.worker.js'),
'process.env.ENABLE_SERVICE_WORKER': JSON.stringify(
process.env.ENABLE_SERVICE_WORKER || 'true'
),
Expand Down Expand Up @@ -139,14 +140,37 @@ export const sourceMapWorkerConfig = {
plugins: [wasmLoader()],
};

export function getSourceMapWorkerPath(metafile) {
const [entryPoint] = sourceMapWorkerConfig.entryPoints;
// Format worker bundle configuration, for formatting JS code in the source
// view. Built in the same way as the source map worker, see above.
export const formatWorkerConfig = {
...baseConfig,
entryPoints: ['src/profile-logic/js-format/format.worker.ts'],
outdir: 'dist',
format: 'iife',
platform: 'browser',
target: browserslistToEsbuild(),
sourcemap: true,
splitting: false,
entryNames: '[name]',
metafile: true,
};

function _getWorkerPath(workerConfig, metafile) {
const [entryPoint] = workerConfig.entryPoints;
const [outputPath] = Object.entries(metafile.outputs).find(
([, output]) => output.entryPoint === entryPoint
);
return '/' + path.basename(outputPath);
}

export function getSourceMapWorkerPath(metafile) {
return _getWorkerPath(sourceMapWorkerConfig, metafile);
}

export function getFormatWorkerPath(metafile) {
return _getWorkerPath(formatWorkerConfig, metafile);
}

// Photon styling build configuration
const photonTemplateHTML = fs.readFileSync(
path.join(projectRoot, 'res', 'photon', 'index.html'),
Expand Down
3 changes: 2 additions & 1 deletion scripts/run-dev-server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import path from 'path';
import {
mainBundleConfig,
sourceMapWorkerConfig,
formatWorkerConfig,
} from './lib/esbuild-configs.mjs';
import { startDevServer } from './lib/dev-server.mjs';
import { serveAndOpenProfile } from './lib/profile-server.mjs';
Expand All @@ -27,7 +28,7 @@ startDevServer(mainBundleConfig, {
host,
distDir: 'dist',
cleanDist: true,
extraWatchConfigs: [sourceMapWorkerConfig],
extraWatchConfigs: [sourceMapWorkerConfig, formatWorkerConfig],
onServerStart: (profilerUrl) => {
const barAscii =
'------------------------------------------------------------------------------------------';
Expand Down
45 changes: 45 additions & 0 deletions src/actions/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import type {
Action,
ThunkAction,
SourceCodeLoadingError,
ApiQueryError,
DecodedInstruction,
IndexIntoSourceTable,
} from 'firefox-profiler/types';
import type { SourceFormattingRequest } from 'firefox-profiler/selectors/code';
import { getSourceFormattingCache } from 'firefox-profiler/selectors/code';
import { runFormatWorker } from 'firefox-profiler/profile-logic/js-format/run-format-worker';
import { assertExhaustiveCheck } from 'firefox-profiler/utils/types';

export function beginLoadingSourceCodeFromUrl(
sourceIndex: IndexIntoSourceTable,
Expand Down Expand Up @@ -69,3 +74,43 @@ export function failLoadingAssemblyCode(
): Action {
return { type: 'ASSEMBLY_CODE_LOADING_ERROR', nativeSymbolKey, errors };
}

/**
* Pretty-prints a source in the format worker. The worker reports its progress
* in steps, so that the source view can show partially-formatted code.
*/
export function startFormattingSource(
request: SourceFormattingRequest
): ThunkAction<void> {
return (dispatch, getState) => {
const { sourceIndex, code, dialect } = request;
if (getSourceFormattingCache(getState()).has(sourceIndex)) {
return;
}
dispatch({ type: 'SOURCE_FORMATTING_BEGIN', sourceIndex });
runFormatWorker({ code, dialect }, (output) => {
switch (output.type) {
case 'progress':
dispatch({
type: 'SOURCE_FORMATTING_PROGRESS',
sourceIndex,
edits: output.edits,
isDone: output.isDone,
});
break;
case 'error':
console.error('Formatting the source failed:', output.message);
// Keep the edits that were made so far.
dispatch({
type: 'SOURCE_FORMATTING_PROGRESS',
sourceIndex,
edits: new Int32Array(0),
isDone: true,
});
break;
default:
throw assertExhaustiveCheck(output);
}
});
};
}
11 changes: 11 additions & 0 deletions src/actions/profile-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1957,6 +1957,8 @@ export function updateBottomBoxContentsAndMaybeOpen(
scrollToInstructionAddress,
highlightedLineNumber,
highlightedInstructionAddress,
scrollToColumnNumber,
highlightedColumnNumber,
} = bottomBoxInfo;

const haveSource = sourceIndex !== null;
Expand All @@ -1980,6 +1982,15 @@ export function updateBottomBoxContentsAndMaybeOpen(
scrollToInstructionAddress,
highlightedLineNumber,
highlightedInstructionAddress,
scrollToColumnNumber,
highlightedColumnNumber,
};
}

export function setSourceViewPrettyPrint(isPrettyPrinted: boolean): Action {
return {
type: 'SET_SOURCE_VIEW_PRETTY_PRINT',
isPrettyPrinted,
};
}

Expand Down
7 changes: 7 additions & 0 deletions src/app-logic/url-handling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ type BaseQuery = {
implementation: string;
timelineType: string;
sourceViewIndex: number;
sourceViewPretty: null | undefined;
assemblyView: string;
};

Expand Down Expand Up @@ -224,6 +225,7 @@ type Query = BaseQuery & {
ctSummary?: string;
transforms?: string;
sourceViewIndex?: number;
sourceViewPretty?: null | undefined;
assemblyView?: string;
bottomFullscreen?: boolean;

Expand Down Expand Up @@ -381,6 +383,9 @@ export function getQueryStringFromUrlState(urlState: UrlState): string {
if (isBottomBoxOpenPerPanel[selectedTab]) {
if (sourceView.sourceIndex !== null) {
query.sourceViewIndex = sourceView.sourceIndex;
query.sourceViewPretty = sourceView.isPrettyPrinted
? null
: undefined;
}
if (assemblyView.isOpen && assemblyView.currentNativeSymbol !== null) {
const { currentNativeSymbol, nativeSymbols } = assemblyView;
Expand Down Expand Up @@ -583,6 +588,8 @@ export function stateFromLocation(
scrollGeneration: 0,
sourceIndex: null,
highlightedLine: null,
highlightedColumn: null,
isPrettyPrinted: query.sourceViewPretty !== undefined,
};
const assemblyView: AssemblyViewState = {
isOpen: false,
Expand Down
11 changes: 11 additions & 0 deletions src/components/app/BottomBox.css
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
margin: 0;
}

.bottom-box-header-buttons,
.bottom-box-header-trailing-buttons {
display: flex;
height: 100%;
Expand All @@ -106,6 +107,16 @@
background-size: 16px 16px;
}

.bottom-pretty-print-button {
min-width: 24px;
height: 24px;
flex-shrink: 0;
padding: 0 4px;
font-family: monospace;
font-size: 12px;
font-weight: bold;
}

.bottom-prev-button,
.bottom-next-button {
width: 16px;
Expand Down
Loading
Loading