diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000000..e2967ea96a
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,15 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.{json,toml,yml,yaml,css}]
+indent_style = space
+indent_size = 2
+
+[*.{js,ts,jsx,tsx,mjs,cjs}]
+indent_style = space
+indent_size = 2
diff --git a/.github/scripts/add-preview-links.mjs b/.github/scripts/add-preview-links.mjs
new file mode 100644
index 0000000000..dca2777425
--- /dev/null
+++ b/.github/scripts/add-preview-links.mjs
@@ -0,0 +1,286 @@
+import fs from 'fs/promises';
+import { pathToFileURL } from 'url';
+
+export const START_MARKER = '';
+export const END_MARKER = '';
+
+const GITHUB_API_URL = 'https://api.github.com';
+const GITHUB_API_VERSION = '2026-03-10';
+const NETLIFY_HOST_SUFFIX = '--perf-html.netlify.app';
+const MAIN_BASE_URL = `https://main${NETLIFY_HOST_SUFFIX}`;
+const PROFILE_HOST = 'profiler.firefox.com';
+const SHARE_HOST = 'share.firefox.dev';
+
+function escapeRegExp(string) {
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
+
+const PROFILE_URL_HOSTS_REGEXP = [
+ 'share\\.firefox\\.dev',
+ 'profiler\\.firefox\\.com',
+ `[a-z0-9-]+${escapeRegExp(NETLIFY_HOST_SUFFIX)}`,
+].join('|');
+
+export function hasDeployPreviewLink(body) {
+ return (
+ body.includes(START_MARKER) ||
+ /https:\/\/deploy-preview-\d+--perf-html\.netlify\.app(?:\/|\b)/.test(body)
+ );
+}
+
+export function extractIssueNumbers(
+ text,
+ owner = 'firefox-devtools',
+ repo = 'profiler'
+) {
+ const issueNumbers = new Set();
+ const markdownIssueRegExp = /(?:^|[^\w/-])#(\d+)\b/g;
+ const issueUrlRegExp = new RegExp(
+ `https://github\\.com/${escapeRegExp(owner)}/${escapeRegExp(
+ repo
+ )}/(?:issues|pull)/(\\d+)\\b`,
+ 'g'
+ );
+
+ for (const match of text.matchAll(markdownIssueRegExp)) {
+ issueNumbers.add(Number(match[1]));
+ }
+
+ for (const match of text.matchAll(issueUrlRegExp)) {
+ issueNumbers.add(Number(match[1]));
+ }
+
+ return [...issueNumbers];
+}
+
+export function extractProfileUrls(text) {
+ const urls = [];
+ const profileUrlRegExp = new RegExp(
+ `https://(?:${PROFILE_URL_HOSTS_REGEXP})/[^\\s<>)\\]]+`,
+ 'g'
+ );
+
+ for (const match of text.matchAll(profileUrlRegExp)) {
+ // Profile links are often followed by punctuation in prose, for example
+ // "Profile: https://share.firefox.dev/466MJwC.".
+ urls.push(match[0].replace(/[.,;:]+$/, ''));
+ }
+
+ return urls;
+}
+
+export function profileUrlToPath(profileUrl) {
+ const url = new URL(profileUrl);
+
+ if (
+ url.hostname !== PROFILE_HOST &&
+ !url.hostname.endsWith(NETLIFY_HOST_SUFFIX)
+ ) {
+ return null;
+ }
+
+ const path = `${url.pathname}${url.search}${url.hash}`;
+ return path === '/' ? null : path;
+}
+
+// Returns the profile path from profiler.firefox.com, main, or deploy preview
+// URLs, and follows share.firefox.dev redirects to resolve short links.
+export async function resolveProfileUrlToPath(profileUrl, fetchImpl = fetch) {
+ const url = new URL(profileUrl);
+ const path = profileUrlToPath(profileUrl);
+
+ if (path) {
+ return path;
+ }
+
+ if (url.hostname !== SHARE_HOST) {
+ return null;
+ }
+
+ try {
+ const response = await fetchImpl(profileUrl, { redirect: 'follow' });
+ return profileUrlToPath(response.url);
+ } catch (error) {
+ console.warn(`Could not resolve ${profileUrl}: ${error.message}`);
+ return null;
+ }
+}
+
+export function normalizePath(path) {
+ if (!path || path === '/') {
+ return '/';
+ }
+
+ return path.startsWith('/') ? path : `/${path}`;
+}
+
+export function buildPreviewLinks(prNumber, path) {
+ const normalizedPath = normalizePath(path);
+ const previewBaseUrl = `https://deploy-preview-${prNumber}--perf-html.netlify.app`;
+
+ return `[Main](${MAIN_BASE_URL}${normalizedPath}) | [Deploy preview](${previewBaseUrl}${normalizedPath})`;
+}
+
+export function addPreviewLinksToBody(body, previewLinks) {
+ const previewLinksBlock = `${START_MARKER}\n${previewLinks}\n${END_MARKER}`;
+ const trimmedBody = body.trim();
+
+ if (!trimmedBody) {
+ return previewLinksBlock;
+ }
+
+ return `${previewLinksBlock}\n\n${trimmedBody}`;
+}
+
+async function githubRequest(path, token, options = {}) {
+ const response = await fetch(`${GITHUB_API_URL}${path}`, {
+ ...options,
+ headers: {
+ accept: 'application/vnd.github+json',
+ authorization: `Bearer ${token}`,
+ 'content-type': 'application/json',
+ 'user-agent': 'firefox-profiler-preview-links',
+ 'x-github-api-version': GITHUB_API_VERSION,
+ ...options.headers,
+ },
+ });
+
+ if (!response.ok) {
+ const message = await response.text();
+ throw new Error(
+ `GitHub API request failed: ${response.status} ${response.statusText}\n${message}`
+ );
+ }
+
+ if (response.status === 204) {
+ return null;
+ }
+
+ return response.json();
+}
+
+async function getIssueTexts({ owner, repo, issueNumber, token }) {
+ const issue = await githubRequest(
+ `/repos/${owner}/${repo}/issues/${issueNumber}`,
+ token
+ );
+ const comments = await githubRequest(
+ `/repos/${owner}/${repo}/issues/${issueNumber}/comments?per_page=100`,
+ token
+ );
+
+ return [issue.body ?? '', ...comments.map((comment) => comment.body ?? '')];
+}
+
+async function getPullRequest({ owner, repo, pullNumber, token }) {
+ return githubRequest(`/repos/${owner}/${repo}/pulls/${pullNumber}`, token);
+}
+
+// Finds the first profile path mentioned in the PR or its linked issues.
+// Returns '/' if no profile link is found, so generated links use the homepage.
+async function findProfilePath({ owner, repo, pullRequest, token }) {
+ const pullRequestText = `${pullRequest.title ?? ''}\n${pullRequest.body ?? ''}`;
+
+ for (const profileUrl of extractProfileUrls(pullRequestText)) {
+ const path = await resolveProfileUrlToPath(profileUrl);
+
+ if (path) {
+ return path;
+ }
+ }
+
+ for (const issueNumber of extractIssueNumbers(pullRequestText, owner, repo)) {
+ let issueTexts;
+
+ try {
+ issueTexts = await getIssueTexts({ owner, repo, issueNumber, token });
+ } catch (error) {
+ console.warn(`Could not read issue #${issueNumber}: ${error.message}`);
+ continue;
+ }
+
+ for (const issueText of issueTexts) {
+ for (const profileUrl of extractProfileUrls(issueText)) {
+ const path = await resolveProfileUrlToPath(profileUrl);
+
+ if (path) {
+ return path;
+ }
+ }
+ }
+ }
+
+ return '/';
+}
+
+export async function main() {
+ const eventPath = process.env.GITHUB_EVENT_PATH;
+ const repository = process.env.GITHUB_REPOSITORY;
+ const token = process.env.GITHUB_TOKEN;
+
+ if (!eventPath || !repository || !token) {
+ throw new Error(
+ 'GITHUB_EVENT_PATH, GITHUB_REPOSITORY, and GITHUB_TOKEN are required.'
+ );
+ }
+
+ const payload = JSON.parse(await fs.readFile(eventPath, 'utf8'));
+ const pullRequest = payload.pull_request;
+
+ if (!pullRequest) {
+ console.log('This event does not include a pull request. Nothing to do.');
+ return;
+ }
+
+ const [owner, repo] = repository.split('/');
+ const body = pullRequest.body ?? '';
+
+ if (hasDeployPreviewLink(body)) {
+ console.log('The pull request already has deploy preview links.');
+ return;
+ }
+
+ const profilePath = await findProfilePath({
+ owner,
+ repo,
+ pullRequest,
+ token,
+ });
+ const previewLinks = buildPreviewLinks(pullRequest.number, profilePath);
+
+ // Re-read the PR body just before patching it. GitHub's API does not support
+ // conditional PATCH requests here, so this is a best-effort guard against
+ // overwriting edits that happened while this script looked up the profile URL.
+ const latestPullRequest = await getPullRequest({
+ owner,
+ repo,
+ pullNumber: pullRequest.number,
+ token,
+ });
+ const latestBody = latestPullRequest.body ?? '';
+
+ if (hasDeployPreviewLink(latestBody)) {
+ console.log('The pull request already has deploy preview links.');
+ return;
+ }
+
+ const updatedBody = addPreviewLinksToBody(latestBody, previewLinks);
+
+ await githubRequest(
+ `/repos/${owner}/${repo}/pulls/${pullRequest.number}`,
+ token,
+ {
+ body: JSON.stringify({ body: updatedBody }),
+ method: 'PATCH',
+ }
+ );
+
+ console.log(`Added preview links to pull request #${pullRequest.number}.`);
+}
+
+if (
+ process.argv[1] &&
+ import.meta.url === pathToFileURL(process.argv[1]).href
+) {
+ await main();
+}
diff --git a/.github/scripts/add-preview-links.test.mjs b/.github/scripts/add-preview-links.test.mjs
new file mode 100644
index 0000000000..521ee17149
--- /dev/null
+++ b/.github/scripts/add-preview-links.test.mjs
@@ -0,0 +1,171 @@
+import assert from 'assert/strict';
+import { test } from 'node:test';
+
+import {
+ END_MARKER,
+ START_MARKER,
+ addPreviewLinksToBody,
+ buildPreviewLinks,
+ extractIssueNumbers,
+ extractProfileUrls,
+ hasDeployPreviewLink,
+ normalizePath,
+ profileUrlToPath,
+ resolveProfileUrlToPath,
+} from './add-preview-links.mjs';
+
+test('hasDeployPreviewLink detects generated and manual preview links', () => {
+ assert.equal(hasDeployPreviewLink('No preview links here.'), false);
+ assert.equal(
+ hasDeployPreviewLink(
+ '[Deploy preview](https://deploy-preview-6083--perf-html.netlify.app/)'
+ ),
+ true
+ );
+ assert.equal(
+ hasDeployPreviewLink(`${START_MARKER}\nlinks\n${END_MARKER}`),
+ true
+ );
+});
+
+test('extractIssueNumbers finds markdown references and issue URLs', () => {
+ const text = [
+ 'Fixes #5598.',
+ 'See https://github.com/firefox-devtools/profiler/issues/6083.',
+ 'Follow-up to https://github.com/firefox-devtools/profiler/pull/6017.',
+ 'Duplicate #5598.',
+ ].join(' ');
+
+ assert.deepEqual(extractIssueNumbers(text), [5598, 6083, 6017]);
+});
+
+test('extractProfileUrls finds profiler, share, and preview URLs', () => {
+ assert.deepEqual(
+ extractProfileUrls('Profile: https://share.firefox.dev/466MJwC.'),
+ ['https://share.firefox.dev/466MJwC']
+ );
+ assert.deepEqual(
+ extractProfileUrls(
+ '[Profile](https://profiler.firefox.com/public/abc/calltree/?thread=1&v=16)'
+ ),
+ ['https://profiler.firefox.com/public/abc/calltree/?thread=1&v=16']
+ );
+ const previewLinks = [
+ '[Main](https://main--perf-html.netlify.app/public/abc/)',
+ '[Deploy preview](https://deploy-preview-6017--perf-html.netlify.app/public/abc/)',
+ ].join(' | ');
+
+ assert.deepEqual(extractProfileUrls(previewLinks), [
+ 'https://main--perf-html.netlify.app/public/abc/',
+ 'https://deploy-preview-6017--perf-html.netlify.app/public/abc/',
+ ]);
+});
+
+test('profileUrlToPath keeps the profiler path, query, and hash', () => {
+ assert.equal(
+ profileUrlToPath(
+ 'https://profiler.firefox.com/public/abc/flame-graph/?thread=1&v=16#hash'
+ ),
+ '/public/abc/flame-graph/?thread=1&v=16#hash'
+ );
+ assert.equal(profileUrlToPath('https://profiler.firefox.com/'), null);
+ assert.equal(
+ profileUrlToPath('https://main--perf-html.netlify.app/public/abc/'),
+ '/public/abc/'
+ );
+ assert.equal(
+ profileUrlToPath(
+ 'https://deploy-preview-6017--perf-html.netlify.app/public/abc/'
+ ),
+ '/public/abc/'
+ );
+});
+
+test('resolveProfileUrlToPath follows share.firefox.dev redirects', async () => {
+ const fetchImpl = async () => ({
+ url: 'https://profiler.firefox.com/public/abc/marker-table/?thread=0&v=16',
+ });
+
+ assert.equal(
+ await resolveProfileUrlToPath(
+ 'https://share.firefox.dev/466MJwC',
+ fetchImpl
+ ),
+ '/public/abc/marker-table/?thread=0&v=16'
+ );
+});
+
+test('resolveProfileUrlToPath handles main and deploy preview links', async () => {
+ assert.equal(
+ await resolveProfileUrlToPath(
+ 'https://main--perf-html.netlify.app/public/abc/?v=16'
+ ),
+ '/public/abc/?v=16'
+ );
+ assert.equal(
+ await resolveProfileUrlToPath(
+ 'https://deploy-preview-6017--perf-html.netlify.app/public/abc/?v=16'
+ ),
+ '/public/abc/?v=16'
+ );
+});
+
+test('resolveProfileUrlToPath ignores broken share.firefox.dev links', async () => {
+ const unresolvedFetchImpl = async () => ({
+ url: 'https://share.firefox.dev/nonsense',
+ });
+ const failingFetchImpl = async () => {
+ throw new Error('Network error');
+ };
+
+ assert.equal(
+ await resolveProfileUrlToPath(
+ 'https://share.firefox.dev/nonsense',
+ unresolvedFetchImpl
+ ),
+ null
+ );
+
+ const originalWarn = console.warn;
+
+ try {
+ console.warn = () => {};
+ assert.equal(
+ await resolveProfileUrlToPath(
+ 'https://share.firefox.dev/nonsense',
+ failingFetchImpl
+ ),
+ null
+ );
+ } finally {
+ console.warn = originalWarn;
+ }
+});
+
+test('buildPreviewLinks uses the main branch and deploy preview hosts', () => {
+ const path = '/public/abc/marker-table/?thread=0&v=16';
+ const mainUrl = `https://main--perf-html.netlify.app${path}`;
+ const previewUrl = `https://deploy-preview-6083--perf-html.netlify.app${path}`;
+
+ assert.equal(normalizePath('public/abc'), '/public/abc');
+ assert.equal(
+ buildPreviewLinks(6083, path),
+ `[Main](${mainUrl}) | [Deploy preview](${previewUrl})`
+ );
+});
+
+test('addPreviewLinksToBody prepends a marked block', () => {
+ assert.equal(
+ addPreviewLinksToBody(
+ 'Fixes #5598.',
+ '[Main](main) | [Deploy preview](preview)'
+ ),
+ [
+ START_MARKER,
+ '[Main](main) | [Deploy preview](preview)',
+ END_MARKER,
+ '',
+ 'Fixes #5598.',
+ ].join('\n')
+ );
+});
diff --git a/.github/workflows/add-preview-links.yml b/.github/workflows/add-preview-links.yml
new file mode 100644
index 0000000000..de5640178f
--- /dev/null
+++ b/.github/workflows/add-preview-links.yml
@@ -0,0 +1,37 @@
+name: Add preview links
+
+on:
+ pull_request_target:
+ types:
+ - opened
+ - edited
+ - reopened
+ - ready_for_review
+
+permissions:
+ contents: read
+ issues: read
+ pull-requests: write
+
+concurrency:
+ group: add-preview-links-${{ github.event.pull_request.number }}
+ cancel-in-progress: false
+
+jobs:
+ add-preview-links:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v5
+ with:
+ persist-credentials: false
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: '24'
+
+ - name: Add preview links
+ run: node .github/scripts/add-preview-links.mjs
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/docs-developer/CHANGELOG-formats.md b/docs-developer/CHANGELOG-formats.md
index b59d5fe0e3..7a3c44138a 100644
--- a/docs-developer/CHANGELOG-formats.md
+++ b/docs-developer/CHANGELOG-formats.md
@@ -6,6 +6,19 @@ Note that this is not an exhaustive list. Processed profile format upgraders can
## Processed profile format
+### Version 66
+
+The `prefix` column of `profile.shared.stackTable` was replaced with a `prefixOffset` column, to improve compressibility.
+For profiles loaded from [JsonSlabs](https://github.com/mstange/json-slabs/) files (.jslb, .jslb.gz),
+`profile.shared.stackTable.prefixOffset` can now optionally be stored as an `Int32Array`. Regular JS / JSON arrays are still accepted.
+
+The `prefixOffset` values have the following meanings:
+
+- `prefixOffset[i] === 0` means: stack `i` is a root.
+- Otherwise, `prefixOffset[i] === k` (k > 0) means: `i`'s parent has index `i - k`.
+
+Parents always come before their children in the stack table (the stack table is stored in topological order), so these offsets are always positive and always point backwards.
+
### Version 65
The stack table's `frame` column (stored at `profile.shared.stackTable.frame`) can now optionally be stored as an `Int32Array`, for profiles loaded from [JsonSlabs](https://github.com/mstange/json-slabs/) files (.jslb, .jslb.gz). Regular JS / JSON arrays are still accepted.
diff --git a/locales/be/app.ftl b/locales/be/app.ftl
index 454393e848..138e9642e5 100644
--- a/locales/be/app.ftl
+++ b/locales/be/app.ftl
@@ -334,12 +334,6 @@ Home--chrome-extension-recording-instructions =
на панэлі інструментаў або цэтлікі для запуску і спынення прафілявання.
Вы таксама можаце экспартаваць профілі і загрузіць іх тут для аналізу.
-## IdleSearchField
-## The component that is used for all the search inputs in the application.
-
-IdleSearchField--search-input =
- .placeholder = Увядзіце ўмовы фільтру
-
## JsTracerSettings
## JSTracer is an experimental feature and it's currently disabled. See Bug 1565788.
diff --git a/locales/de/app.ftl b/locales/de/app.ftl
index 178ac76272..c2092387b0 100644
--- a/locales/de/app.ftl
+++ b/locales/de/app.ftl
@@ -374,8 +374,11 @@ Home--chrome-extension-recording-instructions =
## IdleSearchField
## The component that is used for all the search inputs in the application.
-IdleSearchField--search-input =
- .placeholder = Filterbegriffe eingeben
+# `/` here overrides Firefox's Type Ahead Find shortcut, which would
+# otherwise trigger an unhelpful find bar on top of the profiler UI.
+# The shortcut itself is not localizable.
+IdleSearchField--search-input2 =
+ .placeholder = Filterbegriffe eingeben (/)
## JsTracerSettings
## JSTracer is an experimental feature and it's currently disabled. See Bug 1565788.
diff --git a/locales/el/app.ftl b/locales/el/app.ftl
index d3df514dce..8c7549cd00 100644
--- a/locales/el/app.ftl
+++ b/locales/el/app.ftl
@@ -393,8 +393,11 @@ Home--chrome-extension-recording-instructions =
## IdleSearchField
## The component that is used for all the search inputs in the application.
-IdleSearchField--search-input =
- .placeholder = Εισαγάγετε όρους φίλτρου
+# `/` here overrides Firefox's Type Ahead Find shortcut, which would
+# otherwise trigger an unhelpful find bar on top of the profiler UI.
+# The shortcut itself is not localizable.
+IdleSearchField--search-input2 =
+ .placeholder = Εισαγάγετε όρους φίλτρου (/)
## JsTracerSettings
## JSTracer is an experimental feature and it's currently disabled. See Bug 1565788.
diff --git a/locales/en-CA/app.ftl b/locales/en-CA/app.ftl
index bcd9d98ebb..77f71ee4fa 100644
--- a/locales/en-CA/app.ftl
+++ b/locales/en-CA/app.ftl
@@ -390,12 +390,6 @@ Home--chrome-extension-recording-instructions =
toolbar icon or the shortcuts to start and stop profiling. You can also
export profiles and load them here for detailed analysis.
-## IdleSearchField
-## The component that is used for all the search inputs in the application.
-
-IdleSearchField--search-input =
- .placeholder = Enter filter terms
-
## JsTracerSettings
## JSTracer is an experimental feature and it's currently disabled. See Bug 1565788.
diff --git a/locales/en-GB/app.ftl b/locales/en-GB/app.ftl
index e40039d84c..4462bccd30 100644
--- a/locales/en-GB/app.ftl
+++ b/locales/en-GB/app.ftl
@@ -398,8 +398,11 @@ Home--chrome-extension-recording-instructions =
## IdleSearchField
## The component that is used for all the search inputs in the application.
-IdleSearchField--search-input =
- .placeholder = Enter filter terms
+# `/` here overrides Firefox's Type Ahead Find shortcut, which would
+# otherwise trigger an unhelpful find bar on top of the profiler UI.
+# The shortcut itself is not localizable.
+IdleSearchField--search-input2 =
+ .placeholder = Enter filter terms (/)
## JsTracerSettings
## JSTracer is an experimental feature and it's currently disabled. See Bug 1565788.
diff --git a/locales/en-US/app.ftl b/locales/en-US/app.ftl
index 86409569e7..3697baf81e 100644
--- a/locales/en-US/app.ftl
+++ b/locales/en-US/app.ftl
@@ -428,8 +428,11 @@ Home--chrome-extension-recording-instructions = Once installed, use the extensio
## IdleSearchField
## The component that is used for all the search inputs in the application.
-IdleSearchField--search-input =
- .placeholder = Enter filter terms
+# `/` here overrides Firefox's Type Ahead Find shortcut, which would
+# otherwise trigger an unhelpful find bar on top of the profiler UI.
+# The shortcut itself is not localizable.
+IdleSearchField--search-input2 =
+ .placeholder = Enter filter terms (/)
## JsTracerSettings
## JSTracer is an experimental feature and it's currently disabled. See Bug 1565788.
diff --git a/locales/es-CL/app.ftl b/locales/es-CL/app.ftl
index 7591ba2890..e4f3359d09 100644
--- a/locales/es-CL/app.ftl
+++ b/locales/es-CL/app.ftl
@@ -322,8 +322,11 @@ Home--chrome-extension-recording-instructions = Una vez instalada, utiliza el ic
## IdleSearchField
## The component that is used for all the search inputs in the application.
-IdleSearchField--search-input =
- .placeholder = Ingresa los términos de filtro
+# `/` here overrides Firefox's Type Ahead Find shortcut, which would
+# otherwise trigger an unhelpful find bar on top of the profiler UI.
+# The shortcut itself is not localizable.
+IdleSearchField--search-input2 =
+ .placeholder = Ingresa los términos de filtro (/)
## JsTracerSettings
## JSTracer is an experimental feature and it's currently disabled. See Bug 1565788.
diff --git a/locales/fr/app.ftl b/locales/fr/app.ftl
index 4ac9c5e908..936a69c120 100644
--- a/locales/fr/app.ftl
+++ b/locales/fr/app.ftl
@@ -311,8 +311,11 @@ Home--chrome-extension-recording-instructions = Une fois l’extension installé
## IdleSearchField
## The component that is used for all the search inputs in the application.
-IdleSearchField--search-input =
- .placeholder = Saisissez le filtre
+# `/` here overrides Firefox's Type Ahead Find shortcut, which would
+# otherwise trigger an unhelpful find bar on top of the profiler UI.
+# The shortcut itself is not localizable.
+IdleSearchField--search-input2 =
+ .placeholder = Saisissez le filtre (/)
## JsTracerSettings
## JSTracer is an experimental feature and it's currently disabled. See Bug 1565788.
diff --git a/locales/fur/app.ftl b/locales/fur/app.ftl
index 3389b02751..23f4bf5b6b 100644
--- a/locales/fur/app.ftl
+++ b/locales/fur/app.ftl
@@ -334,12 +334,6 @@ Home--chrome-extension-instructions =
par tirâ dongje i profîi des prestazions in Chrome e analizâju in { -profiler-brand-name }. Instale la estension dal Chrome Web Store.
Home--chrome-extension-recording-instructions = Une volte instalade, dopre la icone de estension te sbare dai struments o lis scurtis par inviâe interompi la profilazion. Tu puedis ancje espuartâ i profîi e cjariâju achì par fâ une analisi detaiade.
-## IdleSearchField
-## The component that is used for all the search inputs in the application.
-
-IdleSearchField--search-input =
- .placeholder = Inserìs i tiermins di cirî
-
## JsTracerSettings
## JSTracer is an experimental feature and it's currently disabled. See Bug 1565788.
diff --git a/locales/fy-NL/app.ftl b/locales/fy-NL/app.ftl
index 2fbae9169a..634ddd686a 100644
--- a/locales/fy-NL/app.ftl
+++ b/locales/fy-NL/app.ftl
@@ -398,8 +398,11 @@ Home--chrome-extension-recording-instructions =
## IdleSearchField
## The component that is used for all the search inputs in the application.
-IdleSearchField--search-input =
- .placeholder = Fier filtertermen yn
+# `/` here overrides Firefox's Type Ahead Find shortcut, which would
+# otherwise trigger an unhelpful find bar on top of the profiler UI.
+# The shortcut itself is not localizable.
+IdleSearchField--search-input2 =
+ .placeholder = Fier filtertermen yn (/)
## JsTracerSettings
## JSTracer is an experimental feature and it's currently disabled. See Bug 1565788.
diff --git a/locales/ia/app.ftl b/locales/ia/app.ftl
index fefc4679d5..ffabe2385f 100644
--- a/locales/ia/app.ftl
+++ b/locales/ia/app.ftl
@@ -384,8 +384,11 @@ Home--chrome-extension-recording-instructions = Un vice installate, usar le icon
## IdleSearchField
## The component that is used for all the search inputs in the application.
-IdleSearchField--search-input =
- .placeholder = Insere terminos del filtro
+# `/` here overrides Firefox's Type Ahead Find shortcut, which would
+# otherwise trigger an unhelpful find bar on top of the profiler UI.
+# The shortcut itself is not localizable.
+IdleSearchField--search-input2 =
+ .placeholder = Insere terminos del filtro (/)
## JsTracerSettings
## JSTracer is an experimental feature and it's currently disabled. See Bug 1565788.
diff --git a/locales/it/app.ftl b/locales/it/app.ftl
index 29b5ef0bc8..37ce133216 100644
--- a/locales/it/app.ftl
+++ b/locales/it/app.ftl
@@ -304,8 +304,11 @@ Home--chrome-extension-recording-instructions = Una volta installata, utilizza l
## IdleSearchField
## The component that is used for all the search inputs in the application.
-IdleSearchField--search-input =
- .placeholder = Inserisci i termini da cercare
+# `/` here overrides Firefox's Type Ahead Find shortcut, which would
+# otherwise trigger an unhelpful find bar on top of the profiler UI.
+# The shortcut itself is not localizable.
+IdleSearchField--search-input2 =
+ .placeholder = Inserisci i termini da filtrare (/)
## JsTracerSettings
## JSTracer is an experimental feature and it's currently disabled. See Bug 1565788.
diff --git a/locales/kab/app.ftl b/locales/kab/app.ftl
index 247331155b..fe261aadf4 100644
--- a/locales/kab/app.ftl
+++ b/locales/kab/app.ftl
@@ -134,12 +134,6 @@ Home--compare-recordings-info = Tzemreḍ daɣen ad tsenmehleḍ iseklasen. L
Home--your-recent-uploaded-recordings-title = Iseklasen-ik·im i d-yulin melmi kan
Home--install-chrome-extension = Sbedd aseɣẓan n Chrome
-## IdleSearchField
-## The component that is used for all the search inputs in the application.
-
-IdleSearchField--search-input =
- .placeholder = Sekcem awalen n yimsizdeg
-
## ListOfPublishedProfiles
## This is the component that displays all the profiles the user has uploaded.
## It's displayed both in the homepage and in the uploaded recordings page.
diff --git a/locales/nl/app.ftl b/locales/nl/app.ftl
index 57a9fd737e..e9e65fbada 100644
--- a/locales/nl/app.ftl
+++ b/locales/nl/app.ftl
@@ -398,8 +398,11 @@ Home--chrome-extension-recording-instructions =
## IdleSearchField
## The component that is used for all the search inputs in the application.
-IdleSearchField--search-input =
- .placeholder = Voer filtertermen in
+# `/` here overrides Firefox's Type Ahead Find shortcut, which would
+# otherwise trigger an unhelpful find bar on top of the profiler UI.
+# The shortcut itself is not localizable.
+IdleSearchField--search-input2 =
+ .placeholder = Voer filtertermen in (/)
## JsTracerSettings
## JSTracer is an experimental feature and it's currently disabled. See Bug 1565788.
diff --git a/locales/pt-BR/app.ftl b/locales/pt-BR/app.ftl
index 3ed870825e..052e2b753c 100644
--- a/locales/pt-BR/app.ftl
+++ b/locales/pt-BR/app.ftl
@@ -295,12 +295,6 @@ Home--install-chrome-extension = Instale a extensão para Chrome
Home--chrome-extension-instructions = Use a extensão { -profiler-brand-name } para Chrome para capturar profiles de desempenho no Chrome e analisar no { -profiler-brand-name }. Instale a extensão a partir do Chrome Web Store.
Home--chrome-extension-recording-instructions = Após instalar, use o ícone da extensão na barra de ferramentas ou os atalhos para iniciar e encerrar a gravação de profiles. Você também pode exportar profiles e carregar aqui para análises detalhadas.
-## IdleSearchField
-## The component that is used for all the search inputs in the application.
-
-IdleSearchField--search-input =
- .placeholder = Insira termos de filtragem
-
## JsTracerSettings
## JSTracer is an experimental feature and it's currently disabled. See Bug 1565788.
diff --git a/locales/ru/app.ftl b/locales/ru/app.ftl
index c30a2ab90b..961a2c5087 100644
--- a/locales/ru/app.ftl
+++ b/locales/ru/app.ftl
@@ -387,8 +387,11 @@ Home--chrome-extension-recording-instructions =
## IdleSearchField
## The component that is used for all the search inputs in the application.
-IdleSearchField--search-input =
- .placeholder = Введите условия фильтра
+# `/` here overrides Firefox's Type Ahead Find shortcut, which would
+# otherwise trigger an unhelpful find bar on top of the profiler UI.
+# The shortcut itself is not localizable.
+IdleSearchField--search-input2 =
+ .placeholder = Введите условия фильтра (/)
## JsTracerSettings
## JSTracer is an experimental feature and it's currently disabled. See Bug 1565788.
diff --git a/locales/sr/app.ftl b/locales/sr/app.ftl
index aa5b65e609..dfa3d922c5 100644
--- a/locales/sr/app.ftl
+++ b/locales/sr/app.ftl
@@ -350,12 +350,6 @@ Home--chrome-extension-instructions =
{ -profiler-brand-name }. Инсталирајте додатак са Chrome Web Store-а.
Home--chrome-extension-recording-instructions = Када га инсталирате, употребите иконицу додатка на алатној траци или пречице да бисте покренули и зауставили профилисање. Такође можете да извезете профиле и учитате их овде за детаљну анализу.
-## IdleSearchField
-## The component that is used for all the search inputs in the application.
-
-IdleSearchField--search-input =
- .placeholder = Унесите појмове за филтрирање
-
## JsTracerSettings
## JSTracer is an experimental feature and it's currently disabled. See Bug 1565788.
diff --git a/locales/sv-SE/app.ftl b/locales/sv-SE/app.ftl
index 1799a97736..2c8825954a 100644
--- a/locales/sv-SE/app.ftl
+++ b/locales/sv-SE/app.ftl
@@ -390,12 +390,6 @@ Home--chrome-extension-recording-instructions =
eller genvägarna för att starta och stoppa profilering.
Du kan också exportera profiler och ladda dem här för detaljerad analys.
-## IdleSearchField
-## The component that is used for all the search inputs in the application.
-
-IdleSearchField--search-input =
- .placeholder = Ange filtervillkor
-
## JsTracerSettings
## JSTracer is an experimental feature and it's currently disabled. See Bug 1565788.
diff --git a/locales/tr/app.ftl b/locales/tr/app.ftl
index 091d0a11e5..e408776c6d 100644
--- a/locales/tr/app.ftl
+++ b/locales/tr/app.ftl
@@ -318,8 +318,11 @@ Home--chrome-extension-recording-instructions =
## IdleSearchField
## The component that is used for all the search inputs in the application.
-IdleSearchField--search-input =
- .placeholder = Filtre terimlerini girin
+# `/` here overrides Firefox's Type Ahead Find shortcut, which would
+# otherwise trigger an unhelpful find bar on top of the profiler UI.
+# The shortcut itself is not localizable.
+IdleSearchField--search-input2 =
+ .placeholder = Filtre terimlerini girin (/)
## JsTracerSettings
## JSTracer is an experimental feature and it's currently disabled. See Bug 1565788.
diff --git a/locales/uk/app.ftl b/locales/uk/app.ftl
index 09a4c07f48..5cee564a75 100644
--- a/locales/uk/app.ftl
+++ b/locales/uk/app.ftl
@@ -332,12 +332,6 @@ Home--chrome-extension-recording-instructions =
або ярлики для запуску та зупинки профілювання. Ви також можете
експортувати профілі та завантажити їх тут для детального аналізу.
-## IdleSearchField
-## The component that is used for all the search inputs in the application.
-
-IdleSearchField--search-input =
- .placeholder = Введіть умови фільтру
-
## JsTracerSettings
## JSTracer is an experimental feature and it's currently disabled. See Bug 1565788.
diff --git a/locales/zh-CN/app.ftl b/locales/zh-CN/app.ftl
index 1906673324..b757cd7003 100644
--- a/locales/zh-CN/app.ftl
+++ b/locales/zh-CN/app.ftl
@@ -297,12 +297,6 @@ Home--install-chrome-extension = 安装 Chrome 扩展
Home--chrome-extension-instructions = 使用 Chrome 版 { -profiler-brand-name } 扩展,在 Chrome 中捕捉性能分析记录,并通过 { -profiler-brand-name } 分析。可到 Chrome 应用商店安装扩展。
Home--chrome-extension-recording-instructions = 安装后,即可使用扩展的工具栏图标和快捷键来开始或停止分析,也可以导出分析记录并在此处加载以进行详细分析。
-## IdleSearchField
-## The component that is used for all the search inputs in the application.
-
-IdleSearchField--search-input =
- .placeholder = 输入过滤条件
-
## JsTracerSettings
## JSTracer is an experimental feature and it's currently disabled. See Bug 1565788.
diff --git a/locales/zh-TW/app.ftl b/locales/zh-TW/app.ftl
index 6f3fb97161..e22ef369b4 100644
--- a/locales/zh-TW/app.ftl
+++ b/locales/zh-TW/app.ftl
@@ -302,8 +302,11 @@ Home--chrome-extension-recording-instructions = 安裝完成後,即可使用
## IdleSearchField
## The component that is used for all the search inputs in the application.
-IdleSearchField--search-input =
- .placeholder = 輸入過濾條件
+# `/` here overrides Firefox's Type Ahead Find shortcut, which would
+# otherwise trigger an unhelpful find bar on top of the profiler UI.
+# The shortcut itself is not localizable.
+IdleSearchField--search-input2 =
+ .placeholder = 輸入過濾條件(/)
## JsTracerSettings
## JSTracer is an experimental feature and it's currently disabled. See Bug 1565788.
diff --git a/package.json b/package.json
index 96e20725f8..0b117af500 100644
--- a/package.json
+++ b/package.json
@@ -68,8 +68,8 @@
"@codemirror/lang-cpp": "^6.0.3",
"@codemirror/lang-javascript": "^6.2.5",
"@codemirror/lang-rust": "^6.0.2",
- "@codemirror/language": "^6.12.3",
- "@codemirror/state": "^6.6.0",
+ "@codemirror/language": "^6.12.4",
+ "@codemirror/state": "^6.7.0",
"@codemirror/view": "^6.43.1",
"@firefox-devtools/react-contextmenu": "^5.2.4",
"@fluent/bundle": "^0.19.1",
@@ -78,7 +78,7 @@
"@lezer/highlight": "^1.2.3",
"@lezer/javascript": "^1.5.4",
"@streamparser/json": "^0.0.22",
- "@tgwf/co2": "^0.18.0",
+ "@tgwf/co2": "^0.19.0",
"array-move": "^3.0.1",
"array-range": "^1.0.1",
"clamp": "^1.0.1",
@@ -101,7 +101,7 @@
"namedtuplemap": "^1.0.0",
"photon-colors": "^3.3.2",
"protobufjs": "^8.6.5",
- "query-string": "^9.4.0",
+ "query-string": "^9.4.1",
"react": "~19.1.8",
"react-dom": "~19.1.8",
"react-intersection-observer": "^10.0.3",
@@ -110,10 +110,10 @@
"redux-logger": "^3.0.6",
"redux-thunk": "^3.1.0",
"reselect": "^4.1.8",
- "smol-toml": "^1.6.1",
+ "smol-toml": "^1.7.0",
"source-map": "^0.7.6",
"url": "^0.11.4",
- "valibot": "^1.4.1",
+ "valibot": "^1.4.2",
"workbox-window": "^7.4.1"
},
"devDependencies": {
@@ -136,12 +136,12 @@
"@types/react-dom": "^19.2.3",
"@types/redux-logger": "^3.0.6",
"@types/tgwf__co2": "^0.14.2",
- "@typescript-eslint/eslint-plugin": "^8.60.0",
- "@typescript-eslint/parser": "^8.60.0",
+ "@typescript-eslint/eslint-plugin": "^8.62.1",
+ "@typescript-eslint/parser": "^8.62.1",
"alex": "^11.0.1",
"babel-jest": "^30.4.1",
"babel-plugin-module-resolver": "^5.0.3",
- "browserslist": "^4.28.2",
+ "browserslist": "^4.28.4",
"browserslist-to-esbuild": "^2.1.1",
"caniuse-lite": "^1.0.30001799",
"cross-env": "^10.1.0",
@@ -153,14 +153,14 @@
"eslint": "^9.39.4",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-import": "^2.32.0",
- "eslint-plugin-jest": "^29.15.2",
+ "eslint-plugin-jest": "^29.15.4",
"eslint-plugin-jest-dom": "^5.5.0",
"eslint-plugin-jest-formatting": "^3.1.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-testing-library": "^7.16.2",
"fake-indexeddb": "^6.2.5",
"fetch-mock": "^12.6.0",
- "globals": "^17.6.0",
+ "globals": "^17.7.0",
"husky": "^4.3.8",
"jest": "^30.4.2",
"jest-environment-jsdom": "^30.4.1",
@@ -171,14 +171,14 @@
"open": "^11.0.0",
"oxfmt": "^0.56.0",
"patch-package": "^8.0.1",
- "postcss": "^8.5.15",
+ "postcss": "^8.5.16",
"postinstall-postinstall": "^2.1.0",
"rimraf": "^6.1.3",
- "stylelint": "^17.13.0",
+ "stylelint": "^17.14.0",
"stylelint-config-idiomatic-order": "^10.0.0",
"stylelint-config-standard": "^40.0.0",
"typescript": "^6.0.3",
- "typescript-eslint": "^8.60.0",
+ "typescript-eslint": "^8.62.1",
"workbox-cli": "^7.4.1",
"yargs": "^18.0.0"
},
diff --git a/profiler-cli/CONTRIBUTING.md b/profiler-cli/CONTRIBUTING.md
index 4d127dcf9f..c8f59a3bdd 100644
--- a/profiler-cli/CONTRIBUTING.md
+++ b/profiler-cli/CONTRIBUTING.md
@@ -139,9 +139,7 @@ export type AllocationInfoResult = {
```typescript
import { Command } from 'commander';
-import { sendCommand } from '../client';
-import { addGlobalOptions } from './shared';
-import { formatOutput } from '../output';
+import { addGlobalOptions, runCommand } from './shared';
export function registerAllocationCommand(
program: Command,
@@ -157,12 +155,11 @@ export function registerAllocationCommand(
.description('Show allocation summary')
.option('--thread ', 'Thread to query')
).action(async (opts) => {
- const result = await sendCommand(
+ await runCommand(
sessionDir,
{ command: 'allocation', subcommand: 'info', thread: opts.thread },
- opts.session
+ opts
);
- console.log(formatOutput(result, opts.json ?? false));
});
}
```
@@ -175,7 +172,7 @@ Add a case to `processMessage()`:
case 'allocation':
switch (command.subcommand) {
case 'info':
- return this.querier!.allocationInfo(command.thread);
+ return this.querier.allocationInfo(command.thread);
default:
throw assertExhaustiveCheck(command);
}
diff --git a/profiler-cli/package.json b/profiler-cli/package.json
index e5dd4fbae9..0216d18372 100644
--- a/profiler-cli/package.json
+++ b/profiler-cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@firefox-devtools/profiler-cli",
- "version": "0.3.0",
+ "version": "0.4.0",
"description": "Command-line interface for querying Firefox Profiler profiles with persistent daemon sessions",
"scripts": {
"prepublishOnly": "node ../scripts/verify-profiler-cli-build.mjs"
diff --git a/profiler-cli/src/commands/counter.ts b/profiler-cli/src/commands/counter.ts
index 15a8e5d9a5..44c267434f 100644
--- a/profiler-cli/src/commands/counter.ts
+++ b/profiler-cli/src/commands/counter.ts
@@ -7,9 +7,7 @@
*/
import type { Command } from 'commander';
-import { sendCommand } from '../client';
-import { formatOutput } from '../output';
-import { addGlobalOptions } from './shared';
+import { addGlobalOptions, runCommand } from './shared';
export function registerCounterCommand(
program: Command,
@@ -24,12 +22,11 @@ export function registerCounterCommand(
.command('list')
.description('List all counters with one-line summaries')
).action(async (opts) => {
- const result = await sendCommand(
+ await runCommand(
sessionDir,
{ command: 'counter', subcommand: 'list' },
- opts.session
+ opts
);
- console.log(formatOutput(result, opts.json ?? false));
});
addGlobalOptions(
@@ -39,11 +36,10 @@ export function registerCounterCommand(
.option('--counter ', 'Counter handle')
).action(async (handleArg: string | undefined, opts) => {
const counterHandle = handleArg ?? opts.counter;
- const result = await sendCommand(
+ await runCommand(
sessionDir,
{ command: 'counter', subcommand: 'info', counter: counterHandle },
- opts.session
+ opts
);
- console.log(formatOutput(result, opts.json ?? false));
});
}
diff --git a/profiler-cli/src/commands/filter.ts b/profiler-cli/src/commands/filter.ts
index c3d7f862e7..d5ca65d51e 100644
--- a/profiler-cli/src/commands/filter.ts
+++ b/profiler-cli/src/commands/filter.ts
@@ -7,13 +7,12 @@
*/
import type { Command } from 'commander';
-import { sendCommand } from '../client';
-import { formatOutput } from '../output';
import { parseFilterSpec } from '../utils/parse';
import {
addGlobalOptions,
addSampleFilterOptions,
parseIntArg,
+ runCommand,
wasExplicit,
} from './shared';
@@ -48,12 +47,11 @@ export function registerFilterCommand(
outsideMarker: opts.outsideMarker,
search: opts.search,
});
- const result = await sendCommand(
+ await runCommand(
sessionDir,
{ command: 'filter', subcommand: 'push', thread: opts.thread, spec },
- opts.session
+ opts
);
- console.log(formatOutput(result, opts.json ?? false));
});
addGlobalOptions(
@@ -70,12 +68,11 @@ export function registerFilterCommand(
1,
'Error: count must be a positive integer'
);
- const result = await sendCommand(
+ await runCommand(
sessionDir,
{ command: 'filter', subcommand: 'pop', thread: opts.thread, count },
- opts.session
+ opts
);
- console.log(formatOutput(result, opts.json ?? false));
});
addGlobalOptions(
@@ -84,12 +81,11 @@ export function registerFilterCommand(
.description('List active filters for current thread')
.option('--thread ', 'Thread handle')
).action(async (opts) => {
- const result = await sendCommand(
+ await runCommand(
sessionDir,
{ command: 'filter', subcommand: 'list', thread: opts.thread },
- opts.session
+ opts
);
- console.log(formatOutput(result, opts.json ?? false));
if (!wasExplicit('filter', 'list')) {
console.log(
@@ -104,11 +100,10 @@ export function registerFilterCommand(
.description('Remove all filters for current thread')
.option('--thread ', 'Thread handle')
).action(async (opts) => {
- const result = await sendCommand(
+ await runCommand(
sessionDir,
{ command: 'filter', subcommand: 'clear', thread: opts.thread },
- opts.session
+ opts
);
- console.log(formatOutput(result, opts.json ?? false));
});
}
diff --git a/profiler-cli/src/commands/function.ts b/profiler-cli/src/commands/function.ts
index 8841beb3d8..770ea5d80e 100644
--- a/profiler-cli/src/commands/function.ts
+++ b/profiler-cli/src/commands/function.ts
@@ -7,9 +7,7 @@
*/
import type { Command } from 'commander';
-import { sendCommand } from '../client';
-import { formatOutput } from '../output';
-import { addGlobalOptions } from './shared';
+import { addGlobalOptions, runCommand } from './shared';
export function registerFunctionCommand(
program: Command,
@@ -24,12 +22,11 @@ export function registerFunctionCommand(
.option('--function ', 'Function handle')
).action(async (handleArg: string | undefined, opts) => {
const funcHandle = handleArg ?? opts.function;
- const result = await sendCommand(
+ await runCommand(
sessionDir,
{ command: 'function', subcommand: 'expand', function: funcHandle },
- opts.session
+ opts
);
- console.log(formatOutput(result, opts.json ?? false));
});
addGlobalOptions(
@@ -39,12 +36,11 @@ export function registerFunctionCommand(
.option('--function ', 'Function handle')
).action(async (handleArg: string | undefined, opts) => {
const funcHandle = handleArg ?? opts.function;
- const result = await sendCommand(
+ await runCommand(
sessionDir,
{ command: 'function', subcommand: 'info', function: funcHandle },
- opts.session
+ opts
);
- console.log(formatOutput(result, opts.json ?? false));
});
addGlobalOptions(
@@ -70,7 +66,7 @@ export function registerFunctionCommand(
)
).action(async (handleArg: string | undefined, opts) => {
const funcHandle = handleArg ?? opts.function;
- const result = await sendCommand(
+ await runCommand(
sessionDir,
{
command: 'function',
@@ -80,8 +76,7 @@ export function registerFunctionCommand(
symbolServerUrl: opts.symbolServer,
annotateContext: opts.context,
},
- opts.session
+ opts
);
- console.log(formatOutput(result, opts.json ?? false));
});
}
diff --git a/profiler-cli/src/commands/marker.ts b/profiler-cli/src/commands/marker.ts
index d84c93e6dc..14a52be7ca 100644
--- a/profiler-cli/src/commands/marker.ts
+++ b/profiler-cli/src/commands/marker.ts
@@ -7,9 +7,7 @@
*/
import type { Command } from 'commander';
-import { sendCommand } from '../client';
-import { formatOutput } from '../output';
-import { addGlobalOptions } from './shared';
+import { addGlobalOptions, runCommand } from './shared';
export function registerMarkerCommand(
program: Command,
@@ -24,12 +22,11 @@ export function registerMarkerCommand(
.option('--marker ', 'Marker handle')
).action(async (handleArg: string | undefined, opts) => {
const markerHandle = handleArg ?? opts.marker;
- const result = await sendCommand(
+ await runCommand(
sessionDir,
{ command: 'marker', subcommand: 'info', marker: markerHandle },
- opts.session
+ opts
);
- console.log(formatOutput(result, opts.json ?? false));
});
addGlobalOptions(
@@ -39,11 +36,10 @@ export function registerMarkerCommand(
.option('--marker ', 'Marker handle')
).action(async (handleArg: string | undefined, opts) => {
const markerHandle = handleArg ?? opts.marker;
- const result = await sendCommand(
+ await runCommand(
sessionDir,
{ command: 'marker', subcommand: 'stack', marker: markerHandle },
- opts.session
+ opts
);
- console.log(formatOutput(result, opts.json ?? false));
});
}
diff --git a/profiler-cli/src/commands/profile.ts b/profiler-cli/src/commands/profile.ts
index 582080324c..cbbbfb5d9b 100644
--- a/profiler-cli/src/commands/profile.ts
+++ b/profiler-cli/src/commands/profile.ts
@@ -7,9 +7,7 @@
*/
import type { Command } from 'commander';
-import { sendCommand } from '../client';
-import { formatOutput } from '../output';
-import { addGlobalOptions, parseIntArg } from './shared';
+import { addGlobalOptions, parseIntArg, runCommand } from './shared';
export function registerProfileCommand(
program: Command,
@@ -29,7 +27,7 @@ export function registerProfileCommand(
)
.option('--search ', 'Filter by substring')
).action(async (opts) => {
- const result = await sendCommand(
+ await runCommand(
sessionDir,
{
command: 'profile',
@@ -37,9 +35,8 @@ export function registerProfileCommand(
all: opts.all,
search: opts.search,
},
- opts.session
+ opts
);
- console.log(formatOutput(result, opts.json ?? false));
});
const VALID_LOG_LEVELS = ['error', 'warn', 'info', 'debug', 'verbose'];
@@ -76,7 +73,7 @@ export function registerProfileCommand(
opts.search !== undefined ||
limit !== undefined;
- const result = await sendCommand(
+ await runCommand(
sessionDir,
{
command: 'profile',
@@ -91,8 +88,7 @@ export function registerProfileCommand(
}
: undefined,
},
- opts.session
+ opts
);
- console.log(formatOutput(result, opts.json ?? false));
});
}
diff --git a/profiler-cli/src/commands/shared.ts b/profiler-cli/src/commands/shared.ts
index 38079ced9e..357ff3d5da 100644
--- a/profiler-cli/src/commands/shared.ts
+++ b/profiler-cli/src/commands/shared.ts
@@ -9,6 +9,31 @@
import type { Command } from 'commander';
import { Option } from 'commander';
import { collectStrings } from '../utils/parse';
+import { sendCommand } from '../client';
+import { formatOutput } from '../output';
+import type { ClientCommand } from '../protocol';
+
+/**
+ * Options shared by every command action via `addGlobalOptions`.
+ */
+export type GlobalOptions = {
+ session?: string;
+ json?: boolean;
+};
+
+/**
+ * Send a command to the daemon and print the formatted result. Centralizes the
+ * `sendCommand` + `formatOutput` tail that every command action shares, so no
+ * call site can forget to pass `opts.session` or honor `opts.json`.
+ */
+export async function runCommand(
+ sessionDir: string,
+ command: ClientCommand,
+ opts: GlobalOptions
+): Promise {
+ const result = await sendCommand(sessionDir, command, opts.session);
+ console.log(formatOutput(result, opts.json ?? false));
+}
/**
* Parse a string as an integer and exit with an error if it is not a valid
diff --git a/profiler-cli/src/commands/thread.ts b/profiler-cli/src/commands/thread.ts
index 4ae326e4b1..6793a455f2 100644
--- a/profiler-cli/src/commands/thread.ts
+++ b/profiler-cli/src/commands/thread.ts
@@ -7,14 +7,13 @@
*/
import type { Command } from 'commander';
-import { sendCommand } from '../client';
-import { formatOutput } from '../output';
import { parseEphemeralFilters } from '../utils/parse';
import {
addGlobalOptions,
addSampleFilterOptions,
parseIntArg,
parseFloatArg,
+ runCommand,
} from './shared';
import type {
CallTreeScoringStrategy,
@@ -95,12 +94,11 @@ export function registerThreadCommand(
.description('Print detailed thread information')
.option('--thread ', 'Thread handle (e.g. t-0)')
).action(async (opts) => {
- const result = await sendCommand(
+ await runCommand(
sessionDir,
{ command: 'thread', subcommand: 'info', thread: opts.thread },
- opts.session
+ opts
);
- console.log(formatOutput(result, opts.json ?? false));
});
// thread select
@@ -111,12 +109,11 @@ export function registerThreadCommand(
.option('--thread ', 'Thread handle')
).action(async (handleArg: string | undefined, opts) => {
const threadHandle = handleArg ?? opts.thread;
- const result = await sendCommand(
+ await runCommand(
sessionDir,
{ command: 'thread', subcommand: 'select', thread: threadHandle },
- opts.session
+ opts
);
- console.log(formatOutput(result, opts.json ?? false));
});
// thread samples
@@ -126,7 +123,7 @@ export function registerThreadCommand(
.description('Show hot functions list for a thread')
).action(async (opts) => {
const sampleFilters = parseEphemeralFilters(opts);
- const result = await sendCommand(
+ await runCommand(
sessionDir,
{
command: 'thread',
@@ -136,9 +133,8 @@ export function registerThreadCommand(
search: opts.search,
sampleFilters: sampleFilters.length ? sampleFilters : undefined,
},
- opts.session
+ opts
);
- console.log(formatOutput(result, opts.json ?? false));
});
// thread samples-top-down
@@ -148,7 +144,7 @@ export function registerThreadCommand(
.description('Show top-down call tree (where CPU time is spent)')
).action(async (opts) => {
const sampleFilters = parseEphemeralFilters(opts);
- const result = await sendCommand(
+ await runCommand(
sessionDir,
{
command: 'thread',
@@ -159,9 +155,8 @@ export function registerThreadCommand(
callTreeOptions: parseCallTreeOptions(opts),
sampleFilters: sampleFilters.length ? sampleFilters : undefined,
},
- opts.session
+ opts
);
- console.log(formatOutput(result, opts.json ?? false));
});
// thread samples-bottom-up
@@ -171,7 +166,7 @@ export function registerThreadCommand(
.description('Show bottom-up call tree (what calls hot functions)')
).action(async (opts) => {
const sampleFilters = parseEphemeralFilters(opts);
- const result = await sendCommand(
+ await runCommand(
sessionDir,
{
command: 'thread',
@@ -182,9 +177,8 @@ export function registerThreadCommand(
callTreeOptions: parseCallTreeOptions(opts),
sampleFilters: sampleFilters.length ? sampleFilters : undefined,
},
- opts.session
+ opts
);
- console.log(formatOutput(result, opts.json ?? false));
});
// thread markers
@@ -282,7 +276,7 @@ export function registerThreadCommand(
}
}
- const result = await sendCommand(
+ await runCommand(
sessionDir,
{
command: 'thread',
@@ -290,9 +284,8 @@ export function registerThreadCommand(
thread: opts.thread,
markerFilters,
},
- opts.session
+ opts
);
- console.log(formatOutput(result, opts.json ?? false));
});
// thread network
@@ -351,7 +344,7 @@ export function registerThreadCommand(
networkFilters.limit = 20;
}
- const result = await sendCommand(
+ await runCommand(
sessionDir,
{
command: 'thread',
@@ -359,9 +352,8 @@ export function registerThreadCommand(
thread: opts.thread,
networkFilters,
},
- opts.session
+ opts
);
- console.log(formatOutput(result, opts.json ?? false));
});
// thread page-load
@@ -401,7 +393,7 @@ export function registerThreadCommand(
);
}
- const result = await sendCommand(
+ await runCommand(
sessionDir,
{
command: 'thread',
@@ -409,9 +401,8 @@ export function registerThreadCommand(
thread: opts.thread,
pageLoadOptions,
},
- opts.session
+ opts
);
- console.log(formatOutput(result, opts.json ?? false));
});
// thread functions
@@ -457,7 +448,7 @@ export function registerThreadCommand(
const sampleFilters = parseEphemeralFilters(opts);
- const result = await sendCommand(
+ await runCommand(
sessionDir,
{
command: 'thread',
@@ -467,8 +458,7 @@ export function registerThreadCommand(
functionFilters,
sampleFilters: sampleFilters.length ? sampleFilters : undefined,
},
- opts.session
+ opts
);
- console.log(formatOutput(result, opts.json ?? false));
});
}
diff --git a/profiler-cli/src/commands/zoom.ts b/profiler-cli/src/commands/zoom.ts
index 26232030cb..1abcd3e692 100644
--- a/profiler-cli/src/commands/zoom.ts
+++ b/profiler-cli/src/commands/zoom.ts
@@ -7,9 +7,7 @@
*/
import type { Command } from 'commander';
-import { sendCommand } from '../client';
-import { formatOutput } from '../output';
-import { addGlobalOptions } from './shared';
+import { addGlobalOptions, runCommand } from './shared';
export function registerZoomCommand(
program: Command,
@@ -24,23 +22,17 @@ export function registerZoomCommand(
'Push a zoom range (e.g. 2.7,3.1 in seconds, 2700ms,3100ms in milliseconds, 10%,20% as percentage, or m-158 for a marker)'
)
).action(async (range: string, opts) => {
- const result = await sendCommand(
+ await runCommand(
sessionDir,
{ command: 'zoom', subcommand: 'push', range },
- opts.session
+ opts
);
- console.log(formatOutput(result, opts.json ?? false));
});
addGlobalOptions(
zoom.command('pop').description('Pop the most recent zoom range')
).action(async (opts) => {
- const result = await sendCommand(
- sessionDir,
- { command: 'zoom', subcommand: 'pop' },
- opts.session
- );
- console.log(formatOutput(result, opts.json ?? false));
+ await runCommand(sessionDir, { command: 'zoom', subcommand: 'pop' }, opts);
});
addGlobalOptions(
@@ -48,11 +40,10 @@ export function registerZoomCommand(
.command('clear')
.description('Clear all zoom ranges (return to full profile)')
).action(async (opts) => {
- const result = await sendCommand(
+ await runCommand(
sessionDir,
{ command: 'zoom', subcommand: 'clear' },
- opts.session
+ opts
);
- console.log(formatOutput(result, opts.json ?? false));
});
}
diff --git a/profiler-cli/src/daemon.ts b/profiler-cli/src/daemon.ts
index bc2375de9b..9e48abc2e7 100644
--- a/profiler-cli/src/daemon.ts
+++ b/profiler-cli/src/daemon.ts
@@ -294,36 +294,40 @@ export class Daemon {
private async processCommand(
command: ClientCommand
): Promise {
+ if (!this.querier) {
+ throw new Error('Profile not loaded');
+ }
+
switch (command.command) {
case 'profile':
switch (command.subcommand) {
case 'info':
- return this.querier!.profileInfo(command.all, command.search);
+ return this.querier.profileInfo(command.all, command.search);
case 'threads':
throw new Error('unimplemented');
case 'logs':
- return this.querier!.profileLogs(command.logFilters);
+ return this.querier.profileLogs(command.logFilters);
default:
throw assertExhaustiveCheck(command);
}
case 'thread':
switch (command.subcommand) {
case 'info':
- return this.querier!.threadInfo(command.thread);
+ return this.querier.threadInfo(command.thread);
case 'select':
if (!command.thread) {
throw new Error('thread handle required for thread select');
}
- return this.querier!.threadSelect(command.thread);
+ return this.querier.threadSelect(command.thread);
case 'samples':
- return this.querier!.threadSamples(
+ return this.querier.threadSamples(
command.thread,
command.includeIdle,
command.search,
command.sampleFilters
);
case 'samples-top-down':
- return this.querier!.threadSamplesTopDown(
+ return this.querier.threadSamplesTopDown(
command.thread,
command.callTreeOptions,
command.includeIdle,
@@ -331,7 +335,7 @@ export class Daemon {
command.sampleFilters
);
case 'samples-bottom-up':
- return this.querier!.threadSamplesBottomUp(
+ return this.querier.threadSamplesBottomUp(
command.thread,
command.callTreeOptions,
command.includeIdle,
@@ -339,24 +343,24 @@ export class Daemon {
command.sampleFilters
);
case 'markers':
- return this.querier!.threadMarkers(
+ return this.querier.threadMarkers(
command.thread,
command.markerFilters
);
case 'functions':
- return this.querier!.threadFunctions(
+ return this.querier.threadFunctions(
command.thread,
command.functionFilters,
command.includeIdle,
command.sampleFilters
);
case 'network':
- return this.querier!.threadNetwork(
+ return this.querier.threadNetwork(
command.thread,
command.networkFilters
);
case 'page-load':
- return this.querier!.threadPageLoad(
+ return this.querier.threadPageLoad(
command.thread,
command.pageLoadOptions
);
@@ -369,12 +373,12 @@ export class Daemon {
if (!command.marker) {
throw new Error('marker handle required for marker info');
}
- return this.querier!.markerInfo(command.marker);
+ return this.querier.markerInfo(command.marker);
case 'stack':
if (!command.marker) {
throw new Error('marker handle required for marker stack');
}
- return this.querier!.markerStack(command.marker);
+ return this.querier.markerStack(command.marker);
case 'select':
throw new Error('unimplemented');
default:
@@ -383,12 +387,12 @@ export class Daemon {
case 'counter':
switch (command.subcommand) {
case 'list':
- return this.querier!.counterList();
+ return this.querier.counterList();
case 'info':
if (!command.counter) {
throw new Error('counter handle required for counter info');
}
- return this.querier!.counterInfo(command.counter);
+ return this.querier.counterInfo(command.counter);
default:
throw assertExhaustiveCheck(command);
}
@@ -407,19 +411,19 @@ export class Daemon {
if (!command.function) {
throw new Error('function handle required for function info');
}
- return this.querier!.functionInfo(command.function);
+ return this.querier.functionInfo(command.function);
case 'expand':
if (!command.function) {
throw new Error('function handle required for function expand');
}
- return this.querier!.functionExpand(command.function);
+ return this.querier.functionExpand(command.function);
case 'select':
throw new Error('unimplemented');
case 'annotate':
if (!command.function) {
throw new Error('function handle required for function annotate');
}
- return this.querier!.functionAnnotate(
+ return this.querier.functionAnnotate(
command.function,
command.annotateMode ?? 'src',
command.symbolServerUrl,
@@ -434,11 +438,11 @@ export class Daemon {
if (!command.range) {
throw new Error('range parameter is required for zoom push');
}
- return this.querier!.pushViewRange(command.range);
+ return this.querier.pushViewRange(command.range);
case 'pop':
- return this.querier!.popViewRange();
+ return this.querier.popViewRange();
case 'clear':
- return this.querier!.clearViewRange();
+ return this.querier.clearViewRange();
default:
throw assertExhaustiveCheck(command);
}
@@ -448,18 +452,18 @@ export class Daemon {
if (!command.spec) {
throw new Error('spec is required for filter push');
}
- return this.querier!.filterPush(command.spec, command.thread);
+ return this.querier.filterPush(command.spec, command.thread);
case 'pop':
- return this.querier!.filterPop(command.count ?? 1, command.thread);
+ return this.querier.filterPop(command.count ?? 1, command.thread);
case 'list':
- return this.querier!.filterList(command.thread);
+ return this.querier.filterList(command.thread);
case 'clear':
- return this.querier!.filterClear(command.thread);
+ return this.querier.filterClear(command.thread);
default:
throw assertExhaustiveCheck(command);
}
case 'status':
- return this.querier!.getStatus();
+ return this.querier.getStatus();
default:
throw assertExhaustiveCheck(command);
}
diff --git a/profiler-cli/src/index.ts b/profiler-cli/src/index.ts
index c637516b9b..88a9126f1d 100644
--- a/profiler-cli/src/index.ts
+++ b/profiler-cli/src/index.ts
@@ -31,7 +31,7 @@ import { startDaemon } from './daemon';
import { startNewDaemon, stopDaemon, sendCommand } from './client';
import { listSessions } from './session';
import { formatOutput } from './output';
-import { addGlobalOptions } from './commands/shared';
+import { addGlobalOptions, runCommand } from './commands/shared';
import { VERSION } from './constants';
import { registerProfileCommand } from './commands/profile';
import { registerThreadCommand } from './commands/thread';
@@ -135,12 +135,7 @@ Examples:
'Show session status (selected thread, zoom ranges, filters)'
)
).action(async (opts) => {
- const result = await sendCommand(
- SESSION_DIR,
- { command: 'status' },
- opts.session
- );
- console.log(formatOutput(result, opts.json ?? false));
+ await runCommand(SESSION_DIR, { command: 'status' }, opts);
});
// profiler-cli stop [id]
diff --git a/res/index.html b/res/index.html
index 071f7cf275..4cf1fcf20c 100644
--- a/res/index.html
+++ b/res/index.html
@@ -6,6 +6,7 @@
+
Firefox Profiler