Skip to content
Merged
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
6 changes: 6 additions & 0 deletions locales/en-US/app.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ AppViewRouter--error-from-localhost-url-safari =
this page in { -firefox-brand-name } or Chrome instead.
.title = Safari cannot import local profiles

# This error message is displayed when the profile is in a newer format version
# than this build of the Profiler is able to read.
AppViewRouter--error-profile-version =
This profile uses a format that is not supported by this version of { -profiler-brand-name }.
Try refreshing the page to check if there is an update available for { -profiler-brand-name }.

AppViewRouter--route-not-found--home =
.specialMessage = The URL you tried to reach was not recognized.

Expand Down
6 changes: 6 additions & 0 deletions profiler-cli/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

// These globals are defined via esbuild's define option.
declare const __BUILD_HASH__: string;
declare const __PACKAGE_NAME__: string;
declare const __VERSION__: string;

/**
Expand All @@ -16,6 +17,11 @@ declare const __VERSION__: string;
*/
export const BUILD_HASH = __BUILD_HASH__;

/**
* Package name from profiler-cli/package.json, injected at build time.
*/
export const PACKAGE_NAME = __PACKAGE_NAME__;

/**
* Package version from profiler-cli/package.json, injected at build time.
*/
Expand Down
32 changes: 26 additions & 6 deletions profiler-cli/src/daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as net from 'net';
import * as fs from 'fs';
import { ProfileQuerier } from '../../src/profile-query';
import type { LoadPhase } from '../../src/profile-query/loader';
import { ProfileVersionError } from 'firefox-profiler/profile-logic/errors';
import type {
ClientCommand,
ClientMessage,
Expand All @@ -28,7 +29,27 @@ import {
ensureSessionDir,
} from './session';
import { assertExhaustiveCheck } from 'firefox-profiler/utils/types';
import { BUILD_HASH } from './constants';
import { BUILD_HASH, PACKAGE_NAME } from './constants';

/**
* Build a user-facing message for a profile load failure. When the profile is
* too new for this build, append instructions on how to update the CLI.
*/
function formatProfileLoadError(error: unknown): string {
if (
error instanceof ProfileVersionError ||
(error instanceof Error && error.name === 'ProfileVersionError')
) {
const versionError = error as ProfileVersionError;
return (
`This profile is version ${versionError.profileVersion}, but this profiler-cli only ` +
`supports up to version ${versionError.supportedVersion} of the ${versionError.formatName} profile format.\n` +
`Update to the latest version with:\n` +
` npm install -g ${PACKAGE_NAME}@latest`
);
}
return error instanceof Error ? error.message : String(error);
}

export class Daemon {
private querier: ProfileQuerier | null = null;
Expand All @@ -41,7 +62,7 @@ export class Daemon {
private profilePath: string;
private symbolServerUrl?: string;
private loadPhase: LoadPhase = 'fetching';
private profileLoadError: Error | null = null;
private profileLoadError: string | null = null;

constructor(
sessionDir: string,
Expand Down Expand Up @@ -149,8 +170,7 @@ export class Daemon {
console.log('Profile loaded successfully');
} catch (error) {
console.error(`Failed to load profile: ${error}`);
this.profileLoadError =
error instanceof Error ? error : new Error(String(error));
this.profileLoadError = formatProfileLoadError(error);
}
}

Expand Down Expand Up @@ -210,7 +230,7 @@ export class Daemon {
if (this.profileLoadError) {
return {
type: 'error',
error: `Profile load failed: ${this.profileLoadError.message}`,
error: `Profile load failed: ${this.profileLoadError}`,
};
}
switch (this.loadPhase) {
Expand Down Expand Up @@ -245,7 +265,7 @@ export class Daemon {
if (this.profileLoadError) {
return {
type: 'error',
error: `Profile load failed: ${this.profileLoadError.message}`,
error: `Profile load failed: ${this.profileLoadError}`,
};
}
if (this.loadPhase !== 'ready' || !this.querier) {
Expand Down
3 changes: 2 additions & 1 deletion scripts/build-profiler-cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import esbuild from 'esbuild';
import { chmodSync, readFileSync } from 'fs';
import { nodeBaseConfig } from './lib/esbuild-configs.mjs';

const { version } = JSON.parse(
const { name, version } = JSON.parse(
readFileSync(new URL('../profiler-cli/package.json', import.meta.url), 'utf8')
);

Expand All @@ -22,6 +22,7 @@ const profilerCliConfig = {
},
define: {
__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.
Expand Down
3 changes: 3 additions & 0 deletions src/components/app/AppViewRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ class AppViewRouterImpl extends PureComponent<AppViewRouterProps> {
if (view.error) {
if (view.error.name === 'SafariLocalhostHTTPLoadError') {
message = 'AppViewRouter--error-from-localhost-url-safari';
} else if (view.error.name === 'ProfileVersionError') {
message = 'AppViewRouter--error-profile-version';
additionalMessage = <p>{view.error.toString()}</p>;
} else {
console.error(view.error);
additionalMessage = (
Expand Down
27 changes: 27 additions & 0 deletions src/profile-logic/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,30 @@ export class SymbolsNotFoundError extends Error {
this.errors = errors;
}
}

// Thrown when a profile's format version is newer than the most recent version
// understood by this build. The message is deliberately neutral and only states
// the facts. Consumers (the web app, the CLI) detect this by name and append
// their own advice on how to update, since that advice is frontend-specific.
export class ProfileVersionError extends Error {
formatName: string;
profileVersion: number;
supportedVersion: number;

constructor(
formatName: string,
profileVersion: number,
supportedVersion: number
) {
super(
`Unable to parse a ${formatName} profile of version ${profileVersion}. ` +
`The most recent version understood by this build is version ${supportedVersion}.`
);
// Workaround for a babel issue when extending Errors
(this as any).__proto__ = ProfileVersionError.prototype;
this.name = 'ProfileVersionError';
this.formatName = formatName;
this.profileVersion = profileVersion;
this.supportedVersion = supportedVersion;
}
}
9 changes: 5 additions & 4 deletions src/profile-logic/gecko-profile-versioning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import { StringTable } from '../utils/string-table';
import { GECKO_PROFILE_VERSION } from '../app-logic/constants';
import { ProfileVersionError } from './errors';

// Gecko profiles before version 1 did not have a profile.meta.version field.
// Treat those as version zero.
Expand Down Expand Up @@ -45,10 +46,10 @@ export function upgradeGeckoProfileToCurrentVersion(json: unknown) {
}

if (profileVersion > GECKO_PROFILE_VERSION) {
throw new Error(
`Unable to parse a Gecko profile of version ${profileVersion}, most likely profiler.firefox.com needs to be refreshed. ` +
`The most recent version understood by this version of profiler.firefox.com is version ${GECKO_PROFILE_VERSION}.\n` +
'You can try refreshing this page in case profiler.firefox.com has updated in the meantime.'
throw new ProfileVersionError(
'Gecko',
profileVersion,
GECKO_PROFILE_VERSION
);
}

Expand Down
6 changes: 6 additions & 0 deletions src/profile-logic/process-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { verifyMagic, SIMPLEPERF as SIMPLEPERF_MAGIC } from '../utils/magic';
import { attemptToUpgradeProcessedProfileThroughMutation } from './processed-profile-versioning';
import type { ProfileUpgradeInfo } from './processed-profile-versioning';
import { upgradeGeckoProfileToCurrentVersion } from './gecko-profile-versioning';
import { ProfileVersionError } from './errors';
import {
isPerfScriptFormat,
convertPerfScriptProfile,
Expand Down Expand Up @@ -2313,6 +2314,11 @@ export async function unserializeProfileOfArbitraryFormat(
return processGeckoOrDevToolsProfile(json);
} catch (e) {
console.error('UnserializationError:', e);
// A version mismatch is already a clear, user-facing error. Re-throw it
// as-is so each frontend can detect it and add its own update advice.
if (e instanceof ProfileVersionError) {
throw e;
}
throw new Error(`Unserializing the profile failed: ${e}`);
}
}
Expand Down
9 changes: 5 additions & 4 deletions src/profile-logic/processed-profile-versioning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { ResourceType } from 'firefox-profiler/types';
import { StringTable } from '../utils/string-table';
import { timeCode } from '../utils/time-code';
import { PROCESSED_PROFILE_VERSION } from '../app-logic/constants';
import { ProfileVersionError } from './errors';
import type { Profile } from 'firefox-profiler/types';

export type ProfileUpgradeInfo = {
Expand Down Expand Up @@ -85,10 +86,10 @@ export function attemptToUpgradeProcessedProfileThroughMutation(
}

if (profileVersion > PROCESSED_PROFILE_VERSION) {
throw new Error(
`Unable to parse a processed profile of version ${profileVersion}, most likely profiler.firefox.com needs to be refreshed. ` +
`The most recent version understood by this version of profiler.firefox.com is version ${PROCESSED_PROFILE_VERSION}.\n` +
'You can try refreshing this page in case profiler.firefox.com has updated in the meantime.'
throw new ProfileVersionError(
'processed',
profileVersion,
PROCESSED_PROFILE_VERSION
);
}

Expand Down
Loading