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