From e4ef68b68a7a34e4ca0bcfe802a90623a48b241d Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Mon, 23 Mar 2026 16:12:23 -0400 Subject: [PATCH 01/64] Propagate isJS to inline frames during symbolication. --- src/profile-logic/symbolication.ts | 4 +- src/test/fixtures/example-symbol-table.ts | 42 ++++++++++++++++++ src/test/store/symbolication.test.ts | 52 ++++++++++++++++++++--- 3 files changed, 89 insertions(+), 9 deletions(-) diff --git a/src/profile-logic/symbolication.ts b/src/profile-logic/symbolication.ts index 3186fb7b3a..cdc01ac0d2 100644 --- a/src/profile-logic/symbolication.ts +++ b/src/profile-logic/symbolication.ts @@ -789,8 +789,8 @@ function _partiallyApplySymbolicationStep( if (funcIndex === undefined) { // Need a new func. funcIndex = funcTable.length; - funcTable.isJS[funcIndex] = false; - funcTable.relevantForJS[funcIndex] = false; + funcTable.isJS[funcIndex] = funcTable.isJS[oldFunc]; + funcTable.relevantForJS[funcIndex] = funcTable.relevantForJS[oldFunc]; funcTable.resource[funcIndex] = resourceIndex; funcTable.source[funcIndex] = null; funcTable.lineNumber[funcIndex] = null; diff --git a/src/test/fixtures/example-symbol-table.ts b/src/test/fixtures/example-symbol-table.ts index 509aa8d79b..40af714732 100644 --- a/src/test/fixtures/example-symbol-table.ts +++ b/src/test/fixtures/example-symbol-table.ts @@ -280,3 +280,45 @@ export const partialSymbolTable: ExampleSymbolTable = { export const completeSymbolTableAsTuple = completeSymbolTable.asTuple; export const partialSymbolTableAsTuple = partialSymbolTable.asTuple; + +// A symbol table for a JIT dump file (e.g. jit-52344.dump), simulating JS +// functions symbolicated from samply + jitdump. The outer function +// "renderButton.js" has an inlined call to "useState.js" at address 0x000a. +const jitDumpSyms = [ + { + address: 0, + name: 'renderButton.js', + file: 'Button.tsx', + lineRanges: [ + { + startAddress: 0x0, + line: 42, + }, + { + startAddress: 0x8, + line: 45, + inlinedCall: { + name: 'useState.js', + file: 'react.js', + lineRanges: [ + { + startAddress: 0x8, + line: 100, + }, + ], + }, + }, + ], + }, + { + address: 0x2000, + name: 'runJobs.js', + file: 'scheduler.js', + }, +]; + +export const jitDumpSymbolTable: ExampleSymbolTable = { + symbols: jitDumpSyms, + asTuple: _makeSymbolTableAsTuple(jitDumpSyms), + getAddressResult: _makeGetAddressResultFunction(jitDumpSyms), +}; diff --git a/src/test/store/symbolication.test.ts b/src/test/store/symbolication.test.ts index 372bdd5980..40c47f71ed 100644 --- a/src/test/store/symbolication.test.ts +++ b/src/test/store/symbolication.test.ts @@ -7,6 +7,7 @@ import { getProfileFromTextSamples } from '../fixtures/profiles/processed-profil import { completeSymbolTable, partialSymbolTable, + jitDumpSymbolTable, } from '../fixtures/example-symbol-table'; import type { ExampleSymbolTable } from '../fixtures/example-symbol-table'; import type { MarkerPayload } from 'firefox-profiler/types'; @@ -29,6 +30,7 @@ import { doSymbolicateProfile } from '../../actions/receive-profile'; import { changeSelectedCallNode, changeExpandedCallNodes, + changeImplementationFilter, } from '../../actions/profile-view'; import { formatTree, formatStack } from '../fixtures/utils'; import { assertSetContainsOnly } from '../fixtures/custom-assertions'; @@ -44,18 +46,17 @@ import { SymbolsNotFoundError } from '../../profile-logic/errors'; */ describe('doSymbolicateProfile', function () { // Initialize a store, an unsymbolicated profile, and helper functions. - function init() { + function init(profile = _createUnsymbolicatedProfile()) { // The rejection in `requestSymbolsFromServer` outputs an error log, let's // silence it here. The fact that we call it is tested in // symbol-store.test.js. jest.spyOn(console, 'log').mockImplementation(() => {}); - const profile = _createUnsymbolicatedProfile(); const store = storeWithProfile(profile); - let symbolTable = completeSymbolTable; + let firefoxSymbolTable = completeSymbolTable; function switchSymbolTable(otherSymbolTable: ExampleSymbolTable) { - symbolTable = otherSymbolTable; + firefoxSymbolTable = otherSymbolTable; } let symbolicationProviderMode: 'from-server' | 'from-browser' = 'from-browser'; @@ -67,12 +68,19 @@ describe('doSymbolicateProfile', function () { requestSymbolsFromServer: async (requests: LibSymbolicationRequest[]) => requests.map((request) => { const { lib, addresses } = request; - if (lib.debugName !== 'firefox.pdb') { + + const symbolTables: Partial> = { + 'firefox.pdb': firefoxSymbolTable, + 'jit-52344.dump': jitDumpSymbolTable, + }; + + const symbolTable = symbolTables[lib.debugName]; + if (symbolTable === undefined) { return { type: 'ERROR' as const, request, error: new SymbolsNotFoundError( - 'Should only have lib called firefox.pdb', + `Lib name ${lib.debugName} is not in the list of known names: ${Object.keys(symbolTables).join(', ')}`, lib ), }; @@ -123,7 +131,7 @@ describe('doSymbolicateProfile', function () { } return readSymbolsFromSymbolTable( addresses, - symbolTable.asTuple, + firefoxSymbolTable.asTuple, (s: string) => s ); }, @@ -478,6 +486,28 @@ describe('doSymbolicateProfile', function () { ]); }); + it('inline frames for JS functions appear in the JS-only call tree after symbolication', async () => { + const { + store: { dispatch, getState }, + profile, + symbolStore, + switchSymbolProviderMode, + } = init(_createUnsymbolicatedJitProfile()); + + switchSymbolProviderMode('from-server'); + + await doSymbolicateProfile(dispatch, profile, symbolStore); + + // Check that the `useState.js` node shows up in the JS-only call tree. + // This function is an inline frame from the jitdump symbol info. + dispatch(changeImplementationFilter('js')); + expect(formatTree(getCallTree(getState()))).toEqual([ + '- renderButton.js (total: 1, self: —)', + ' - useState.js (total: 1, self: 1)', + '- runJobs.js (total: 1, self: 1)', + ]); + }); + it('can re-symbolicate a partially-symbolicated profile even if it needs to add funcs to the funcTable', async () => { const { store: { dispatch, getState }, @@ -623,3 +653,11 @@ function _createUnsymbolicatedProfile() { return profile; } + +function _createUnsymbolicatedJitProfile() { + // See jitDumpSyms (in example-symbol-table.ts) for the corresponding symbols. + const { profile } = getProfileFromTextSamples(` + renderButton.js[lib:jit-52344.dump][address:a] runJobs.js[lib:jit-52344.dump][address:2000] + `); + return profile; +} From d199335143a53b38555f67daea3b48fbcd7012fb Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Tue, 24 Mar 2026 15:16:00 -0400 Subject: [PATCH 02/64] Use createStackTableBySkippingDiscarded in focusSelf. Fixes #5915. Before this fix, we were keeping the filtered-out stacks in the stack table. Now we create a stack table which has those stacks removed. A smaller stack table makes all the follow-on work faster. Before: https://share.firefox.dev/4bXlszU After: https://share.firefox.dev/4bJCQb6 --- src/profile-logic/transforms.ts | 51 +++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/src/profile-logic/transforms.ts b/src/profile-logic/transforms.ts index 604b7f1179..c2220a331e 100644 --- a/src/profile-logic/transforms.ts +++ b/src/profile-logic/transforms.ts @@ -12,6 +12,8 @@ import { updateThreadStacksByGeneratingNewStackColumns, getMapStackUpdater, getOriginAnnotationForFunc, + createStackTableBySkippingDiscarded, + applyTransformOutputToThread, } from './profile-data'; import { timeCode } from '../utils/time-code'; import { assertExhaustiveCheck, convertToTransformType } from '../utils/types'; @@ -54,6 +56,7 @@ import { translateFuncIndex, translateResourceIndex, } from './index-translation'; +import { checkBit, makeBitSet, setBit } from 'firefox-profiler/utils/bitset'; /** * This file contains the functions and logic for working with and applying transforms @@ -1443,40 +1446,52 @@ export function focusSelf( const funcMatchesImplementation = FUNC_MATCHES[implementation]; - const shouldKeepStack = new Uint8Array(stackTable.length); + const shouldKeepStack = makeBitSet(stackTable.length); - const newPrefixCol = stackTable.prefix.slice(); + const newPrefixCol = new Array(); + const oldStackToNewStack = new Int32Array(stackTable.length); for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) { const frameIndex = stackTable.frame[stackIndex]; const funcIndex = frameTable.func[frameIndex]; if (funcIndex === funcIndexToFocus) { - shouldKeepStack[stackIndex] = 1; - newPrefixCol[stackIndex] = null; + setBit(shouldKeepStack, stackIndex); + const newStackIndex = newPrefixCol.length; + newPrefixCol[newStackIndex] = null; + oldStackToNewStack[stackIndex] = newStackIndex; } else { - const prefix = newPrefixCol[stackIndex]; + const oldPrefix = stackTable.prefix[stackIndex]; if ( - prefix !== null && - shouldKeepStack[prefix] === 1 && + oldPrefix !== null && + checkBit(shouldKeepStack, oldPrefix) && !funcMatchesImplementation(thread, funcIndex) ) { - shouldKeepStack[stackIndex] = 1; + setBit(shouldKeepStack, stackIndex); + const newPrefix = oldStackToNewStack[oldPrefix]; + const newStackIndex = newPrefixCol.length; + newPrefixCol[newStackIndex] = newPrefix; + oldStackToNewStack[stackIndex] = newStackIndex; } } } - const newStackTable = { - ...stackTable, - prefix: newPrefixCol, - }; + const newStackTable = createStackTableBySkippingDiscarded( + stackTable, + newPrefixCol, + shouldKeepStack + ); - return updateThreadStacks(thread, newStackTable, (oldStack) => { - if (oldStack === null || shouldKeepStack[oldStack] === 0) { - return null; - } - return oldStack; - }); + return applyTransformOutputToThread( + { + newStackTable, + effectOnThreadData: { + dropIfOldStackIsNot: shouldKeepStack, + oldStackToNewStack, + }, + }, + thread + ); }); } From f933ed3b4fe40fa674a73f2854174abc09951fdc Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Fri, 6 Mar 2026 19:52:35 -0500 Subject: [PATCH 03/64] Always render the CPU-usage-aware activity graph when CPU information is available. The UI for switching the activity graph type isn't visible by default and nobody has complained about its absence. This simplifies the code. The timelineType values 'category' and 'cpu-category' are now equivalent. --- .../shared/thread/ActivityGraph.tsx | 12 +-------- .../shared/thread/ActivityGraphCanvas.tsx | 3 --- .../shared/thread/ActivityGraphFills.tsx | 27 +++++-------------- src/components/shared/thread/SampleGraph.tsx | 16 +++-------- src/components/timeline/TrackThread.tsx | 8 ------ src/profile-logic/profile-data.ts | 5 ++-- src/types/actions.ts | 3 +++ 7 files changed, 15 insertions(+), 59 deletions(-) diff --git a/src/components/shared/thread/ActivityGraph.tsx b/src/components/shared/thread/ActivityGraph.tsx index 60dcefc65b..5310970e54 100644 --- a/src/components/shared/thread/ActivityGraph.tsx +++ b/src/components/shared/thread/ActivityGraph.tsx @@ -23,7 +23,6 @@ import type { IndexIntoSamplesTable, Milliseconds, CssPixels, - TimelineType, } from 'firefox-profiler/types'; import type { ActivityFillGraphQuerier, @@ -49,9 +48,7 @@ export type Props = { a: IndexIntoSamplesTable, b: IndexIntoSamplesTable ) => number; - readonly enableCPUUsage: boolean; readonly implementationFilter: ImplementationFilter; - readonly timelineType: TimelineType; readonly zeroAt: Milliseconds; readonly profileTimelineUnit: string; } & SizeProps; @@ -136,11 +133,9 @@ class ThreadActivityGraphImpl extends React.PureComponent { sampleIndexOffset, sampleSelectedStates, treeOrderSampleComparator, - enableCPUUsage, implementationFilter, width, height, - timelineType, zeroAt, profileTimelineUnit, } = this.props; @@ -168,7 +163,6 @@ class ThreadActivityGraphImpl extends React.PureComponent { categories={categories} passFillsQuerier={this._setFillsQuerier} onClick={this._onClick} - enableCPUUsage={enableCPUUsage} width={width} height={height} /> @@ -176,11 +170,7 @@ class ThreadActivityGraphImpl extends React.PureComponent { void; readonly onClick: (param: React.MouseEvent) => void; - readonly enableCPUUsage: boolean; } & SizeProps; export class ActivityGraphCanvas extends React.PureComponent { @@ -132,7 +131,6 @@ export class ActivityGraphCanvas extends React.PureComponent { sampleIndexOffset, sampleSelectedStates, treeOrderSampleComparator, - enableCPUUsage, width, height, } = this.props; @@ -153,7 +151,6 @@ export class ActivityGraphCanvas extends React.PureComponent { rangeEnd, sampleIndexOffset, sampleSelectedStates, - enableCPUUsage, xPixelsPerMs: canvasPixelWidth / (rangeEnd - rangeStart), treeOrderSampleComparator, categoryDrawStyles: this._getCategoryDrawStyles(ctx!), diff --git a/src/components/shared/thread/ActivityGraphFills.tsx b/src/components/shared/thread/ActivityGraphFills.tsx index 9a4f88df79..0b01e803dd 100644 --- a/src/components/shared/thread/ActivityGraphFills.tsx +++ b/src/components/shared/thread/ActivityGraphFills.tsx @@ -36,7 +36,6 @@ type RenderedComponentSettings = { readonly rangeEnd: Milliseconds; readonly sampleIndexOffset: number; readonly xPixelsPerMs: number; - readonly enableCPUUsage: boolean; readonly treeOrderSampleComparator: | ((a: IndexIntoSamplesTable, b: IndexIntoSamplesTable) => number) | null; @@ -215,7 +214,6 @@ export class ActivityGraphFillComputer { fullThread, rangeFilteredThread: { samples }, interval, - enableCPUUsage, sampleIndexOffset, rangeStart, sampleSelectedStates, @@ -243,12 +241,8 @@ export class ActivityGraphFillComputer { const nextSampleTime = samples.time[i + 1]; const category = samples.category[i]; - let beforeSampleCpuPercent = 100; - let afterSampleCpuPercent = 100; - if (enableCPUUsage) { - beforeSampleCpuPercent = threadCPUPercent[i]; - afterSampleCpuPercent = threadCPUPercent[i + 1]; - } + const beforeSampleCpuPercent = threadCPUPercent[i]; + const afterSampleCpuPercent = threadCPUPercent[i + 1]; const percentageBuffers = this.mutablePercentageBuffers[category]; const selectedState = sampleSelectedStates[i]; @@ -273,12 +267,8 @@ export class ActivityGraphFillComputer { const lastIdx = samples.length - 1; const lastSampleCategory = samples.category[lastIdx]; - let beforeSampleCpuPercent = 100; - let afterSampleCpuPercent = 100; - if (enableCPUUsage) { - beforeSampleCpuPercent = threadCPUPercent[lastIdx]; - afterSampleCpuPercent = threadCPUPercent[lastIdx + 1]; // guaranteed to exist - } + const beforeSampleCpuPercent = threadCPUPercent[lastIdx]; + const afterSampleCpuPercent = threadCPUPercent[lastIdx + 1]; // guaranteed to exist const nextSampleTime = sampleTime + interval; const percentageBuffers = this.mutablePercentageBuffers[lastSampleCategory]; @@ -596,7 +586,6 @@ export class ActivityFillGraphQuerier { ): number { const { rangeFilteredThread: { samples }, - enableCPUUsage, interval, sampleIndexOffset, fullThread, @@ -617,13 +606,9 @@ export class ActivityFillGraphQuerier { ? fullThread.samples.time[fullThreadSample + 1] : sampleTime + interval; - let beforeSampleCpuPercent = 100; - let afterSampleCpuPercent = 100; const { threadCPUPercent } = samples; - if (enableCPUUsage) { - beforeSampleCpuPercent = threadCPUPercent[sample]; - afterSampleCpuPercent = threadCPUPercent[sample + 1]; // guaranteed to exist - } + const beforeSampleCpuPercent = threadCPUPercent[sample]; + const afterSampleCpuPercent = threadCPUPercent[sample + 1]; // guaranteed to exist const kernelRangeStartTime = rangeStart + kernelPos / xPixelsPerMs; diff --git a/src/components/shared/thread/SampleGraph.tsx b/src/components/shared/thread/SampleGraph.tsx index a478a75285..7790794440 100644 --- a/src/components/shared/thread/SampleGraph.tsx +++ b/src/components/shared/thread/SampleGraph.tsx @@ -25,17 +25,14 @@ import type { IndexIntoSamplesTable, Milliseconds, CssPixels, - TimelineType, ImplementationFilter, } from 'firefox-profiler/types'; import { SelectedState } from 'firefox-profiler/types'; import type { SizeProps } from 'firefox-profiler/components/shared/WithSize'; -import type { CpuRatioInTimeRange } from './ActivityGraphFills'; import { lightDark } from 'firefox-profiler/utils/dark-mode'; export type HoveredPixelState = { readonly sample: IndexIntoSamplesTable | null; - readonly cpuRatioInTimeRange: CpuRatioInTimeRange | null; }; type Props = { @@ -51,7 +48,6 @@ type Props = { sampleIndex: IndexIntoSamplesTable | null ) => void; readonly trackName: string; - readonly timelineType: TimelineType; readonly implementationFilter: ImplementationFilter; readonly zeroAt: Milliseconds; readonly profileTimelineUnit: string; @@ -252,7 +248,7 @@ class ThreadSampleGraphCanvas extends React.PureComponent { } export class ThreadSampleGraphImpl extends PureComponent { - override state = { + override state: State = { hoveredPixelState: null, mouseX: 0, mouseY: 0, @@ -333,7 +329,6 @@ export class ThreadSampleGraphImpl extends PureComponent { return { sample: sampleIndex, - cpuRatioInTimeRange: null, }; } @@ -341,7 +336,6 @@ export class ThreadSampleGraphImpl extends PureComponent { const { className, trackName, - timelineType, categories, implementationFilter, thread, @@ -379,12 +373,8 @@ export class ThreadSampleGraphImpl extends PureComponent { {hoveredPixelState === null ? null : ( number; readonly selectedThreadIndexes: Set; - readonly enableCPUUsage: boolean; readonly isExperimentalCPUGraphsEnabled: boolean; readonly implementationFilter: ImplementationFilter; readonly callTreeVisible: boolean; @@ -186,7 +185,6 @@ class TimelineTrackThreadImpl extends PureComponent { treeOrderSampleComparator, trackType, trackName, - enableCPUUsage, isExperimentalCPUGraphsEnabled, implementationFilter, zeroAt, @@ -257,9 +255,7 @@ class TimelineTrackThreadImpl extends PureComponent { categories={categories} sampleSelectedStates={sampleSelectedStates} treeOrderSampleComparator={treeOrderSampleComparator} - enableCPUUsage={enableCPUUsage} implementationFilter={implementationFilter} - timelineType={timelineType} zeroAt={zeroAt} profileTimelineUnit={profileTimelineUnit} /> @@ -274,7 +270,6 @@ class TimelineTrackThreadImpl extends PureComponent { sampleSelectedStates={sampleSelectedStates} categories={categories} onSampleClick={this._onSampleClick} - timelineType={timelineType} implementationFilter={implementationFilter} zeroAt={zeroAt} profileTimelineUnit={profileTimelineUnit} @@ -335,8 +330,6 @@ export const TimelineTrackThread = explicitConnect< const committedRange = getCommittedRange(state); const fullThread = selectors.getThread(state); const timelineType = getTimelineType(state); - const enableCPUUsage = - timelineType === 'cpu-category' && fullThread.samples.hasCPUDeltas; return { fullThread, @@ -361,7 +354,6 @@ export const TimelineTrackThread = explicitConnect< treeOrderSampleComparator: selectors.getTreeOrderComparatorInFilteredThread(state), selectedThreadIndexes, - enableCPUUsage, isExperimentalCPUGraphsEnabled: getIsExperimentalCPUGraphsEnabled(state), implementationFilter: getImplementationFilter(state), callTreeVisible: selectors.getUsefulTabs(state).includes('calltree'), diff --git a/src/profile-logic/profile-data.ts b/src/profile-logic/profile-data.ts index 49a521c9f1..1c36781522 100644 --- a/src/profile-logic/profile-data.ts +++ b/src/profile-logic/profile-data.ts @@ -4305,10 +4305,9 @@ export function getNativeSymbolInfo( /** * Determines the timeline type by looking at the profile data. * - * There are three options: * 'cpu-category': If a profile has both category and cpu usage information. - * 'category': If a profile has category information but not the cpu usage. - * 'stack': If a profile doesn't have category or cpu usage information. + * 'category': If a profile has category information but not cpu usage. + * 'stack': If a profile doesn't have category information. */ export function determineTimelineType(profile: Profile): TimelineType { if (!profile.meta.categories) { diff --git a/src/types/actions.ts b/src/types/actions.ts index b3dccd0f99..40b9656a64 100644 --- a/src/types/actions.ts +++ b/src/types/actions.ts @@ -85,6 +85,9 @@ export type DataSource = // this browser, and allows deleting / unpublishing those profiles. | 'uploaded-recordings'; +// Controls which graph is shown in thread tracks. +// 'cpu-category', 'category': Show the category activity graph. +// 'stack': Shows the stack graph, for profiles without category information. export type TimelineType = 'stack' | 'category' | 'cpu-category'; export type PreviewSelection = { readonly isModifying: boolean; From 7aad3282f49e90b4cc78dd42cc78cd9ea1266fe4 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sat, 28 Mar 2026 12:22:30 -0400 Subject: [PATCH 04/64] Remove timelineType 'category' since it's now equivalent to 'cpu-category'. --- src/app-logic/url-handling.ts | 12 ++++-------- src/components/timeline/TrackThread.tsx | 3 +-- src/profile-logic/profile-data.ts | 13 +------------ src/test/store/profile-view.test.ts | 6 ++++-- .../unit/__snapshots__/window-console.test.ts.snap | 2 +- src/test/url-handling.test.ts | 11 ++++++++--- src/types/actions.ts | 6 +++--- src/utils/window-console.ts | 10 +++------- 8 files changed, 25 insertions(+), 38 deletions(-) diff --git a/src/app-logic/url-handling.ts b/src/app-logic/url-handling.ts index f2b1196339..eecd06a2be 100644 --- a/src/app-logic/url-handling.ts +++ b/src/app-logic/url-handling.ts @@ -309,7 +309,7 @@ export function getQueryStringFromUrlState(urlState: UrlState): string { : urlState.profileSpecific.implementation, timelineType: // The default is the cpu-category view, so only add it to the URL if it's - // the stack or category view. + // the stack view. urlState.profileSpecific.timelineType === 'cpu-category' ? undefined : urlState.profileSpecific.timelineType, @@ -1445,14 +1445,10 @@ function getVersion4JSCallNodePathFromStackIndex( function validateTimelineType( timelineType: string | null | undefined ): TimelineType { - const VALID_TIMELINE_TYPES: Record = { - stack: true, - category: true, - 'cpu-category': true, - }; - if (timelineType && timelineType in VALID_TIMELINE_TYPES) { - return timelineType as TimelineType; + if (timelineType === 'stack') { + return 'stack'; } + // 'category' is an old value that is treated as 'cpu-category'. return 'cpu-category'; } diff --git a/src/components/timeline/TrackThread.tsx b/src/components/timeline/TrackThread.tsx index 292fd77c9c..b0ee5338a6 100644 --- a/src/components/timeline/TrackThread.tsx +++ b/src/components/timeline/TrackThread.tsx @@ -239,8 +239,7 @@ class TimelineTrackThreadImpl extends PureComponent { /> ) : null} - {(timelineType === 'category' || timelineType === 'cpu-category') && - !filteredThread.isJsTracer ? ( + {timelineType !== 'stack' && !filteredThread.isJsTracer ? ( <> thread.samples.threadCPUDelta) - ) { - // Have category information but doesn't have the CPU usage information. - // Use 'category'. - return 'category'; - } - - // Have both category and CPU usage information. Use 'cpu-category'. return 'cpu-category'; } diff --git a/src/test/store/profile-view.test.ts b/src/test/store/profile-view.test.ts index 8d530c8758..c78779dcf3 100644 --- a/src/test/store/profile-view.test.ts +++ b/src/test/store/profile-view.test.ts @@ -3869,12 +3869,14 @@ describe('timeline type', function () { ); }); - it('should use the category view when cpu is not provided', () => { + it('should use the cpu-category view even if no cpu is provided', () => { const { profile } = getProfileFromTextSamples('A'); // Load the store after mutating the profile. const { getState } = storeWithProfile(profile); - expect(UrlStateSelectors.getTimelineType(getState())).toEqual('category'); + expect(UrlStateSelectors.getTimelineType(getState())).toEqual( + 'cpu-category' + ); }); it('should use the stack height view when category and cpu is not provided', () => { diff --git a/src/test/unit/__snapshots__/window-console.test.ts.snap b/src/test/unit/__snapshots__/window-console.test.ts.snap index d55758c4da..2bb9d98cf7 100644 --- a/src/test/unit/__snapshots__/window-console.test.ts.snap +++ b/src/test/unit/__snapshots__/window-console.test.ts.snap @@ -35,7 +35,7 @@ Array [ %cwindow.actions%c - All the actions that can be dispatched to change the state. %cwindow.experimental%c - The object that holds flags of all the experimental features. %cwindow.togglePseudoLocalization%c - Enable pseudo localizations by passing \\"accented\\" or \\"bidi\\" to this function, or disable using no parameters. -%cwindow.toggleTimelineType%c - Toggle timeline graph type by passing \\"cpu-category\\", \\"category\\", or \\"stack\\". +%cwindow.toggleTimelineType%c - Toggle timeline graph type by passing \\"cpu-category\\" or \\"stack\\". %cwindow.toggleDarkMode%c - Cycle through theme preferences: system, light, dark. %cwindow.retrieveRawProfileDataFromBrowser%c - Retrieve the profile attached to the current tab and returns it. Use \\"await\\" to call it, and use saveToDisk to save it. %cwindow.extractGeckoLogs%c - Retrieve recorded logs in the current range, using the MOZ_LOG format. Use with \\"copy\\" or \\"saveToDisk\\". diff --git a/src/test/url-handling.test.ts b/src/test/url-handling.test.ts index ad61c235cb..88ff994ee0 100644 --- a/src/test/url-handling.test.ts +++ b/src/test/url-handling.test.ts @@ -1234,14 +1234,19 @@ describe('url upgrading', function () { expect(query.timelineType).toBeFalsy(); }); - it('add an explicit category from the url', function () { + it('maps the implicit category type to cpu-category', function () { + // In v6, the default timeline type was 'category'. The v7 upgrader adds + // an explicit 'category' to the URL, which is then parsed as 'cpu-category' + // since the two are now equivalent. const { getState } = _getStoreWithURL({ pathname: '/public/e71ce9584da34298627fb66ac7f2f245ba5edbf5/calltree/', search: '', v: 6, }); - expect(urlStateSelectors.getTimelineType(getState())).toBe('category'); + expect(urlStateSelectors.getTimelineType(getState())).toBe( + 'cpu-category' + ); const newUrl = new URL( urlFromState(urlStateSelectors.getUrlState(getState())), @@ -1250,7 +1255,7 @@ describe('url upgrading', function () { const query = queryString.parse(newUrl.search.substr(1), { arrayFormat: 'bracket', }); - expect(query.timelineType).toBe('category'); + expect(query.timelineType).toBeFalsy(); }); it('keeps stack category the same', function () { diff --git a/src/types/actions.ts b/src/types/actions.ts index 40b9656a64..6f11b70e73 100644 --- a/src/types/actions.ts +++ b/src/types/actions.ts @@ -86,9 +86,9 @@ export type DataSource = | 'uploaded-recordings'; // Controls which graph is shown in thread tracks. -// 'cpu-category', 'category': Show the category activity graph. -// 'stack': Shows the stack graph, for profiles without category information. -export type TimelineType = 'stack' | 'category' | 'cpu-category'; +// 'cpu-category': Shows the category activity graph. +// 'stack': Shows the stack graph, for profiles without category information. +export type TimelineType = 'stack' | 'cpu-category'; export type PreviewSelection = { readonly isModifying: boolean; readonly selectionStart: number; diff --git a/src/utils/window-console.ts b/src/utils/window-console.ts index 0beb4651fd..97803f23d1 100644 --- a/src/utils/window-console.ts +++ b/src/utils/window-console.ts @@ -180,14 +180,10 @@ export function addDataToWindowObject( }; target.toggleTimelineType = function (timelineType?: string) { - if ( - timelineType !== 'cpu-category' && - timelineType !== 'category' && - timelineType !== 'stack' - ) { + if (timelineType !== 'cpu-category' && timelineType !== 'stack') { console.log(stripIndent` ❗ The timeline type "${timelineType}" is unknown. - 💡 Valid types are: "cpu-category", "category", or "stack". + 💡 Valid types are: "cpu-category" or "stack". Please try again 😊 `); return; @@ -421,7 +417,7 @@ export function logFriendlyPreamble() { %cwindow.actions%c - All the actions that can be dispatched to change the state. %cwindow.experimental%c - The object that holds flags of all the experimental features. %cwindow.togglePseudoLocalization%c - Enable pseudo localizations by passing "accented" or "bidi" to this function, or disable using no parameters. - %cwindow.toggleTimelineType%c - Toggle timeline graph type by passing "cpu-category", "category", or "stack". + %cwindow.toggleTimelineType%c - Toggle timeline graph type by passing "cpu-category" or "stack". %cwindow.toggleDarkMode%c - Cycle through theme preferences: system, light, dark. %cwindow.retrieveRawProfileDataFromBrowser%c - Retrieve the profile attached to the current tab and returns it. Use "await" to call it, and use saveToDisk to save it. %cwindow.extractGeckoLogs%c - Retrieve recorded logs in the current range, using the MOZ_LOG format. Use with "copy" or "saveToDisk". From d975ed2673c47430ca571bc732aa9bc28ca08aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Thu, 2 Apr 2026 11:53:00 +0200 Subject: [PATCH 05/64] Add Adel to the automatic dependency reviews (#5930) --- .github/CODEOWNERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 51e2f8ea6c..d51d70b842 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,5 +4,5 @@ /locales/en-US/**/*.ftl @firefox-devtools/performance-l10n # Automatically request reviews for the dependency updates -/package.json @canova -/yarn.lock @canova +/package.json @canova @fatadel +/yarn.lock @canova @fatadel From 634b4b918f038e2cdc8ecd539f740d37c74f5f24 Mon Sep 17 00:00:00 2001 From: fatadel Date: Thu, 2 Apr 2026 12:03:21 +0200 Subject: [PATCH 06/64] Fix arrow panel appearing behind marker tooltips (#5926) Tooltips are rendered via portal into #root-overlay, which is a sibling of #root. Two stacking contexts were trapping the arrow panel inside #root, preventing it from competing with tooltips: - #root had z-index: 0, creating a stacking context (it's a flex item) - .dragAndDropArea had isolation: isolate Remove both so the arrow panel's z-index: 5 reaches the body stacking context. Add z-index: 4 to tooltips so they remain above regular content but below arrow panels and context menus (z-index: 5). --- res/css/global.css | 2 +- src/components/app/DragAndDrop.css | 1 - src/components/app/Root.css | 1 - src/components/tooltip/Tooltip.css | 1 + 4 files changed, 2 insertions(+), 3 deletions(-) diff --git a/res/css/global.css b/res/css/global.css index 9c47eb63df..5353f161f0 100644 --- a/res/css/global.css +++ b/res/css/global.css @@ -8,7 +8,7 @@ * ===================== */ --z-tab-selected: 1; --z-warning: 4; - --z-root: 0; + --z-tooltip: 4; --z-bottom-box: 1; --z-serviceworker-notice: 4; --z-arrow-panel: 5; diff --git a/src/components/app/DragAndDrop.css b/src/components/app/DragAndDrop.css index 01a44cb61b..1d77eef8f0 100644 --- a/src/components/app/DragAndDrop.css +++ b/src/components/app/DragAndDrop.css @@ -5,7 +5,6 @@ .dragAndDropArea { display: inherit; width: 100%; - isolation: isolate; } .dragAndDropOverlayWrapper { diff --git a/src/components/app/Root.css b/src/components/app/Root.css index f19f764684..c9519c981e 100644 --- a/src/components/app/Root.css +++ b/src/components/app/Root.css @@ -3,7 +3,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #root { - z-index: var(--z-root); display: flex; min-width: 0; /* This allows Flexible Layout to shrink this further than its min-content */ flex: 1; diff --git a/src/components/tooltip/Tooltip.css b/src/components/tooltip/Tooltip.css index 230085ffc2..1ec269b652 100644 --- a/src/components/tooltip/Tooltip.css +++ b/src/components/tooltip/Tooltip.css @@ -9,6 +9,7 @@ --internal-label-foreground-color: var(--grey-60); position: fixed; + z-index: var(--z-tooltip); display: inline-block; overflow: hidden; max-width: 600px; From 307de87c964168485ecc0f67b5f00c24e827cc0a Mon Sep 17 00:00:00 2001 From: fatadel Date: Thu, 2 Apr 2026 12:06:55 +0200 Subject: [PATCH 07/64] Upgrade Node.js from v22 to v24 (#5923) Node 24 is the current LTS. Snapshot changes are due to undici v7 (bundled with Node 24) removing internal symbols from web spec objects like Response. --- .devcontainer/devcontainer.json | 4 +- .../actions/setup-node-and-install/action.yml | 2 +- .github/workflows/l10n-sync.yml | 2 +- .nvmrc | 2 +- package.json | 4 +- .../receive-profile.test.ts.snap | 252 +----------------- 6 files changed, 10 insertions(+), 256 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5257f37474..afc7cc4009 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,9 +1,9 @@ { "name": "Firefox Profiler", - "image": "mcr.microsoft.com/devcontainers/javascript-node:22", + "image": "mcr.microsoft.com/devcontainers/javascript-node:24", "features": { "ghcr.io/devcontainers/features/node:1": { - "version": "22" + "version": "24" } }, "forwardPorts": [4242], diff --git a/.github/actions/setup-node-and-install/action.yml b/.github/actions/setup-node-and-install/action.yml index 85a3a8ea8e..ca5192e5d4 100644 --- a/.github/actions/setup-node-and-install/action.yml +++ b/.github/actions/setup-node-and-install/action.yml @@ -6,7 +6,7 @@ runs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: '22.14' + node-version: '24.14' cache: 'yarn' - name: Install dependencies diff --git a/.github/workflows/l10n-sync.yml b/.github/workflows/l10n-sync.yml index b38183aea0..f59a296eae 100644 --- a/.github/workflows/l10n-sync.yml +++ b/.github/workflows/l10n-sync.yml @@ -35,7 +35,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: - node-version: '22.14' + node-version: '24.14' cache: 'yarn' - name: Configure git diff --git a/.nvmrc b/.nvmrc index 744ca17ec0..fd655f8a35 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.14 +24.14 diff --git a/package.json b/package.json index 7d417a3186..66225e7dbf 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "private": true, "engines": { - "node": ">= 22 < 23" + "node": ">= 24 < 25" }, "devEngines": { "runtime": { "name": "node", - "version": ">= 22 < 23" + "version": ">= 24 < 25" } }, "browser": { diff --git a/src/test/store/__snapshots__/receive-profile.test.ts.snap b/src/test/store/__snapshots__/receive-profile.test.ts.snap index ac9c30b543..04b089ee95 100644 --- a/src/test/store/__snapshots__/receive-profile.test.ts.snap +++ b/src/test/store/__snapshots__/receive-profile.test.ts.snap @@ -13,89 +13,7 @@ Array [ ], Array [ "Fetch response:", - Response { - Symbol(state): Object { - "aborted": false, - "body": Object { - "length": 7, - "source": Uint8Array [], - "stream": ReadableStream { - Symbol(kType): "ReadableStream", - Symbol(kState): Object { - "controller": ReadableByteStreamController { - Symbol(kType): "ReadableByteStreamController", - Symbol(kState): Object { - "autoAllocateChunkSize": undefined, - "byobRequest": null, - "cancelAlgorithm": undefined, - "closeRequested": false, - "highWaterMark": 0, - "pendingPullIntos": Array [], - "pullAgain": false, - "pullAlgorithm": undefined, - "pulling": false, - "queue": Array [], - "queueTotalSize": 0, - "started": true, - "stream": [Circular], - }, - }, - "disturbed": true, - "reader": ReadableStreamDefaultReader { - Symbol(kType): "ReadableStreamDefaultReader", - Symbol(kState): Object { - "close": Object { - "promise": Promise {}, - "reject": [Function], - "resolve": [Function], - }, - "readRequests": Array [], - "stream": [Circular], - }, - }, - "state": "closed", - "storedError": undefined, - "transfer": Object { - "port1": undefined, - "port2": undefined, - "promise": undefined, - "writable": undefined, - }, - }, - Symbol(nodejs.webstream.isClosedPromise): Object { - "promise": Promise {}, - "reject": [Function], - "resolve": [Function], - }, - Symbol(nodejs.webstream.controllerErrorFunction): [Function], - }, - }, - "cacheState": "", - "headersList": HeadersList { - "cookies": null, - Symbol(headers map): Map { - "content-length" => Object { - "name": "content-length", - "value": "7", - }, - "content-type" => Object { - "name": "content-type", - "value": "application/json", - }, - }, - Symbol(headers map sorted): null, - }, - "rangeRequested": false, - "requestIncludesCredentials": false, - "status": 200, - "statusText": "OK", - "timingAllowPassed": false, - "timingInfo": null, - "type": "default", - "urlList": Array [], - }, - Symbol(headers): Headers {}, - }, + Response {}, ], ] `; @@ -113,89 +31,7 @@ Array [ ], Array [ "Fetch response:", - Response { - Symbol(state): Object { - "aborted": false, - "body": Object { - "length": 7, - "source": Uint8Array [], - "stream": ReadableStream { - Symbol(kType): "ReadableStream", - Symbol(kState): Object { - "controller": ReadableByteStreamController { - Symbol(kType): "ReadableByteStreamController", - Symbol(kState): Object { - "autoAllocateChunkSize": undefined, - "byobRequest": null, - "cancelAlgorithm": undefined, - "closeRequested": false, - "highWaterMark": 0, - "pendingPullIntos": Array [], - "pullAgain": false, - "pullAlgorithm": undefined, - "pulling": false, - "queue": Array [], - "queueTotalSize": 0, - "started": true, - "stream": [Circular], - }, - }, - "disturbed": true, - "reader": ReadableStreamDefaultReader { - Symbol(kType): "ReadableStreamDefaultReader", - Symbol(kState): Object { - "close": Object { - "promise": Promise {}, - "reject": [Function], - "resolve": [Function], - }, - "readRequests": Array [], - "stream": [Circular], - }, - }, - "state": "closed", - "storedError": undefined, - "transfer": Object { - "port1": undefined, - "port2": undefined, - "promise": undefined, - "writable": undefined, - }, - }, - Symbol(nodejs.webstream.isClosedPromise): Object { - "promise": Promise {}, - "reject": [Function], - "resolve": [Function], - }, - Symbol(nodejs.webstream.controllerErrorFunction): [Function], - }, - }, - "cacheState": "", - "headersList": HeadersList { - "cookies": null, - Symbol(headers map): Map { - "content-length" => Object { - "name": "content-length", - "value": "7", - }, - "content-type" => Object { - "name": "content-type", - "value": "undefined", - }, - }, - Symbol(headers map sorted): null, - }, - "rangeRequested": false, - "requestIncludesCredentials": false, - "status": 200, - "statusText": "OK", - "timingAllowPassed": false, - "timingInfo": null, - "type": "default", - "urlList": Array [], - }, - Symbol(headers): Headers {}, - }, + Response {}, ], ] `; @@ -213,89 +49,7 @@ Array [ ], Array [ "Fetch response:", - Response { - Symbol(state): Object { - "aborted": false, - "body": Object { - "length": 4, - "source": Uint8Array [], - "stream": ReadableStream { - Symbol(kType): "ReadableStream", - Symbol(kState): Object { - "controller": ReadableByteStreamController { - Symbol(kType): "ReadableByteStreamController", - Symbol(kState): Object { - "autoAllocateChunkSize": undefined, - "byobRequest": null, - "cancelAlgorithm": undefined, - "closeRequested": false, - "highWaterMark": 0, - "pendingPullIntos": Array [], - "pullAgain": false, - "pullAlgorithm": undefined, - "pulling": false, - "queue": Array [], - "queueTotalSize": 0, - "started": true, - "stream": [Circular], - }, - }, - "disturbed": true, - "reader": ReadableStreamDefaultReader { - Symbol(kType): "ReadableStreamDefaultReader", - Symbol(kState): Object { - "close": Object { - "promise": Promise {}, - "reject": [Function], - "resolve": [Function], - }, - "readRequests": Array [], - "stream": [Circular], - }, - }, - "state": "closed", - "storedError": undefined, - "transfer": Object { - "port1": undefined, - "port2": undefined, - "promise": undefined, - "writable": undefined, - }, - }, - Symbol(nodejs.webstream.isClosedPromise): Object { - "promise": Promise {}, - "reject": [Function], - "resolve": [Function], - }, - Symbol(nodejs.webstream.controllerErrorFunction): [Function], - }, - }, - "cacheState": "", - "headersList": HeadersList { - "cookies": null, - Symbol(headers map): Map { - "content-length" => Object { - "name": "content-length", - "value": "4", - }, - "content-type" => Object { - "name": "content-type", - "value": "application/zip", - }, - }, - Symbol(headers map sorted): null, - }, - "rangeRequested": false, - "requestIncludesCredentials": false, - "status": 200, - "statusText": "OK", - "timingAllowPassed": false, - "timingInfo": null, - "type": "default", - "urlList": Array [], - }, - Symbol(headers): Headers {}, - }, + Response {}, ], ] `; From 938af543d9c8db3079c446208f0b5092e7e7d6c5 Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Sun, 29 Mar 2026 11:23:25 -0400 Subject: [PATCH 08/64] Use createStackTableBySkippingDiscarded in focusSubtree, focusFunction, and focusCategory. --- src/profile-logic/transforms.ts | 172 ++++++++++---------------------- 1 file changed, 51 insertions(+), 121 deletions(-) diff --git a/src/profile-logic/transforms.ts b/src/profile-logic/transforms.ts index c2220a331e..edd23a51f0 100644 --- a/src/profile-logic/transforms.ts +++ b/src/profile-logic/transforms.ts @@ -1257,29 +1257,18 @@ export function focusSubtree( const prefixDepth = callNodePath.length; const stackMatches = new Int32Array(stackTable.length); const funcMatchesImplementation = FUNC_MATCHES[implementation]; - const oldStackToNewStack: Map< - IndexIntoStackTable | null, - IndexIntoStackTable | null - > = new Map(); - // A root stack's prefix will be null. Maintain that relationship from old to new - // stacks by mapping from null to null. - oldStackToNewStack.set(null, null); - const newFrameCol = []; - const newPrefixCol = []; - const newCategoryCol = []; - const newSubcategoryCol = []; - let newLength = 0; + const oldStackToNewStack = new Int32Array(stackTable.length).fill(-1); + const newPrefixCol: Array = []; + const keepStack = makeBitSet(stackTable.length); for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) { const prefix = stackTable.prefix[stackIndex]; const prefixMatchesUpTo = prefix !== null ? stackMatches[prefix] : 0; let stackMatchesUpTo = -1; if (prefixMatchesUpTo !== -1) { - const frame = stackTable.frame[stackIndex]; - const category = stackTable.category[stackIndex]; - const subcategory = stackTable.subcategory[stackIndex]; if (prefixMatchesUpTo === prefixDepth) { stackMatchesUpTo = prefixDepth; } else { + const frame = stackTable.frame[stackIndex]; const funcIndex = frameTable.func[frame]; if (funcIndex === callNodePath[prefixMatchesUpTo]) { stackMatchesUpTo = prefixMatchesUpTo + 1; @@ -1288,41 +1277,25 @@ export function focusSubtree( } } if (stackMatchesUpTo === prefixDepth) { - const newStackIndex = newLength++; - const newStackPrefix = oldStackToNewStack.get(prefix); - newPrefixCol[newStackIndex] = newStackPrefix ?? null; - newFrameCol[newStackIndex] = frame; - newCategoryCol[newStackIndex] = category; - newSubcategoryCol[newStackIndex] = subcategory; - oldStackToNewStack.set(stackIndex, newStackIndex); + const prefixNewStack = + prefix === null ? -1 : oldStackToNewStack[prefix]; + oldStackToNewStack[stackIndex] = newPrefixCol.length; + newPrefixCol.push(prefixNewStack === -1 ? null : prefixNewStack); + setBit(keepStack, stackIndex); } } stackMatches[stackIndex] = stackMatchesUpTo; } - const newStackTable = { - frame: newFrameCol, - prefix: newPrefixCol, - category: new Uint8Array(newCategoryCol), - subcategory: - stackTable.subcategory instanceof Uint8Array - ? new Uint8Array(newSubcategoryCol) - : new Uint16Array(newSubcategoryCol), - length: newLength, - }; - - return updateThreadStacks(thread, newStackTable, (oldStack) => { - if (oldStack === null || stackMatches[oldStack] !== prefixDepth) { - return null; - } - const newStack = oldStackToNewStack.get(oldStack); - if (newStack === undefined) { - throw new Error( - 'Converting from the old stack to a new stack cannot be undefined' - ); - } - return newStack; - }); + const newStackTable = createStackTableBySkippingDiscarded( + stackTable, + newPrefixCol, + keepStack + ); + return applyTransformOutputToThread( + { newStackTable, effectOnThreadData: { oldStackToNewStack } }, + thread + ); }); } @@ -1386,53 +1359,32 @@ export function focusFunction( ): Thread { return timeCode('focusFunction', () => { const { stackTable, frameTable } = thread; - // A map oldStack -> newStack+1, implemented as a Uint32Array for performance. - // If newStack+1 is zero it means "null", i.e. this stack was filtered out. - // Typed arrays are initialized to zero, which we interpret as null. - const oldStackToNewStackPlusOne = new Uint32Array(stackTable.length); - - const newFrameCol = []; - const newPrefixCol = []; - const newCategoryCol = []; - const newSubcategoryCol = []; - let newLength = 0; + const oldStackToNewStack = new Int32Array(stackTable.length).fill(-1); + const newPrefixCol: Array = []; + const keepStack = makeBitSet(stackTable.length); for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) { const prefix = stackTable.prefix[stackIndex]; const frameIndex = stackTable.frame[stackIndex]; const funcIndex = frameTable.func[frameIndex]; - const newPrefixPlusOne = - prefix === null ? 0 : oldStackToNewStackPlusOne[prefix]; - const newPrefix = newPrefixPlusOne === 0 ? null : newPrefixPlusOne - 1; - if (newPrefix !== null || funcIndex === funcIndexToFocus) { - const newStackIndex = newLength++; - newPrefixCol[newStackIndex] = newPrefix; - newFrameCol[newStackIndex] = frameIndex; - newCategoryCol[newStackIndex] = stackTable.category[stackIndex]; - newSubcategoryCol[newStackIndex] = stackTable.subcategory[stackIndex]; - oldStackToNewStackPlusOne[stackIndex] = newStackIndex + 1; + const prefixNewStack = prefix === null ? -1 : oldStackToNewStack[prefix]; + if (prefixNewStack !== -1 || funcIndex === funcIndexToFocus) { + oldStackToNewStack[stackIndex] = newPrefixCol.length; + newPrefixCol.push(prefixNewStack === -1 ? null : prefixNewStack); + setBit(keepStack, stackIndex); } } - const newStackTable = { - frame: newFrameCol, - prefix: newPrefixCol, - category: new Uint8Array(newCategoryCol), - subcategory: - stackTable.subcategory instanceof Uint8Array - ? new Uint8Array(newSubcategoryCol) - : new Uint16Array(newSubcategoryCol), - length: newLength, - }; - - return updateThreadStacks(thread, newStackTable, (oldStack) => { - if (oldStack === null) { - return null; - } - const newStackPlusOne = oldStackToNewStackPlusOne[oldStack]; - return newStackPlusOne === 0 ? null : newStackPlusOne - 1; - }); + const newStackTable = createStackTableBySkippingDiscarded( + stackTable, + newPrefixCol, + keepStack + ); + return applyTransformOutputToThread( + { newStackTable, effectOnThreadData: { oldStackToNewStack } }, + thread + ); }); } @@ -1498,56 +1450,34 @@ export function focusSelf( export function focusCategory(thread: Thread, category: IndexIntoCategoryList) { return timeCode('focusCategory', () => { const { stackTable } = thread; - const oldStackToNewStack: Map< - IndexIntoStackTable | null, - IndexIntoStackTable | null - > = new Map(); - oldStackToNewStack.set(null, null); - - const newFrameCol = []; - const newPrefixCol = []; - const newCategoryCol = []; - const newSubcategoryCol = []; - let newLength = 0; + const oldStackToNewStack = new Int32Array(stackTable.length).fill(-1); + const newPrefixCol: Array = []; + const keepStack = makeBitSet(stackTable.length); // fill the new stack table with the kept frames for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) { const prefix = stackTable.prefix[stackIndex]; - const newPrefix = oldStackToNewStack.get(prefix); - if (newPrefix === undefined) { - throw new Error('The prefix should not map to an undefined value'); - } + const prefixNewStack = prefix === null ? -1 : oldStackToNewStack[prefix]; if (stackTable.category[stackIndex] !== category) { - oldStackToNewStack.set(stackIndex, newPrefix); + oldStackToNewStack[stackIndex] = prefixNewStack; continue; } - const newStackIndex = newLength++; - newPrefixCol[newStackIndex] = newPrefix; - newFrameCol[newStackIndex] = stackTable.frame[stackIndex]; - newCategoryCol[newStackIndex] = stackTable.category[stackIndex]; - newSubcategoryCol[newStackIndex] = stackTable.subcategory[stackIndex]; - oldStackToNewStack.set(stackIndex, newStackIndex); + oldStackToNewStack[stackIndex] = newPrefixCol.length; + newPrefixCol.push(prefixNewStack === -1 ? null : prefixNewStack); + setBit(keepStack, stackIndex); } - const newStackTable = { - frame: newFrameCol, - prefix: newPrefixCol, - category: new Uint8Array(newCategoryCol), - subcategory: - stackTable.subcategory instanceof Uint8Array - ? new Uint8Array(newSubcategoryCol) - : new Uint16Array(newSubcategoryCol), - length: newLength, - }; - - const updated = updateThreadStacks( - thread, - newStackTable, - getMapStackUpdater(oldStackToNewStack) + const newStackTable = createStackTableBySkippingDiscarded( + stackTable, + newPrefixCol, + keepStack + ); + return applyTransformOutputToThread( + { newStackTable, effectOnThreadData: { oldStackToNewStack } }, + thread ); - return updated; }); } From bc322140c7cdc3fa6b83cf7d454697ffcf8f9cde Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Thu, 2 Apr 2026 09:47:14 -0400 Subject: [PATCH 09/64] Change mergeCallNode to only create a new prefix column, like mergeFunction. --- src/profile-logic/transforms.ts | 133 +++++++++++++------------------- 1 file changed, 54 insertions(+), 79 deletions(-) diff --git a/src/profile-logic/transforms.ts b/src/profile-logic/transforms.ts index edd23a51f0..05ba86b2bc 100644 --- a/src/profile-logic/transforms.ts +++ b/src/profile-logic/transforms.ts @@ -10,7 +10,6 @@ import { toValidImplementationFilter, updateThreadStacks, updateThreadStacksByGeneratingNewStackColumns, - getMapStackUpdater, getOriginAnnotationForFunc, createStackTableBySkippingDiscarded, applyTransformOutputToThread, @@ -759,102 +758,78 @@ export function mergeCallNode( ): Thread { return timeCode('mergeCallNode', () => { const { stackTable, frameTable } = thread; - // Depth here is 0 indexed. - const depthAtCallNodePathLeaf = callNodePath.length - 1; - const oldStackToNewStack: Map< + + // Maps merged stacks to their effective parent (the stack that samples pointing + // to the merged stack should be attributed to). Only contains entries for merged + // stacks; the vast majority of stacks are not merged and map to themselves. + const mergedStackToEffectiveParent = new Map< IndexIntoStackTable | null, IndexIntoStackTable | null - > = new Map(); - // A root stack's prefix will be null. Maintain that relationship from old to new - // stacks by mapping from null to null. - oldStackToNewStack.set(null, null); - const newFrameCol = []; - const newPrefixCol = []; - const newCategoryCol = []; - const newSubcategoryCol = []; - let newLength = 0; - // Provide two arrays to efficiently cache values for the algorithm. This probably - // could be refactored to use only one array here. - const stackDepths = []; - const stackMatches = []; + >(); + const newPrefixCol = new Array( + stackTable.length + ); + const funcMatchesImplementation = FUNC_MATCHES[implementation]; + + const callNodePathLength = callNodePath.length; + // A map to keep track of whether a stack matches (part of) the call node path. + // If undefined: no match + // Otherwise: length of the partial match, including this stack + // All values are < callNodePathLength. + const partialMatchLengthAtStack = new Map< + IndexIntoStackTable | null, + number + >(); + partialMatchLengthAtStack.set(null, 0); for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) { const prefix = stackTable.prefix[stackIndex]; + + // If our prefix got merged away, remap it to its parent. + const parentOfPrefix = mergedStackToEffectiveParent.get(prefix); + const effectivePrefix = + parentOfPrefix !== undefined ? parentOfPrefix : prefix; + newPrefixCol[stackIndex] = effectivePrefix; + + const prefixPartialMatchLength = partialMatchLengthAtStack.get(prefix); + if (prefixPartialMatchLength === undefined) { + // No match, nothing else to do here + continue; + } + + // Now we know that this stack's prefix was a (partial) match for our CallNodePath. const frameIndex = stackTable.frame[stackIndex]; - const category = stackTable.category[stackIndex]; - const subcategory = stackTable.subcategory[stackIndex]; const funcIndex = frameTable.func[frameIndex]; - const doesPrefixMatch = prefix === null ? true : stackMatches[prefix]; - const prefixDepth: number = prefix === null ? -1 : stackDepths[prefix]; - const currentFuncOnPath = callNodePath[prefixDepth + 1]; - - let doMerge = false; - let stackDepth: number = prefixDepth; - let doesMatchCallNodePath; - if (doesPrefixMatch && stackDepth < depthAtCallNodePathLeaf) { - // This stack's prefixes were in our CallNodePath. - if (currentFuncOnPath === funcIndex) { - // This stack's function matches too! - doesMatchCallNodePath = true; - if (stackDepth + 1 === depthAtCallNodePathLeaf) { - // Holy cow, we found a match for our merge operation and can merge this stack. - doMerge = true; - } else { - // Since we found a match, increase the stack depth. This should match - // the depth of the implementation filtered stacks. - stackDepth++; - } - } else if (!funcMatchesImplementation(thread, funcIndex)) { - // This stack's function does not match the CallNodePath, however it's not part - // of the CallNodePath's implementation filter. Go ahead and keep it. - doesMatchCallNodePath = true; + if (funcIndex === callNodePath[prefixPartialMatchLength]) { + // This stack's function matches too! + const matchLength = prefixPartialMatchLength + 1; + if (matchLength === callNodePathLength) { + // The entire path matched and we found a node that needs to be merged away. + mergedStackToEffectiveParent.set(stackIndex, effectivePrefix); } else { - // While all of the predecessors matched, this stack's function does not :( - doesMatchCallNodePath = false; + // Not reached the end yet, store the partial match length. + partialMatchLengthAtStack.set(stackIndex, matchLength); } + } else if (!funcMatchesImplementation(thread, funcIndex)) { + // This stack's function does not match the CallNodePath, however it's not part + // of the CallNodePath's implementation filter. Inherit the parent's partial + // match length + partialMatchLengthAtStack.set(stackIndex, prefixPartialMatchLength); } else { - // This stack is not part of a matching branch of the tree. - doesMatchCallNodePath = false; - } - stackMatches[stackIndex] = doesMatchCallNodePath; - stackDepths[stackIndex] = stackDepth; - - // Map the oldStackToNewStack, and only push on the stacks that aren't merged. - if (doMerge) { - const newStackPrefix = oldStackToNewStack.get(prefix); - oldStackToNewStack.set( - stackIndex, - newStackPrefix === undefined ? null : newStackPrefix - ); - } else { - const newStackIndex = newLength++; - const newStackPrefix = oldStackToNewStack.get(prefix); - newPrefixCol[newStackIndex] = - newStackPrefix === undefined ? null : newStackPrefix; - newFrameCol[newStackIndex] = frameIndex; - newCategoryCol[newStackIndex] = category; - newSubcategoryCol[newStackIndex] = subcategory; - oldStackToNewStack.set(stackIndex, newStackIndex); + // While all of the predecessors matched, this stack's function does not :( } } const newStackTable = { - frame: newFrameCol, + ...stackTable, prefix: newPrefixCol, - category: new Uint8Array(newCategoryCol), - subcategory: - stackTable.subcategory instanceof Uint8Array - ? new Uint8Array(newSubcategoryCol) - : new Uint16Array(newSubcategoryCol), - length: newLength, }; - return updateThreadStacks( - thread, - newStackTable, - getMapStackUpdater(oldStackToNewStack) - ); + return updateThreadStacks(thread, newStackTable, (oldStack) => { + const effectiveParent = mergedStackToEffectiveParent.get(oldStack); + return effectiveParent !== undefined ? effectiveParent : oldStack; + }); }); } From 1244dfcbc4cfc1fcd1fbbe8349c45b319d27dafd Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Fri, 20 Mar 2026 15:25:24 -0400 Subject: [PATCH 10/64] Update the hovered item when panning any viewport canvas. Fixes #3807. --- src/components/shared/chart/Canvas.tsx | 41 +++++++++++++------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/src/components/shared/chart/Canvas.tsx b/src/components/shared/chart/Canvas.tsx index b2dc0380e7..4f12710e52 100644 --- a/src/components/shared/chart/Canvas.tsx +++ b/src/components/shared/chart/Canvas.tsx @@ -85,10 +85,9 @@ export class ChartCanvas extends React.Component< State > { _devicePixelRatio: number = 1; - // The current mouse position. Needs to be stored for tooltip - // hit-test if props update. - _offsetX: CssPixels = 0; - _offsetY: CssPixels = 0; + // The current mouse position inside the canvas, or null if the mouse + // is outside. Needs to be stored for tooltip hit-test if props update. + _mousePosition: { x: CssPixels; y: CssPixels } | null = null; // The position of the most recent mouse down event. Needed for // comparison with the current mouse position in order to // distinguish between clicks and drags. @@ -267,8 +266,9 @@ export class ChartCanvas extends React.Component< this.props.onMouseMove(event); } - this._offsetX = event.nativeEvent.offsetX; - this._offsetY = event.nativeEvent.offsetY; + const offsetX = event.nativeEvent.offsetX; + const offsetY = event.nativeEvent.offsetY; + this._mousePosition = { x: offsetX, y: offsetY }; // event.buttons is a bitfield representing which buttons are pressed at the // time of the mousemove event. The first bit is for the left click. // This operation checks if the left button is clicked, but this will also @@ -281,15 +281,15 @@ export class ChartCanvas extends React.Component< if ( !this._mouseMovedWhileClicked && hasLeftClick && - (Math.abs(this._offsetX - this._mouseDownOffsetX) > + (Math.abs(offsetX - this._mouseDownOffsetX) > MOUSE_CLICK_MAX_MOVEMENT_DELTA || - Math.abs(this._offsetY - this._mouseDownOffsetY) > + Math.abs(offsetY - this._mouseDownOffsetY) > MOUSE_CLICK_MAX_MOVEMENT_DELTA) ) { this._mouseMovedWhileClicked = true; } - const maybeHoveredItem = this.props.hitTest(this._offsetX, this._offsetY); + const maybeHoveredItem = this.props.hitTest(offsetX, offsetY); if (maybeHoveredItem !== null) { if (this.state.selectedItem === null) { // Update both the hovered item and the pageX and pageY values. The @@ -323,6 +323,7 @@ export class ChartCanvas extends React.Component< }; _onMouseOut = () => { + this._mousePosition = null; if ( this.state.hoveredItem !== null && // This persistTooltips property is part of the web console API. It helps @@ -390,18 +391,16 @@ export class ChartCanvas extends React.Component< }; override UNSAFE_componentWillReceiveProps() { - // It is possible that the data backing the chart has been - // changed, for instance after symbolication. Clear the - // hoveredItem if the mouse no longer hovers over it. - const { hoveredItem } = this.state; - if ( - hoveredItem !== null && - !hoveredItemsAreEqual( - this.props.hitTest(this._offsetX, this._offsetY), - hoveredItem - ) - ) { - this.setState({ hoveredItem: null }); + // Update the hovered item if the rendered data has changed or if + // the chart has been scrolled so that a new element is under the + // mouse cursor. + if (!this._mousePosition) { + return; + } + const { x, y } = this._mousePosition; + const newHoveredItem = this.props.hitTest(x, y); + if (!hoveredItemsAreEqual(newHoveredItem, this.state.hoveredItem)) { + this.setState({ hoveredItem: newHoveredItem }); } } From 3682333ca384dfedcb6bb2475af7f2bea236fc97 Mon Sep 17 00:00:00 2001 From: Pontoon Date: Mon, 6 Apr 2026 15:10:40 +0000 Subject: [PATCH 11/64] Pontoon/Firefox Profiler: Update Portuguese (Brazil) (pt-BR) Co-authored-by: Marcelo Ghelman (pt-BR) --- locales/pt-BR/app.ftl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/locales/pt-BR/app.ftl b/locales/pt-BR/app.ftl index 10e9ff686a..cb0ad49bad 100644 --- a/locales/pt-BR/app.ftl +++ b/locales/pt-BR/app.ftl @@ -284,6 +284,13 @@ Home--enable-button-unavailable = # This message can be seen on https://main--perf-html.netlify.app/ . Home--web-channel-unavailable = Esta instância do profiler não conseguiu se conectar ao WebChannel. Isso geralmente significa que está sendo executado em um host diferente daquele especificado na preferência devtools.performance.recording.ui-base-url. Se você quiser capturar novos profiles com esta instância e dar a ela controle programático do botão de menu do profiler, pode ir em about: config e alterar a preferência. Home--record-instructions = Para iniciar a gravação de um profile, clique no botão de gravação de profile ou use os atalhos de teclado. O ícone fica azul quando um profile está sendo gravado. Use Capturar para carregar os dados no profiler.firefox.com. +Home--instructions-content2 = + A gravação de profiles de desempenho requer o { -firefox-brand-name } de computador. + No entanto, profiles existentes podem ser vistos em qualquer navegador moderno. +Home--fenix-instructions-directly = É possível criar profiles do { -firefox-android-brand-name } diretamente neste dispositivo. Para mais informações, consulte Como criar profiles do { -firefox-android-brand-name } diretamente no dispositivo. +Home--fenix-instructions-remotely = + Você também pode criar profiles do { -firefox-android-brand-name } remotamente a partir do { -firefox-brand-name } de computador. Para mais informações, consulte esta documentação: + Como criar profiles do { -firefox-android-brand-name } remotamente. Home--record-instructions-start-stop = Interrompa e inicie a gravação de profiles Home--record-instructions-capture-load = Capture e carregue um profile Home--profiler-motto = Capture um profile de desempenho. Analise. Compartilhe. Torne a web mais rápida. From a3dc3e63defee97bfee70453a137fda9dd535503 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 11:59:02 +0200 Subject: [PATCH 12/64] Bump lodash from 4.17.23 to 4.18.1 (#5933) Bumps [lodash](https://github.com/lodash/lodash) from 4.17.23 to 4.18.1. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.23...4.18.1) --- updated-dependencies: - dependency-name: lodash dependency-version: 4.18.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index bd6dc08b88..2d0e125d2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7323,9 +7323,9 @@ lodash.truncate@^4.4.2: integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21: - version "4.17.23" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a" - integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w== + version "4.18.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" + integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== log-symbols@^4.1.0: version "4.1.0" From 99f4176a580f8fde0f44afb4afcdac494a4bdab6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Tue, 7 Apr 2026 12:24:51 +0200 Subject: [PATCH 13/64] Fallback to javascript highlighting in the source view as a backup (#5936) --- src/components/shared/SourceView-codemirror.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/shared/SourceView-codemirror.ts b/src/components/shared/SourceView-codemirror.ts index 5365efbe54..6ddf2823b0 100644 --- a/src/components/shared/SourceView-codemirror.ts +++ b/src/components/shared/SourceView-codemirror.ts @@ -77,7 +77,11 @@ function _languageExtForPath( ) { return cpp(); } - return []; + + // Fallback to JavaScript highlighting. Inline scripts share the page URL, so + // their path won't have a .js extension. This may be incorrect for + // unknown/unsupported file types, but is the best guess for the common case. + return javascript(); } // Adjustments to make a CodeMirror editor work as a non-editable code viewer. From a94ccca63c9ccfd1b6433e810949e64e5b5f0f1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Tue, 7 Apr 2026 12:39:54 +0200 Subject: [PATCH 14/64] Properly type the return value of _languageExtForPath (#5937) --- src/components/shared/SourceView-codemirror.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/shared/SourceView-codemirror.ts b/src/components/shared/SourceView-codemirror.ts index 6ddf2823b0..ea61ab2357 100644 --- a/src/components/shared/SourceView-codemirror.ts +++ b/src/components/shared/SourceView-codemirror.ts @@ -20,7 +20,7 @@ */ import { EditorView, lineNumbers } from '@codemirror/view'; import { EditorState, Compartment } from '@codemirror/state'; -import { syntaxHighlighting } from '@codemirror/language'; +import { type LanguageSupport, syntaxHighlighting } from '@codemirror/language'; import { classHighlighter } from '@lezer/highlight'; import { cpp } from '@codemirror/lang-cpp'; import { rust } from '@codemirror/lang-rust'; @@ -46,9 +46,7 @@ const highlightedLineConf = new Compartment(); const lineNumbersConf = new Compartment(); // Detect the right language based on the file extension. -function _languageExtForPath( - path: string | null -): any /* LanguageSupport | [] */ { +function _languageExtForPath(path: string | null): LanguageSupport | [] { if (path === null) { return []; } From 64becd049a6443681ab2e5ac537fcfb7d25060c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Tue, 7 Apr 2026 14:32:25 +0200 Subject: [PATCH 15/64] Update typescript eslint dependencies (#5938) `yarn lint-js` was showing a warning, saying that the version of typescript wasn't supported. This was due to the fact that `@typescript-eslint` didn't officially support TS 6. This is now resolved in the new versions. --- package.json | 6 +- yarn.lock | 183 ++++++++++++++++++++++++++------------------------- 2 files changed, 97 insertions(+), 92 deletions(-) diff --git a/package.json b/package.json index 66225e7dbf..43354850f9 100644 --- a/package.json +++ b/package.json @@ -134,8 +134,8 @@ "@types/react-transition-group": "^4.4.5", "@types/redux-logger": "^3.0.6", "@types/tgwf__co2": "^0.14.2", - "@typescript-eslint/eslint-plugin": "^8.56.0", - "@typescript-eslint/parser": "^8.56.0", + "@typescript-eslint/eslint-plugin": "^8.58.0", + "@typescript-eslint/parser": "^8.58.0", "alex": "^11.0.1", "babel-jest": "^30.2.0", "babel-plugin-module-resolver": "^5.0.2", @@ -177,7 +177,7 @@ "stylelint-config-idiomatic-order": "^10.0.0", "stylelint-config-standard": "^40.0.0", "typescript": "^6.0.2", - "typescript-eslint": "^8.56.0", + "typescript-eslint": "^8.58.0", "workbox-cli": "^7.4.0", "yargs": "^18.0.0" }, diff --git a/yarn.lock b/yarn.lock index 2d0e125d2b..dd43ea0c55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2489,38 +2489,29 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@8.56.0", "@typescript-eslint/eslint-plugin@^8.56.0": - version "8.56.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz#5aec3db807a6b8437ea5d5ebf7bd16b4119aba8d" - integrity sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw== +"@typescript-eslint/eslint-plugin@8.58.0", "@typescript-eslint/eslint-plugin@^8.58.0": + version "8.58.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz#ad40e492f1931f46da1bd888e52b9e56df9063aa" + integrity sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg== dependencies: "@eslint-community/regexpp" "^4.12.2" - "@typescript-eslint/scope-manager" "8.56.0" - "@typescript-eslint/type-utils" "8.56.0" - "@typescript-eslint/utils" "8.56.0" - "@typescript-eslint/visitor-keys" "8.56.0" + "@typescript-eslint/scope-manager" "8.58.0" + "@typescript-eslint/type-utils" "8.58.0" + "@typescript-eslint/utils" "8.58.0" + "@typescript-eslint/visitor-keys" "8.58.0" ignore "^7.0.5" natural-compare "^1.4.0" - ts-api-utils "^2.4.0" - -"@typescript-eslint/parser@8.56.0", "@typescript-eslint/parser@^8.56.0": - version "8.56.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.56.0.tgz#8ecff1678b8b1a742d29c446ccf5eeea7f971d72" - integrity sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg== - dependencies: - "@typescript-eslint/scope-manager" "8.56.0" - "@typescript-eslint/types" "8.56.0" - "@typescript-eslint/typescript-estree" "8.56.0" - "@typescript-eslint/visitor-keys" "8.56.0" - debug "^4.4.3" + ts-api-utils "^2.5.0" -"@typescript-eslint/project-service@8.56.0": - version "8.56.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.56.0.tgz#bb8562fecd8f7922e676fc6a1189c20dd7991d73" - integrity sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg== +"@typescript-eslint/parser@8.58.0", "@typescript-eslint/parser@^8.58.0": + version "8.58.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.58.0.tgz#da04ece1967b6c2fe8f10c3473dabf3825795ef7" + integrity sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA== dependencies: - "@typescript-eslint/tsconfig-utils" "^8.56.0" - "@typescript-eslint/types" "^8.56.0" + "@typescript-eslint/scope-manager" "8.58.0" + "@typescript-eslint/types" "8.58.0" + "@typescript-eslint/typescript-estree" "8.58.0" + "@typescript-eslint/visitor-keys" "8.58.0" debug "^4.4.3" "@typescript-eslint/project-service@8.56.1": @@ -2532,13 +2523,14 @@ "@typescript-eslint/types" "^8.56.1" debug "^4.4.3" -"@typescript-eslint/scope-manager@8.56.0": - version "8.56.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz#604030a4c6433df3728effdd441d47f45a86edb4" - integrity sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w== +"@typescript-eslint/project-service@8.58.0": + version "8.58.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.58.0.tgz#66ceda0aabf7427aec3e2713fa43eb278dead2aa" + integrity sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg== dependencies: - "@typescript-eslint/types" "8.56.0" - "@typescript-eslint/visitor-keys" "8.56.0" + "@typescript-eslint/tsconfig-utils" "^8.58.0" + "@typescript-eslint/types" "^8.58.0" + debug "^4.4.3" "@typescript-eslint/scope-manager@8.56.1", "@typescript-eslint/scope-manager@^8.56.0": version "8.56.1" @@ -2548,51 +2540,44 @@ "@typescript-eslint/types" "8.56.1" "@typescript-eslint/visitor-keys" "8.56.1" -"@typescript-eslint/tsconfig-utils@8.56.0": - version "8.56.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz#2538ce83cbc376e685487960cbb24b65fe2abc4e" - integrity sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg== +"@typescript-eslint/scope-manager@8.58.0": + version "8.58.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz#e304142775e49a1b7ac3c8bf2536714447c72cab" + integrity sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ== + dependencies: + "@typescript-eslint/types" "8.58.0" + "@typescript-eslint/visitor-keys" "8.58.0" -"@typescript-eslint/tsconfig-utils@8.56.1", "@typescript-eslint/tsconfig-utils@^8.56.0", "@typescript-eslint/tsconfig-utils@^8.56.1": +"@typescript-eslint/tsconfig-utils@8.56.1", "@typescript-eslint/tsconfig-utils@^8.56.1": version "8.56.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz#1afa830b0fada5865ddcabdc993b790114a879b7" integrity sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ== -"@typescript-eslint/type-utils@8.56.0": - version "8.56.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz#72b4edc1fc73988998f1632b3ec99c2a66eaac6e" - integrity sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA== +"@typescript-eslint/tsconfig-utils@8.58.0", "@typescript-eslint/tsconfig-utils@^8.58.0": + version "8.58.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz#c5a8edb21f31e0fdee565724e1b984171c559482" + integrity sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A== + +"@typescript-eslint/type-utils@8.58.0": + version "8.58.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.58.0.tgz#ce0e72cd967ffbbe8de322db6089bd4374be352f" + integrity sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg== dependencies: - "@typescript-eslint/types" "8.56.0" - "@typescript-eslint/typescript-estree" "8.56.0" - "@typescript-eslint/utils" "8.56.0" + "@typescript-eslint/types" "8.58.0" + "@typescript-eslint/typescript-estree" "8.58.0" + "@typescript-eslint/utils" "8.58.0" debug "^4.4.3" - ts-api-utils "^2.4.0" - -"@typescript-eslint/types@8.56.0": - version "8.56.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.56.0.tgz#a2444011b9a98ca13d70411d2cbfed5443b3526a" - integrity sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ== + ts-api-utils "^2.5.0" -"@typescript-eslint/types@8.56.1", "@typescript-eslint/types@^8.56.0", "@typescript-eslint/types@^8.56.1": +"@typescript-eslint/types@8.56.1", "@typescript-eslint/types@^8.56.1": version "8.56.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.56.1.tgz#975e5942bf54895291337c91b9191f6eb0632ab9" integrity sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw== -"@typescript-eslint/typescript-estree@8.56.0": - version "8.56.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz#fadbc74c14c5bac947db04980ff58bb178701c2e" - integrity sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q== - dependencies: - "@typescript-eslint/project-service" "8.56.0" - "@typescript-eslint/tsconfig-utils" "8.56.0" - "@typescript-eslint/types" "8.56.0" - "@typescript-eslint/visitor-keys" "8.56.0" - debug "^4.4.3" - minimatch "^9.0.5" - semver "^7.7.3" - tinyglobby "^0.2.15" - ts-api-utils "^2.4.0" +"@typescript-eslint/types@8.58.0", "@typescript-eslint/types@^8.58.0": + version "8.58.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.58.0.tgz#e94ae7abdc1c6530e71183c1007b61fa93112a5a" + integrity sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww== "@typescript-eslint/typescript-estree@8.56.1": version "8.56.1" @@ -2609,15 +2594,30 @@ tinyglobby "^0.2.15" ts-api-utils "^2.4.0" -"@typescript-eslint/utils@8.56.0": - version "8.56.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.56.0.tgz#063ce6f702ec603de1b83ee795ed5e877d6f7841" - integrity sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ== +"@typescript-eslint/typescript-estree@8.58.0": + version "8.58.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz#ed233faa8e2f2a2e1357c3e7d553d6465a0ee59a" + integrity sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA== + dependencies: + "@typescript-eslint/project-service" "8.58.0" + "@typescript-eslint/tsconfig-utils" "8.58.0" + "@typescript-eslint/types" "8.58.0" + "@typescript-eslint/visitor-keys" "8.58.0" + debug "^4.4.3" + minimatch "^10.2.2" + semver "^7.7.3" + tinyglobby "^0.2.15" + ts-api-utils "^2.5.0" + +"@typescript-eslint/utils@8.58.0": + version "8.58.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.58.0.tgz#21a74a7963b0d288b719a4121c7dd555adaab3c3" + integrity sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA== dependencies: "@eslint-community/eslint-utils" "^4.9.1" - "@typescript-eslint/scope-manager" "8.56.0" - "@typescript-eslint/types" "8.56.0" - "@typescript-eslint/typescript-estree" "8.56.0" + "@typescript-eslint/scope-manager" "8.58.0" + "@typescript-eslint/types" "8.58.0" + "@typescript-eslint/typescript-estree" "8.58.0" "@typescript-eslint/utils@^8.0.0", "@typescript-eslint/utils@^8.56.0": version "8.56.1" @@ -2629,14 +2629,6 @@ "@typescript-eslint/types" "8.56.1" "@typescript-eslint/typescript-estree" "8.56.1" -"@typescript-eslint/visitor-keys@8.56.0": - version "8.56.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz#7d6592ab001827d3ce052155edf7ecad19688d7d" - integrity sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg== - dependencies: - "@typescript-eslint/types" "8.56.0" - eslint-visitor-keys "^5.0.0" - "@typescript-eslint/visitor-keys@8.56.1": version "8.56.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz#50e03475c33a42d123dc99e63acf1841c0231f87" @@ -2645,6 +2637,14 @@ "@typescript-eslint/types" "8.56.1" eslint-visitor-keys "^5.0.0" +"@typescript-eslint/visitor-keys@8.58.0": + version "8.58.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz#2abd55a4be70fd55967aceaba4330b9ba9f45189" + integrity sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ== + dependencies: + "@typescript-eslint/types" "8.58.0" + eslint-visitor-keys "^5.0.0" + "@ungap/structured-clone@^1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" @@ -8286,7 +8286,7 @@ minimatch@^8.0.2: dependencies: brace-expansion "^2.0.1" -minimatch@^9.0.4, minimatch@^9.0.5: +minimatch@^9.0.4: version "9.0.5" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== @@ -10928,6 +10928,11 @@ ts-api-utils@^2.4.0: resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.4.0.tgz#2690579f96d2790253bdcf1ca35d569ad78f9ad8" integrity sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA== +ts-api-utils@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz#4acd4a155e22734990a5ed1fe9e97f113bcb37c1" + integrity sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA== + tsconfig-paths@^3.15.0: version "3.15.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" @@ -11080,15 +11085,15 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typescript-eslint@^8.56.0: - version "8.56.0" - resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.56.0.tgz#f4686ccaaf2fb86daf0133820da40ca5961a2236" - integrity sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg== +typescript-eslint@^8.58.0: + version "8.58.0" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.58.0.tgz#5758b1b68ae7ec05d756b98c63a1f6953a01172b" + integrity sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA== dependencies: - "@typescript-eslint/eslint-plugin" "8.56.0" - "@typescript-eslint/parser" "8.56.0" - "@typescript-eslint/typescript-estree" "8.56.0" - "@typescript-eslint/utils" "8.56.0" + "@typescript-eslint/eslint-plugin" "8.58.0" + "@typescript-eslint/parser" "8.58.0" + "@typescript-eslint/typescript-estree" "8.58.0" + "@typescript-eslint/utils" "8.58.0" typescript@^6.0.2: version "6.0.2" From b41e166a1d6cfc797002ee25f71fa1b7fdc62e7b Mon Sep 17 00:00:00 2001 From: Paul Adenot Date: Tue, 7 Apr 2026 19:24:38 +0200 Subject: [PATCH 16/64] Fix extractGeckoLogs for structured Log marker format (bug 2022540) (#5927) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug 2022540 switched Log markers from {module, name} fields to structured {level, message} fields, with the module name stored as the marker name. The old format used a single module string that could include the log level prefix (e.g. D/cubeb); the new format uses an integer level field. Profiles use one format exclusively, never a mix. We detect which by checking the first Log marker found in each thread. Co-authored-by: Nazım Can Altınova --- src/test/unit/window-console.test.ts | 50 ++++++++++++++++++++++++++++ src/types/markers.ts | 16 ++++++--- src/utils/window-console.ts | 48 +++++++++++++++----------- 3 files changed, 90 insertions(+), 24 deletions(-) diff --git a/src/test/unit/window-console.test.ts b/src/test/unit/window-console.test.ts index 82c939f7c3..8dbd8a0a3f 100644 --- a/src/test/unit/window-console.test.ts +++ b/src/test/unit/window-console.test.ts @@ -112,6 +112,56 @@ describe('console-accessible values on the window object', function () { `); }); + it('can extract gecko logs in new structured format', function () { + const profile = getProfileWithMarkers([ + [ + 'nsHttp', + 170, + null, + { + type: 'Log', + level: 1, + message: + 'ParentChannelListener::ParentChannelListener [this=7fb5e19b98d0]', + }, + ], + [ + 'nsJarProtocol', + 190, + null, + { + type: 'Log', + level: 2, + message: 'nsJARChannel::nsJARChannel [this=0x87f1ec80]\n', + }, + ], + ['cubeb', 200, null, { type: 'Log', level: 3, message: 'cubeb_init' }], + [ + 'AudioStream', + 210, + null, + { type: 'Log', level: 4, message: 'AudioStream init\n' }, + ], + [ + 'VideoSink', + 220, + null, + { type: 'Log', level: 5, message: 'VideoSink::VideoSink' }, + ], + ]); + const store = storeWithProfile(profile); + const target: MixedObject = {}; + addDataToWindowObject(store.getState, store.dispatch, target); + const result = (target as any).extractGeckoLogs(); + expect(result).toBe(stripIndent` + 1970-01-01 00:00:00.170000000 UTC - [Unknown Process 0: Empty]: E/nsHttp ParentChannelListener::ParentChannelListener [this=7fb5e19b98d0] + 1970-01-01 00:00:00.190000000 UTC - [Unknown Process 0: Empty]: W/nsJarProtocol nsJARChannel::nsJARChannel [this=0x87f1ec80] + 1970-01-01 00:00:00.200000000 UTC - [Unknown Process 0: Empty]: I/cubeb cubeb_init + 1970-01-01 00:00:00.210000000 UTC - [Unknown Process 0: Empty]: D/AudioStream AudioStream init + 1970-01-01 00:00:00.220000000 UTC - [Unknown Process 0: Empty]: V/VideoSink VideoSink::VideoSink + `); + }); + describe('totalMarkerDuration', function () { function setup(): ExtraPropertiesOnWindowForConsole { jest.spyOn(console, 'log').mockImplementation(() => {}); diff --git a/src/types/markers.ts b/src/types/markers.ts index 7c2d63d4a8..8f5f5e5005 100644 --- a/src/types/markers.ts +++ b/src/types/markers.ts @@ -634,11 +634,17 @@ export type ChromeEventPayload = { * Gecko includes rich log information. This marker payload is used to mirror that * log information in the profile. */ -export type LogMarkerPayload = { - type: 'Log'; - name: string; - module: string; -}; +export type LogMarkerPayload = + | { + type: 'Log'; + name: string; + module: string; + } + | { + type: 'Log'; + level: number; + message: string; + }; export type DOMEventMarkerPayload = { type: 'DOMEvent'; diff --git a/src/utils/window-console.ts b/src/utils/window-console.ts index 6752289527..96bb9c74b3 100644 --- a/src/utils/window-console.ts +++ b/src/utils/window-console.ts @@ -9,6 +9,7 @@ import type { Profile, Thread, Marker, + LogMarkerPayload, } from 'firefox-profiler/types'; import { selectorsForConsole } from 'firefox-profiler/selectors'; import actions from 'firefox-profiler/actions'; @@ -297,36 +298,45 @@ export function addDataToWindowObject( const range = selectorsForConsole.profile.getPreviewSelectionRange(getState()); + const LOG_LEVEL_LETTER: Record = { + 1: 'E', + 2: 'W', + 3: 'I', + 4: 'D', + 5: 'V', + }; + for (const thread of profile.threads) { const { markers } = thread; + for (let i = 0; i < markers.length; i++) { const startTime = markers.startTime[i]; - // Note that Log markers are instant markers, so they only have a start time. if ( startTime !== null && - markers.data[i] && markers.data[i]?.type === 'Log' && startTime >= range.start && startTime <= range.end ) { - const data = markers.data[i]; - const markerStartTime = markers.startTime[i]; - if ( - data && - markerStartTime !== null && - (data as any).module && - (data as any).name - ) { - const strTimestamp = d2s(profile.meta.startTime + markerStartTime); - const processName = thread.processName ?? 'Unknown Process'; - - // The log module may contain the log level for profiles captured after bug 1995503. - // If the log module does not contain /, we fake it to D/module - const logModule = (data as any).module; - const prefix = logModule.includes('/') ? '' : 'D/'; - const statement = `${strTimestamp} - [${processName} ${thread.pid}: ${thread.name}]: ${prefix}${logModule} ${(data as any).name.trim()}`; - logs.push(statement); + const data = markers.data[i] as LogMarkerPayload; + const strTimestamp = d2s(profile.meta.startTime + startTime); + const processName = thread.processName ?? 'Unknown Process'; + + let statement; + if ('message' in data) { + if (!data.message) { + continue; + } + const moduleName = profile.shared.stringArray[markers.name[i]]; + const levelLetter = LOG_LEVEL_LETTER[data.level] ?? 'D'; + statement = `${strTimestamp} - [${processName} ${thread.pid}: ${thread.name}]: ${levelLetter}/${moduleName} ${data.message.trim()}`; + } else { + if (!data.name) { + continue; + } + const prefix = data.module.includes('/') ? '' : 'D/'; + statement = `${strTimestamp} - [${processName} ${thread.pid}: ${thread.name}]: ${prefix}${data.module} ${data.name.trim()}`; } + logs.push(statement); } } } From 4ebd3d5ffff790c59f77dddf53a6f4fed3829aa0 Mon Sep 17 00:00:00 2001 From: Pontoon Date: Wed, 8 Apr 2026 10:50:36 +0000 Subject: [PATCH 17/64] Pontoon/Firefox Profiler: Update Russian (ru) Co-authored-by: berry (ru) Co-authored-by: Valery Ledovskoy (ru) --- locales/ru/app.ftl | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/locales/ru/app.ftl b/locales/ru/app.ftl index c89f90da4d..2e1833b7c1 100644 --- a/locales/ru/app.ftl +++ b/locales/ru/app.ftl @@ -146,7 +146,7 @@ CallNodeContextMenu--expand-all = Развернуть всё # See: https://searchfox.org/ CallNodeContextMenu--searchfox = Найти название функции на Searchfox CallNodeContextMenu--copy-function-name = Скопировать имя функции -CallNodeContextMenu--copy-script-url = Скопировать URL сценария +CallNodeContextMenu--copy-script-url = Копировать URL скрипта CallNodeContextMenu--copy-stack = Скопировать стек CallNodeContextMenu--show-the-function-in-devtools = Показать функцию в DevTools @@ -154,11 +154,7 @@ CallNodeContextMenu--show-the-function-in-devtools = Показать функц ## This is the component for Call Tree panel. CallTree--tracing-ms-total = Время работы (мс) - .title = - «Общее» время выполнения включает в себя сводку всего времени, в течение которого наблюдалось нахождение этой - функции в стеке. Это включает в себя время, когда - функция фактически была запущена, и время, проведенное в вызывающих из - этой функции. + .title = «Общее» время выполнения включает в себя сводку всего времени, в течение которого наблюдалось нахождение этой функции в стеке. Это включает в себя время, когда функция фактически была запущена, и время, проведённое в вызывающих из этой функции. CallTree--tracing-ms-self = Собственное (мс) .title = «Собственное» время включает в себя только то время, когда функция была @@ -166,11 +162,7 @@ CallTree--tracing-ms-self = Собственное (мс) то время работы «других» функций не учитывается. «Собственное» время полезно для понимания того, на что на самом деле было потрачено время в программе. CallTree--samples-total = Общее (семплы) - .title = - «Общее» количество семплов включает в себя сводку по каждому семплу, в котором - было обнаружено наличие этой функции в стеке. Оно включает в себя время, когда - функция фактически была запущена, и время, проведенное в вызывающих из этой - функции. + .title = «Общее» количество семплов включает в себя сводку по каждому семплу, в котором было обнаружено наличие этой функции в стеке. Оно включает в себя время, когда функция фактически была запущена, и время, проведённое в вызывающих из этой функции. CallTree--samples-self = Собственные .title = Количество «собственных» семплов включает только те семплы, в которых функция была @@ -396,7 +388,7 @@ IdleSearchField--search-input = ## JSTracer is an experimental feature and it's currently disabled. See Bug 1565788. JsTracerSettings--show-only-self-time = Показывать только собственное время - .title = Показывать только время, проведенное в узле вызова, игнорируя его дочерние элементы. + .title = Показывать только время, проведённое в узле вызова, игнорируя его дочерние элементы. ## ListOfPublishedProfiles ## This is the component that displays all the profiles the user has uploaded. From 790f326c82b0cfd8e12027edc2745f16688afb0f Mon Sep 17 00:00:00 2001 From: Pontoon Date: Wed, 8 Apr 2026 14:50:36 +0000 Subject: [PATCH 18/64] Pontoon/Firefox Profiler: Update Chinese (China) (zh-CN) Co-authored-by: Olvcpr423 (zh-CN) --- locales/zh-CN/app.ftl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/locales/zh-CN/app.ftl b/locales/zh-CN/app.ftl index 8efb28dbc9..b32c9f2e33 100644 --- a/locales/zh-CN/app.ftl +++ b/locales/zh-CN/app.ftl @@ -112,16 +112,16 @@ CallNodeContextMenu--show-the-function-in-devtools = 在开发者工具中显示 CallTree--tracing-ms-total = 总运行时间(ms) .title = 此函数在栈上被观察到出现的“总计”时长汇总。包含函数实际运行的时长,以及此函数中所调用的时长。 -CallTree--tracing-ms-self = Self(ms) - .title = “Self”时间只包含函数在栈底结束时的时间。若此函数是通过其他函数调用的,则不包含“该函数”的时间。“self”时间适合用于了解程序中实际用了多长时间在哪些函数上。 +CallTree--tracing-ms-self = 自身(ms) + .title = “自身”时间只包含函数在栈底结束时的时间。若此函数是通过其他函数调用的,则不包含“该函数”的时间。“自身”时间适合用于了解程序中实际用了多长时间在哪些函数上。 CallTree--samples-total = 总计(样本数) .title = 此函数在栈上被观察到出现的“总计”次数汇总。包含实际运行的的次数,以及此函数中所调用的次数。 -CallTree--samples-self = Self - .title = “Self”样本数只包含函数在栈底结束时的次数。若此函数是通过其他函数调用的,则不包含“该函数”的次数。“self”次数适合用于了解程序中实际用了多长时间在哪些函数上。 +CallTree--samples-self = 自身 + .title = “自身”样本数只包含函数在栈底结束时的次数。若此函数是通过其他函数调用的,则不包含“该函数”的次数。“自身”次数适合用于了解程序中实际用了多长时间在哪些函数上。 CallTree--bytes-total = 总大小(字节) .title = 此函数在栈上被观察到分配或释放的“总计”字节汇总。包含函数实际运行时使用的大小,以及此函数中所调用其他函数所用的内存大小。 -CallTree--bytes-self = Self(字节) - .title = “Self”字节数只包含函数在栈底分配或释放的内存用量。若此函数是通过其他函数调用的,则不包含“该函数”的用量。“Self”字节数适合用于了解程序中实际用了多少内存在哪些函数上。 +CallTree--bytes-self = 自身(字节) + .title = “自身”字节数只包含函数在栈底分配或释放的内存用量。若此函数是通过其他函数调用的,则不包含“该函数”的用量。“自身”字节数适合用于了解程序中实际用了多少内存在哪些函数上。 ## Call tree "badges" (icons) with tooltips ## From fd558cb37f65349205010f302e03b657ae7736da Mon Sep 17 00:00:00 2001 From: Pontoon Date: Wed, 8 Apr 2026 15:00:33 +0000 Subject: [PATCH 19/64] Pontoon/Firefox Profiler: Update Chinese (China) (zh-CN) Co-authored-by: Olvcpr423 (zh-CN) --- locales/zh-CN/app.ftl | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/locales/zh-CN/app.ftl b/locales/zh-CN/app.ftl index b32c9f2e33..34d6c306fc 100644 --- a/locales/zh-CN/app.ftl +++ b/locales/zh-CN/app.ftl @@ -76,6 +76,14 @@ CallNodeContextMenu--transform-focus-function = 聚焦于函数 CallNodeContextMenu--transform-focus-function-inverted = 聚焦于函数(反向) .title = { CallNodeContextMenu--transform-focus-function-title } +## The translation for "self" in these strings should match the translation used +## in CallTree--samples-self and CallTree--bytes-self. Alternatively it can be +## translated as "self values" or "self time" (though "self time" is less desirable +## because this menu item is also shown in "bytes" mode). + +CallNodeContextMenu--transform-focus-self = 只聚焦于自身 + .title = { CallNodeContextMenu--transform-focus-self-title } + ## CallNodeContextMenu--transform-focus-subtree = 只聚焦于子树 @@ -290,8 +298,8 @@ IdleSearchField--search-input = ## JsTracerSettings ## JSTracer is an experimental feature and it's currently disabled. See Bug 1565788. -JsTracerSettings--show-only-self-time = 只显示 self 时间 - .title = 只显示调用节点所用的时间,而忽略其 children。 +JsTracerSettings--show-only-self-time = 只显示自身时间 + .title = 只显示调用节点所用的时间,而忽略其子节点。 ## ListOfPublishedProfiles ## This is the component that displays all the profiles the user has uploaded. From 38a994c21a20e8ff926bc67ad6aa7e5acd779872 Mon Sep 17 00:00:00 2001 From: Pontoon Date: Wed, 8 Apr 2026 15:10:35 +0000 Subject: [PATCH 20/64] Pontoon/Firefox Profiler: Update Chinese (China) (zh-CN) Co-authored-by: wxie (zh-CN) Co-authored-by: Olvcpr423 (zh-CN) --- locales/zh-CN/app.ftl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/locales/zh-CN/app.ftl b/locales/zh-CN/app.ftl index 34d6c306fc..cbd3215b82 100644 --- a/locales/zh-CN/app.ftl +++ b/locales/zh-CN/app.ftl @@ -275,6 +275,10 @@ Home--enable-button-unavailable = # This message can be seen on https://main--perf-html.netlify.app/ . Home--web-channel-unavailable = 此分析器无法连接至 WebChannel。通常是因为运行分析器的主机与 devtools.performance.recording.ui-base-url 首选项中指定的主机不同。若您想要使用此分析器捕捉新的性能分析记录,并可程序化控制分析器菜单按钮,可到 about:config 调整该首选项。 Home--record-instructions = 要进行分析,请点击“分析”按钮,或使用键盘快捷键。在性能记录时,此图标将会变为蓝色。按下捕捉即可将数据加载至 profiler.firefox.com。 +Home--fenix-instructions-directly = 可直接在此设备上对 { -firefox-android-brand-name } 进行性能分析。有关更多信息,请阅读直接在设备上对 { -firefox-android-brand-name } 进行性能分析。 +Home--fenix-instructions-remotely = + 您也可以通过桌面版 { -firefox-brand-name } 对 { -firefox-android-brand-name } 进行远程性能分析。有关更多信息,请参阅文档: + 对 { -firefox-android-brand-name } 进行远程性能分析。 Home--record-instructions-start-stop = 停止并开始分析 Home--record-instructions-capture-load = 捕捉并加载分析记录 Home--profiler-motto = 捕捉性能分析记录。剖析、分享、让网站速度更快。 @@ -282,6 +286,7 @@ Home--additional-content-title = 加载现有分析记录 Home--additional-content-content = 您可以将分析记录拖放至此处,或: Home--compare-recordings-info = 您也可以比较记录内容。打开比较界面。 Home--your-recent-uploaded-recordings-title = 您最近上传的记录 +Home--dark-mode-title = 深色模式 # We replace the elements such as and with links to the # documentation to use these tools. Home--load-files-from-other-tools2 = { -profiler-brand-name } 也可以从其他分析器导入记录,例如 Linux perfAndroid SimplePerf、Chrome 性能面板、Android Studio,支持直接导入 dhatGoogle 的 Trace Event 格式保存的分析记录。点此了解如何编写您自己的导入程序。 From 0db3bfd3303c7bd7955dc96cd5c58f6509b49a22 Mon Sep 17 00:00:00 2001 From: Pontoon Date: Wed, 8 Apr 2026 15:50:32 +0000 Subject: [PATCH 21/64] Pontoon/Firefox Profiler: Update Chinese (China) (zh-CN) Co-authored-by: Olvcpr423 (zh-CN) --- locales/zh-CN/app.ftl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/locales/zh-CN/app.ftl b/locales/zh-CN/app.ftl index cbd3215b82..249ec052f3 100644 --- a/locales/zh-CN/app.ftl +++ b/locales/zh-CN/app.ftl @@ -275,6 +275,7 @@ Home--enable-button-unavailable = # This message can be seen on https://main--perf-html.netlify.app/ . Home--web-channel-unavailable = 此分析器无法连接至 WebChannel。通常是因为运行分析器的主机与 devtools.performance.recording.ui-base-url 首选项中指定的主机不同。若您想要使用此分析器捕捉新的性能分析记录,并可程序化控制分析器菜单按钮,可到 about:config 调整该首选项。 Home--record-instructions = 要进行分析,请点击“分析”按钮,或使用键盘快捷键。在性能记录时,此图标将会变为蓝色。按下捕捉即可将数据加载至 profiler.firefox.com。 +Home--instructions-content2 = 记录性能分析数据需要使用桌面版 { -firefox-brand-name },但已有的分析记录可使用任意现代浏览器查看。 Home--fenix-instructions-directly = 可直接在此设备上对 { -firefox-android-brand-name } 进行性能分析。有关更多信息,请阅读直接在设备上对 { -firefox-android-brand-name } 进行性能分析。 Home--fenix-instructions-remotely = 您也可以通过桌面版 { -firefox-brand-name } 对 { -firefox-android-brand-name } 进行远程性能分析。有关更多信息,请参阅文档: @@ -967,6 +968,12 @@ TransformNavigator--focus-subtree = 聚焦节点:{ $item } # Variables: # $item (String) - Name of the function that transform applied to. TransformNavigator--focus-function = 聚焦:{ $item } +# "Focus self" transform. +# See: https://profiler.firefox.com/docs/#/./guide-filtering-call-trees?id=focus-on-function-self +# Also see the translation note above CallNodeContextMenu--transform-focus-self. +# Variables: +# $item (String) - Name of the function that transform applied to. +TransformNavigator--focus-self = 聚焦于自身:{ $item } # "Focus category" transform. The word "Focus" has the meaning of an adjective here. # See: https://profiler.firefox.com/docs/#/./guide-filtering-call-trees?id=focus-category # Variables: From 7018717379644f77b369631e11c51d7b844880db Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Thu, 2 Apr 2026 18:49:19 -0400 Subject: [PATCH 22/64] Replace react-transition-group usage with plain CSS animations. --- package.json | 2 - src/components/app/Home.css | 21 +- src/components/app/Home.tsx | 537 ++++++++++++++++++------------------ yarn.lock | 27 +- 4 files changed, 278 insertions(+), 309 deletions(-) diff --git a/package.json b/package.json index 43354850f9..5a43e7340b 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,6 @@ "react-intersection-observer": "^10.0.3", "react-redux": "^9.2.0", "react-splitter-layout": "^4.0.0", - "react-transition-group": "^4.4.5", "redux": "^5.0.1", "redux-logger": "^3.0.6", "redux-thunk": "^3.1.0", @@ -131,7 +130,6 @@ "@types/react": "^18.3.28", "@types/react-dom": "^18.3.1", "@types/react-splitter-layout": "^4.0.0", - "@types/react-transition-group": "^4.4.5", "@types/redux-logger": "^3.0.6", "@types/tgwf__co2": "^0.14.2", "@typescript-eslint/eslint-plugin": "^8.58.0", diff --git a/src/components/app/Home.css b/src/components/app/Home.css index fe2fb6c878..24805022d5 100644 --- a/src/components/app/Home.css +++ b/src/components/app/Home.css @@ -199,17 +199,20 @@ /* Only show the home transition when reduced motion is not preferred */ @media (prefers-reduced-motion: no-preference) { - .homeTransition-enter { - opacity: 0.1; - transform: translateX(100px); + @keyframes homeSlideIn { + from { + opacity: 0.1; + transform: translateX(100px); + } + + to { + opacity: 1; + transform: translateX(0); + } } - .homeTransition-enter.homeTransition-enter-active { - opacity: 1; - transform: translateX(0); - transition: - opacity 300ms ease-out, - transform 300ms ease-out; + .homeInstructions-animate { + animation: homeSlideIn 300ms ease-out; } } diff --git a/src/components/app/Home.tsx b/src/components/app/Home.tsx index b3df5dea00..1dc03a0847 100644 --- a/src/components/app/Home.tsx +++ b/src/components/app/Home.tsx @@ -16,7 +16,6 @@ import { triggerLoadingFromUrl, } from 'firefox-profiler/actions/receive-profile'; import type { BrowserConnection } from 'firefox-profiler/app-logic/browser-connection'; -import { CSSTransition, TransitionGroup } from 'react-transition-group'; import { queryIsMenuButtonEnabled, enableMenuButton, @@ -182,17 +181,6 @@ function DocsButton() { ); } -function InstructionTransition(props: { children: React.ReactNode }) { - return ( - - ); -} - type OwnHomeProps = { readonly specialMessage?: string; }; @@ -264,6 +252,18 @@ class HomeImpl extends React.PureComponent { } _renderInstructions() { + // The returned element's `key` controls whether React remounts the element + // or reuses it when the phase changes. Remounting triggers the CSS slide-in + // animation. All setup phases share key="setup" so they transition without + // animation; switching to key="record" forces a remount and plays the animation. + // + // Transition scenarios: + // Initial page load → no animation (first paint) + // checking-webchannel → suggest-enable-popup → no animation (same key) + // checking-webchannel → webchannel-unavailable → no animation (same key) + // checking-webchannel → popup-enabled → animates (key change) + // suggest-enable-popup → popup-enabled → animates (key change, user clicked) + // All other phases (android, chrome, other) → fixed at initial state, never change const { popupInstallPhase } = this.state; switch (popupInstallPhase) { case 'checking-webchannel': @@ -302,137 +302,137 @@ class HomeImpl extends React.PureComponent { _renderEnablePopupInstructions(webChannelAvailable: boolean) { return ( - -
- {/* Grid container: homeInstructions */} - {/* Left column: img */} - screenshot of profiler.firefox.com - {/* Right column: instructions */} -
- + {/* Grid container: homeInstructions */} + {/* Left column: img */} + screenshot of profiler.firefox.com + {/* Right column: instructions */} +
+ + - - - {webChannelAvailable ? ( - -

- Enable the profiler menu button to start recording a - performance profile in Firefox, then analyze it and share it - with profiler.firefox.com. -

-
- ) : ( - , - }} - > -

- This profiler instance was unable to connect to the - WebChannel. This usually means that it’s running on a - different host from the one that is specified in the - preference{' '} - devtools.performance.recording.ui-base-url. If - you would like to capture new profiles with this instance, and - give it programmatic control of the profiler menu button, you - can go to about:config - and change the preference. -

+ + + + Enable Profiler Menu Button - )} + +
+ + {webChannelAvailable ? ( + +

+ Enable the profiler menu button to start recording a performance + profile in Firefox, then analyze it and share it with + profiler.firefox.com. +

+
+ ) : ( - ), + code: , }} >

- You can also profile Firefox for Android. For more information, - please consult this documentation:{' '} - Profiling Firefox for Android directly on device. + This profiler instance was unable to connect to the WebChannel. + This usually means that it’s running on a different host from + the one that is specified in the preference{' '} + devtools.performance.recording.ui-base-url. If you + would like to capture new profiles with this instance, and give + it programmatic control of the profiler menu button, you can go + to about:config + and change the preference.

-
- {/* end of grid container */} + )} + + ), + }} + > +

+ You can also profile Firefox for Android. For more information, + please consult this documentation:{' '} + Profiling Firefox for Android directly on device. +

+
- + {/* end of grid container */} +
); } _renderFenixInstructions() { return ( - -
- {/* Grid container: homeInstructions */} - {/* Left column: img */} - screenshot of profiler.firefox.com - {/* Right column: instructions */} -
- - - ), - }} - > -

- Firefox for Android can be profiled directly on this device. For - more information, read{' '} - Profiling Firefox for Android directly on device. -

-
- - ), - }} - > -

- You can also profile Firefox for Android remotely from Firefox - for desktop. For more information, please consult this - documentation: Profiling Firefox for Android remotely. -

-
-
- {/* end of grid container */} +
+ {/* Grid container: homeInstructions */} + {/* Left column: img */} + screenshot of profiler.firefox.com + {/* Right column: instructions */} +
+ + + ), + }} + > +

+ Firefox for Android can be profiled directly on this device. For + more information, read{' '} + Profiling Firefox for Android directly on device. +

+
+ + ), + }} + > +

+ You can also profile Firefox for Android remotely from Firefox for + desktop. For more information, please consult this documentation:{' '} + Profiling Firefox for Android remotely. +

+
- + {/* end of grid container */} +
); } @@ -440,164 +440,155 @@ class HomeImpl extends React.PureComponent { const chromeExtensionUrl = 'https://chromewebstore.google.com/detail/firefox-profiler/ljmahpnflmbkgaipnfbpgjipcnahlghn'; return ( - -
- {/* Grid container: homeInstructions */} - {/* Left column: img */} - screenshot of profiler.firefox.com - {/* Right column: instructions */} -
- - + - - Install the Chrome extension - - - - - ), - }} - > -

- Use the Firefox Profiler extension for Chrome to capture - performance profiles in Chrome and analyze them in the Firefox - Profiler. Install the extension from the Chrome Web Store. -

-
- -

- Once installed, use the extension’s toolbar icon or the - shortcuts to start and stop profiling. You can also export - profiles and load them here for detailed analysis. -

+
+ {/* Grid container: homeInstructions */} + {/* Left column: img */} + screenshot of profiler.firefox.com + {/* Right column: instructions */} + - {/* end of grid container */} + + + + ), + }} + > +

+ Use the Firefox Profiler extension for Chrome to capture + performance profiles in Chrome and analyze them in the Firefox + Profiler. Install the extension from the Chrome Web Store. +

+
+ +

+ Once installed, use the extension’s toolbar icon or the shortcuts + to start and stop profiling. You can also export profiles and load + them here for detailed analysis. +

+
+ {this._renderShortcuts()}
- + {/* end of grid container */} +
); } _renderRecordInstructions(screenshotSrc: string) { return ( - -
- {/* Grid container: homeInstructions */} - {/* Left column: img */} - Screenshot of the profiler settings from the Firefox menu. - {/* Right column: instructions */} -
- - , - }} - > -

- To start profiling, click on the profiling button, or use the - keyboard shortcuts. The icon is blue when a profile is - recording. Hit Capture to load the data into - profiler.firefox.com. -

-
- {this._renderShortcuts()} - - ), - }} - > -

- You can also profile Firefox for Android. For more information, - please consult this documentation:{' '} - Profiling Firefox for Android directly on device. -

-
-
- {/* end of grid container */} +
+ {/* Grid container: homeInstructions */} + {/* Left column: img */} + Screenshot of the profiler settings from the Firefox menu. + {/* Right column: instructions */} +
+ + , + }} + > +

+ To start profiling, click on the profiling button, or use the + keyboard shortcuts. The icon is blue when a profile is recording. + Hit Capture to load the data into profiler.firefox.com. +

+
+ {this._renderShortcuts()} + + ), + }} + > +

+ You can also profile Firefox for Android. For more information, + please consult this documentation:{' '} + Profiling Firefox for Android directly on device. +

+
- + {/* end of grid container */} +
); } _renderOtherBrowserInstructions() { return ( - -
- {/* Grid container: homeInstructions */} - {/* Left column: img */} - screenshot of profiler.firefox.com - {/* Right column: instructions */} -
- - , - }} - > -

- Recording performance profiles requires{' '} - Firefox for desktop. However, existing profiles can be - viewed in any modern browser. -

-
- - ), - }} - > -

- You can also profile Firefox for Android. For more information, - please consult this documentation:{' '} - Profiling Firefox for Android directly on device. -

-
-
- {/* end of grid container */} +
+ {/* Grid container: homeInstructions */} + {/* Left column: img */} + screenshot of profiler.firefox.com + {/* Right column: instructions */} +
+ + , + }} + > +

+ Recording performance profiles requires Firefox for desktop + . However, existing profiles can be viewed in any modern browser. +

+
+ + ), + }} + > +

+ You can also profile Firefox for Android. For more information, + please consult this documentation:{' '} + Profiling Firefox for Android directly on device. +

+
- + {/* end of grid container */} +
); } @@ -644,9 +635,9 @@ class HomeImpl extends React.PureComponent { faster.

- +
{this._renderInstructions()} - +
{/* Grid container: homeAdditionalContent */}

diff --git a/yarn.lock b/yarn.lock index dd43ea0c55..2e53af1ad4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1011,7 +1011,7 @@ "@babel/plugin-transform-modules-commonjs" "^7.27.1" "@babel/plugin-transform-typescript" "^7.28.5" -"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3": version "7.26.10" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2" integrity sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw== @@ -2417,11 +2417,6 @@ dependencies: "@types/react" "*" -"@types/react-transition-group@^4.4.5": - version "4.4.12" - resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044" - integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w== - "@types/react@*", "@types/react@^18.3.28": version "18.3.28" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.28.tgz#0a85b1a7243b4258d9f626f43797ba18eb5f8781" @@ -3957,7 +3952,7 @@ cssstyle@^4.2.1: "@asamuzakjp/css-color" "^3.2.0" rrweb-cssom "^0.8.0" -csstype@^3.0.2, csstype@^3.2.2: +csstype@^3.2.2: version "3.2.3" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== @@ -4242,14 +4237,6 @@ dom-accessibility-api@^0.6.3: resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8" integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w== -dom-helpers@^5.0.1: - version "5.2.0" - resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.0.tgz#57fd054c5f8f34c52a3eeffdb7e7e93cd357d95b" - integrity sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ== - dependencies: - "@babel/runtime" "^7.8.7" - csstype "^3.0.2" - dot-prop@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-6.0.1.tgz#fc26b3cf142b9e59b74dbd39ed66ce620c681083" @@ -9368,16 +9355,6 @@ react-splitter-layout@^4.0.0: resolved "https://registry.yarnpkg.com/react-splitter-layout/-/react-splitter-layout-4.0.0.tgz#70b43ca6a78c056f5e5fbf29c67b597f040fbf2e" integrity sha512-SLqOjBOxRuizWUa83w6q5/u9cDWa9/yj9Iko9V9JFN8x+cqIXiDlUFWSx+icz3IIgvsN/oRIw3za5/32RjIwrA== -react-transition-group@^4.4.5: - version "4.4.5" - resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" - integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== - dependencies: - "@babel/runtime" "^7.5.5" - dom-helpers "^5.0.1" - loose-envify "^1.4.0" - prop-types "^15.6.2" - react@^16.8.6: version "16.14.0" resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" From 2486c9594f22269dc13ca796d8a424ae642d1c9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Thu, 9 Apr 2026 16:54:50 +0200 Subject: [PATCH 23/64] Move some profile fetching code into a separate module. (#5939) This logic can be useful independently of redux actions, for CLI scripts which want to support profiler URLs. Co-authored-by: Markus Stange --- src/actions/receive-profile.ts | 287 +-------------- src/test/components/Root-history.test.tsx | 2 +- src/test/fixtures/utils.ts | 20 + .../receive-profile.test.ts.snap | 54 --- src/test/store/receive-profile.test.ts | 207 +---------- .../__snapshots__/profile-fetch.test.ts.snap | 55 +++ src/test/unit/profile-fetch.test.ts | 208 +++++++++++ src/utils/profile-fetch.ts | 346 ++++++++++++++++++ 8 files changed, 648 insertions(+), 531 deletions(-) create mode 100644 src/test/unit/__snapshots__/profile-fetch.test.ts.snap create mode 100644 src/test/unit/profile-fetch.test.ts create mode 100644 src/utils/profile-fetch.ts diff --git a/src/actions/receive-profile.ts b/src/actions/receive-profile.ts index faa49d33d4..7eb4182cdd 100644 --- a/src/actions/receive-profile.ts +++ b/src/actions/receive-profile.ts @@ -2,6 +2,13 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { oneLine } from 'common-tags'; +import { + fetchProfile, + getProfileUrlForHash, + type ProfileOrZip, + deduceContentType, + extractJsonFromArrayBuffer, +} from 'firefox-profiler/utils/profile-fetch'; import queryString from 'query-string'; import type JSZip from 'jszip'; import { @@ -20,10 +27,8 @@ import { } from 'firefox-profiler/profile-logic/symbolication'; import * as MozillaSymbolicationAPI from 'firefox-profiler/profile-logic/mozilla-symbolication-api'; import { mergeProfilesForDiffing } from 'firefox-profiler/profile-logic/merge-compare'; -import { decompress, isGzip } from 'firefox-profiler/utils/gz'; import { expandUrl } from 'firefox-profiler/utils/shorten-url'; import { TemporaryError } from 'firefox-profiler/utils/errors'; -import { isLocalURL } from 'firefox-profiler/utils/url'; import { getSelectedThreadIndexesOrNull, getGlobalTrackOrder, @@ -67,7 +72,6 @@ import { import { setDataSource } from './profile-view'; import { fatalError } from './errors'; import { batchLoadDataUrlIcons } from './icons'; -import { GOOGLE_STORAGE_BUCKET } from 'firefox-profiler/app-logic/constants'; import { determineTimelineType, hasUsefulSamples, @@ -547,7 +551,7 @@ async function _unpackGeckoProfileFromBrowser( // global. This happens especially with tests but could happen in the future // in Firefox too. if (Object.prototype.toString.call(profile) === '[object ArrayBuffer]') { - return _extractJsonFromArrayBuffer(profile as ArrayBuffer); + return extractJsonFromArrayBuffer(profile as ArrayBuffer); } return profile; } @@ -557,9 +561,9 @@ function getSymbolStore( symbolServerUrl: string, browserConnection: BrowserConnection | null ): SymbolStore | null { - if (!window.indexedDB) { - // We could be running in a test environment with no indexedDB support. Do not - // return a symbol store in this case. + if (typeof window === 'undefined' || !window.indexedDB) { + // We could be running in a test environment or Node.js with no indexedDB support. + // Do not return a symbol store in this case. return null; } @@ -983,265 +987,6 @@ export function temporaryError(error: TemporaryError): Action { }; } -function _wait(delayMs: number): Promise { - return new Promise((resolve) => setTimeout(resolve, delayMs)); -} - -function _loadProbablyFailedDueToSafariLocalhostHTTPRestriction( - url: string, - error: Error -): boolean { - if (!navigator.userAgent.match(/Safari\/\d+\.\d+/)) { - return false; - } - // Check if Safari considers this mixed content. - const parsedUrl = new URL(url); - return ( - error.name === 'TypeError' && - parsedUrl.protocol === 'http:' && - isLocalURL(parsedUrl) && - location.protocol === 'https:' - ); -} - -class SafariLocalhostHTTPLoadError extends Error { - override name = 'SafariLocalhostHTTPLoadError'; -} - -type FetchProfileArgs = { - url: string; - onTemporaryError: (param: TemporaryError) => void; - // Allow tests to capture the reported error, but normally use console.error. - reportError?: (...data: Array) => void; -}; - -type ProfileOrZip = - | { responseType: 'PROFILE'; profile: unknown } - | { responseType: 'ZIP'; zip: JSZip }; - -/** - * Tries to fetch a profile on `url`. If the profile is not found, - * `onTemporaryError` is called with an appropriate error, we wait 1 second, and - * then tries again. If we still can't find the profile after 11 tries, the - * returned promise is rejected with a fatal error. - * If we can retrieve the profile properly, the returned promise is resolved - * with the JSON.parsed profile. - */ -export async function _fetchProfile( - args: FetchProfileArgs -): Promise { - const MAX_WAIT_SECONDS = 10; - let i = 0; - const { url, onTemporaryError } = args; - // Allow tests to capture the reported error, but normally use console.error. - const reportError = args.reportError || console.error; - - while (true) { - let response; - try { - response = await fetch(url); - } catch (e) { - // Case 1: Exception. - if (_loadProbablyFailedDueToSafariLocalhostHTTPRestriction(url, e)) { - throw new SafariLocalhostHTTPLoadError(); - } - throw e; - } - - // Case 2: successful answer. - if (response.ok) { - return _extractProfileOrZipFromResponse(url, response, reportError); - } - - // case 3: unrecoverable error. - if (response.status !== 403) { - throw new Error(oneLine` - Could not fetch the profile on remote server. - Response was: ${response.status} ${response.statusText}. - `); - } - - // case 4: 403 errors can be transient while a profile is uploaded. - - if (i++ === MAX_WAIT_SECONDS) { - // In the last iteration we don't send a temporary error because we'll - // throw an error right after the while loop. - break; - } - - onTemporaryError( - new TemporaryError( - 'Profile not found on remote server.', - { count: i, total: MAX_WAIT_SECONDS + 1 } // 11 tries during 10 seconds - ) - ); - - await _wait(1000); - } - - throw new Error(oneLine` - Could not fetch the profile on remote server: - still not found after ${MAX_WAIT_SECONDS} seconds. - `); -} - -/** - * Deduce the file type from a url and content type. Third parties can give us - * arbitrary information, so make sure that we try out best to extract the proper - * information about it. - */ -function _deduceContentType( - url: string, - contentType: string | null -): 'application/json' | 'application/zip' | null { - if (contentType === 'application/zip' || contentType === 'application/json') { - return contentType; - } - if (url.match(/\.zip$/)) { - return 'application/zip'; - } - if (url.match(/\.json/)) { - return 'application/json'; - } - return null; -} - -/** - * This function guesses the correct content-type (even if one isn't sent) and then - * attempts to use the proper method to extract the response. - */ -async function _extractProfileOrZipFromResponse( - url: string, - response: Response, - reportError: (...data: Array) => void -): Promise { - const contentType = _deduceContentType( - url, - response.headers.get('content-type') - ); - switch (contentType) { - case 'application/zip': - return { - responseType: 'ZIP', - zip: await _extractZipFromResponse(response, reportError), - }; - case 'application/json': - case null: - // The content type is null if it is unknown, or an unsupported type. Go ahead - // and try to process it as a profile. - return { - responseType: 'PROFILE', - profile: await _extractJsonFromResponse( - response, - reportError, - contentType - ), - }; - default: - throw assertExhaustiveCheck(contentType); - } -} - -/** - * Attempt to load a zip file from a third party. This process can fail, so make sure - * to handle and report the error if it does. - */ -async function _extractZipFromResponse( - response: Response, - reportError: (...data: Array) => void -): Promise { - const buffer = await response.arrayBuffer(); - // Workaround for https://github.com/Stuk/jszip/issues/941 - // When running this code in tests, `buffer` doesn't inherits from _this_ - // realm's ArrayBuffer object, and this breaks JSZip which doesn't account for - // this case. We workaround the issue by wrapping the buffer in an Uint8Array - // that comes from this realm. - const typedBuffer = new Uint8Array(buffer); - try { - const { default: JSZip } = await import('jszip'); - const zip = await JSZip.loadAsync(typedBuffer); - // Catch the error if unable to load the zip. - return zip; - } catch (error) { - const message = 'Unable to open the archive file.'; - reportError(message); - reportError('Error:', error); - reportError('Fetch response:', response); - throw new Error( - `${message} The full error information has been printed out to the DevTool’s console.` - ); - } -} - -/** - * Parse JSON from an optionally gzipped array buffer. - */ -async function _extractJsonFromArrayBuffer( - arrayBuffer: ArrayBuffer -): Promise { - let profileBytes = new Uint8Array(arrayBuffer); - // Check for the gzip magic number in the header. - if (isGzip(profileBytes)) { - profileBytes = await decompress(profileBytes); - } - - const textDecoder = new TextDecoder(); - return JSON.parse(textDecoder.decode(profileBytes)); -} - -/** - * Don't trust third party responses, try and handle a variety of responses gracefully. - */ -async function _extractJsonFromResponse( - response: Response, - reportError: (...data: Array) => void, - fileType: 'application/json' | null -): Promise { - let arrayBuffer: ArrayBuffer | null = null; - try { - // await before returning so that we can catch JSON parse errors. - arrayBuffer = await response.arrayBuffer(); - return await _extractJsonFromArrayBuffer(arrayBuffer); - } catch (error) { - // Change the error message depending on the circumstance: - let message; - if (error && typeof error === 'object' && error.name === 'AbortError') { - message = 'The network request to load the profile was aborted.'; - } else if (fileType === 'application/json') { - message = 'The profile’s JSON could not be decoded.'; - } else if (fileType === null && arrayBuffer !== null) { - // If the content type is not specified, use a raw array buffer - // to fallback to other supported profile formats. - return arrayBuffer; - } else { - message = oneLine` - The profile could not be downloaded and decoded. This does not look like a supported file - type. - `; - } - - // Provide helpful debugging information to the console. - reportError(message); - reportError('JSON parsing error:', error); - reportError('Fetch response:', response); - - throw new Error( - `${message} The full error information has been printed out to the DevTool’s console.` - ); - } -} - -export function getProfileUrlForHash(hash: string): string { - // See https://cloud.google.com/storage/docs/access-public-data - // The URL is https://storage.googleapis.com//. - // https://.storage.googleapis.com/ seems to also work but - // is not documented nowadays. - - // By convention, "profile-store" is the name of our bucket, and the file path - // is the hash we receive in the URL. - return `https://storage.googleapis.com/${GOOGLE_STORAGE_BUCKET}/${hash}`; -} - export function retrieveProfileFromStore( hash: string, initialLoad: boolean = false @@ -1262,7 +1007,7 @@ export function retrieveProfileOrZipFromUrl( dispatch(waitingForProfileFromUrl(profileUrl)); try { - const response: ProfileOrZip = await _fetchProfile({ + const response: ProfileOrZip = await fetchProfile({ url: profileUrl, onTemporaryError: (e: TemporaryError) => { dispatch(temporaryError(e)); @@ -1293,7 +1038,7 @@ export function retrieveProfileOrZipFromUrl( default: throw assertExhaustiveCheck( response as never, - 'Expected to receive an archive or profile from _fetchProfile.' + 'Expected to receive an archive or profile from fetchProfile.' ); } } catch (error) { @@ -1349,7 +1094,7 @@ export function retrieveProfileFromFile( dispatch(waitingForProfileFromFile()); try { - if (_deduceContentType(file.name, file.type) === 'application/zip') { + if (deduceContentType(file.name, file.type) === 'application/zip') { // Open a zip file in the zip file viewer const buffer = await fileReader(file).asArrayBuffer(); const { default: JSZip } = await import('jszip'); @@ -1446,14 +1191,14 @@ export function retrieveProfilesToCompare( const profileUrl = getProfileFetchUrl(url); - const response: ProfileOrZip = await _fetchProfile({ + const response: ProfileOrZip = await fetchProfile({ url: profileUrl, onTemporaryError: (e: TemporaryError) => { dispatch(temporaryError(e)); }, }); if (response.responseType !== 'PROFILE') { - throw new Error('Expected to receive a profile from _fetchProfile'); + throw new Error('Expected to receive a profile from fetchProfile'); } const upgradeInfo: ProfileUpgradeInfo = {}; diff --git a/src/test/components/Root-history.test.tsx b/src/test/components/Root-history.test.tsx index e2c34a6bf2..13a130aa71 100644 --- a/src/test/components/Root-history.test.tsx +++ b/src/test/components/Root-history.test.tsx @@ -11,7 +11,7 @@ import { import { Root } from '../../components/app/Root'; import { autoMockCanvasContext } from '../fixtures/mocks/canvas-context'; import { fireFullClick } from '../fixtures/utils'; -import { getProfileUrlForHash } from '../../actions/receive-profile'; +import { getProfileUrlForHash } from '../../utils/profile-fetch'; import { blankStore } from '../fixtures/stores'; import { getProfileFromTextSamples } from '../fixtures/profiles/processed-profile'; import { diff --git a/src/test/fixtures/utils.ts b/src/test/fixtures/utils.ts index 0f50030d9d..84290f4f7b 100644 --- a/src/test/fixtures/utils.ts +++ b/src/test/fixtures/utils.ts @@ -641,6 +641,26 @@ function isControlInput(element: HTMLElement): boolean { ); } +/** + * Returns an ArrayBuffer which contains only the bytes that are covered by the + * Uint8Array, making a copy if needed. + */ +export function extractArrayBuffer( + bufferView: Uint8Array +): ArrayBuffer { + if ( + bufferView.byteOffset === 0 && + bufferView.byteLength === bufferView.buffer.byteLength + ) { + return bufferView.buffer; + } + + // There was extra data at the start or at the end. Make a copy. + const copy = new Uint8Array(bufferView.byteLength); + copy.set(bufferView); + return copy.buffer; +} + /** * Adds a source entry to the sources table and returns the index. * If a source with the same URL already exists, returns the existing index. diff --git a/src/test/store/__snapshots__/receive-profile.test.ts.snap b/src/test/store/__snapshots__/receive-profile.test.ts.snap index 04b089ee95..02c2f06f5a 100644 --- a/src/test/store/__snapshots__/receive-profile.test.ts.snap +++ b/src/test/store/__snapshots__/receive-profile.test.ts.snap @@ -1,59 +1,5 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`actions/receive-profile _fetchProfile fails if a bad profile JSON is passed in 1`] = `[Error: The profile’s JSON could not be decoded. The full error information has been printed out to the DevTool’s console.]`; - -exports[`actions/receive-profile _fetchProfile fails if a bad profile JSON is passed in 2`] = ` -Array [ - Array [ - "The profile’s JSON could not be decoded.", - ], - Array [ - "JSON parsing error:", - [SyntaxError: Unexpected token 'i', "invalid" is not valid JSON], - ], - Array [ - "Fetch response:", - Response {}, - ], -] -`; - -exports[`actions/receive-profile _fetchProfile fails if a bad profile JSON is passed in, with no content type 1`] = `[Error: The profile’s JSON could not be decoded. The full error information has been printed out to the DevTool’s console.]`; - -exports[`actions/receive-profile _fetchProfile fails if a bad profile JSON is passed in, with no content type 2`] = ` -Array [ - Array [ - "The profile’s JSON could not be decoded.", - ], - Array [ - "JSON parsing error:", - [SyntaxError: Unexpected token 'i', "invalid" is not valid JSON], - ], - Array [ - "Fetch response:", - Response {}, - ], -] -`; - -exports[`actions/receive-profile _fetchProfile fails if a bad zip file is passed in 1`] = `[Error: Unable to open the archive file. The full error information has been printed out to the DevTool’s console.]`; - -exports[`actions/receive-profile _fetchProfile fails if a bad zip file is passed in 2`] = ` -Array [ - Array [ - "Unable to open the archive file.", - ], - Array [ - "Error:", - [Error: Can't find end of central directory : is this a zip file ? If it is, see https://stuk.github.io/jszip/documentation/howto/read_zip.html], - ], - Array [ - "Fetch response:", - Response {}, - ], -] -`; - exports[`actions/receive-profile retrieveProfileFromFile will be an error to view a profile with no threads 1`] = `"No threads were captured in this profile, there is nothing to display."`; exports[`actions/receive-profile retrieveProfileFromFile will give an error when unable to decompress a zipped profile 1`] = `[Error: Can't find end of central directory : is this a zip file ? If it is, see https://stuk.github.io/jszip/documentation/howto/read_zip.html]`; diff --git a/src/test/store/receive-profile.test.ts b/src/test/store/receive-profile.test.ts index 0b336a05d8..08fa5b3601 100644 --- a/src/test/store/receive-profile.test.ts +++ b/src/test/store/receive-profile.test.ts @@ -27,9 +27,9 @@ import { retrieveProfileOrZipFromUrl, retrieveProfileFromFile, retrieveProfilesToCompare, - _fetchProfile, retrieveProfileForRawUrl, } from '../../actions/receive-profile'; +import { fetchProfile as _fetchProfile } from '../../utils/profile-fetch'; import { SymbolsNotFoundError } from '../../profile-logic/errors'; import { createGeckoProfile } from '../fixtures/profiles/gecko-profile'; @@ -46,7 +46,7 @@ import { getProfileWithThreadCPUDelta, } from '../fixtures/profiles/processed-profile'; import { getHumanReadableTracks } from '../fixtures/profiles/tracks'; -import { waitUntilState } from '../fixtures/utils'; +import { waitUntilState, extractArrayBuffer } from '../fixtures/utils'; import { dataUrlToBytes } from 'firefox-profiler/utils/base64'; import { compress } from '../../utils/gz'; @@ -88,22 +88,6 @@ function simulateSymbolStoreHasNoCache() { })); } -// Returns an ArrayBuffer which contains only the bytes that -// are covered by the Uint8Array, making a copy if needed. -function extractArrayBuffer(bufferView: Uint8Array): ArrayBuffer { - if ( - bufferView.byteOffset === 0 && - bufferView.byteLength === bufferView.buffer.byteLength - ) { - return bufferView.buffer; - } - - // There was extra data at the start or at the end. Make a copy. - const copy = new Uint8Array(bufferView.byteLength); - copy.set(bufferView); - return copy.buffer; -} - describe('actions/receive-profile', function () { beforeEach(() => { // The SymbolStore requires the use of IndexedDB, ensure that it exists so that @@ -1150,193 +1134,6 @@ describe('actions/receive-profile', function () { }); }); - /** - * _fetchProfile is a helper function for the actions, but it is tested separately - * since it has a decent amount of complexity around different issues with loading - * in different support URL formats. It's mainly testing what happens when JSON - * and zip file is sent, and what happens when things fail. - */ - describe('_fetchProfile', function () { - /** - * This helper function encapsulates various configurations for the type of content - * as well and response headers. - */ - async function configureFetch(obj: { - url: string; - contentType?: string; - content: 'generated-zip' | 'generated-json' | Uint8Array; - }) { - const { url, contentType, content } = obj; - const stringProfile = serializeProfile(_getSimpleProfile()); - const profile = JSON.parse(stringProfile); - let arrayBuffer; - - switch (content) { - case 'generated-zip': { - const zip = new JSZip(); - zip.file('profile.json', stringProfile); - arrayBuffer = await zip.generateAsync({ type: 'uint8array' }); - break; - } - case 'generated-json': - arrayBuffer = encode(stringProfile); - break; - default: - arrayBuffer = content; - break; - } - - window.fetchMock.catch(403).get(url, { - body: arrayBuffer, - headers: { - 'content-type': contentType, - }, - }); - - const reportError = jest.fn(); - const args = { - url, - onTemporaryError: () => {}, - reportError, - }; - - // Return fetch's args, based on the inputs. - return { profile, args, reportError }; - } - - it('fetches a normal profile with the correct content-type headers', async function () { - const { profile, args } = await configureFetch({ - url: 'https://example.com/profile.json', - contentType: 'application/json', - content: 'generated-json', - }); - - const profileOrZip = await _fetchProfile(args); - expect(profileOrZip).toEqual({ responseType: 'PROFILE', profile }); - }); - - it('fetches a zipped profile with correct content-type headers', async function () { - const { args, reportError } = await configureFetch({ - url: 'https://example.com/profile.zip', - contentType: 'application/zip', - content: 'generated-zip', - }); - - const profileOrZip = await _fetchProfile(args); - expect(profileOrZip.responseType).toBe('ZIP'); - expect(reportError.mock.calls.length).toBe(0); - }); - - it('fetches a zipped profile with incorrect content-type headers, but .zip extension', async function () { - const { args, reportError } = await configureFetch({ - url: 'https://example.com/profile.zip', - content: 'generated-zip', - }); - - const profileOrZip = await _fetchProfile(args); - expect(profileOrZip.responseType).toBe('ZIP'); - expect(reportError.mock.calls.length).toBe(0); - }); - - it('fetches a profile with incorrect content-type headers, but .json extension', async function () { - const { profile, args, reportError } = await configureFetch({ - url: 'https://example.com/profile.json', - content: 'generated-json', - }); - - const profileOrZip = await _fetchProfile(args); - expect(profileOrZip).toEqual({ responseType: 'PROFILE', profile }); - expect(reportError.mock.calls.length).toBe(0); - }); - - it('fetches a profile with incorrect content-type headers, no known extension, and attempts to JSON parse it it', async function () { - const { profile, args, reportError } = await configureFetch({ - url: 'https://example.com/profile.file', - content: 'generated-json', - }); - - const profileOrZip = await _fetchProfile(args); - expect(profileOrZip).toEqual({ responseType: 'PROFILE', profile }); - expect(reportError.mock.calls.length).toBe(0); - }); - - it('fails if a bad zip file is passed in', async function () { - const { args, reportError } = await configureFetch({ - url: 'https://example.com/profile.file', - contentType: 'application/zip', - content: new Uint8Array([0, 1, 2, 3]), - }); - - let userFacingError; - try { - await _fetchProfile(args); - } catch (error) { - userFacingError = error; - } - expect(userFacingError).toMatchSnapshot(); - expect(reportError.mock.calls.length).toBeGreaterThan(0); - expect(reportError.mock.calls).toMatchSnapshot(); - }); - - it('fails if a bad profile JSON is passed in', async function () { - const invalidJSON = 'invalid'; - const { args, reportError } = await configureFetch({ - url: 'https://example.com/profile.json', - contentType: 'application/json', - content: encode(invalidJSON), - }); - - let userFacingError; - try { - await _fetchProfile(args); - } catch (error) { - userFacingError = error; - } - expect(userFacingError).toMatchSnapshot(); - expect(reportError.mock.calls.length).toBeGreaterThan(0); - expect(reportError.mock.calls).toMatchSnapshot(); - }); - - it('fails if a bad profile JSON is passed in, with no content type', async function () { - const invalidJSON = 'invalid'; - const { args, reportError } = await configureFetch({ - url: 'https://example.com/profile.json', - content: encode(invalidJSON), - }); - - let userFacingError; - try { - await _fetchProfile(args); - } catch (error) { - userFacingError = error; - } - expect(userFacingError).toMatchSnapshot(); - expect(reportError.mock.calls.length).toBeGreaterThan(0); - expect(reportError.mock.calls).toMatchSnapshot(); - }); - - it('fallback behavior if a completely unknown file is passed in', async function () { - const invalidJSON = 'invalid'; - const profile = encode(invalidJSON); - const { args } = await configureFetch({ - url: 'https://example.com/profile.unknown', - content: profile, - }); - - let userFacingError = null; - try { - const profileOrZip = await _fetchProfile(args); - expect(profileOrZip).toEqual({ - responseType: 'PROFILE', - profile: extractArrayBuffer(profile), - }); - } catch (error) { - userFacingError = error; - } - expect(userFacingError).toBeNull(); - }); - }); - describe('retrieveProfileFromFile', function () { /** * Bypass all of Flow's checks, and mock out the file interface. diff --git a/src/test/unit/__snapshots__/profile-fetch.test.ts.snap b/src/test/unit/__snapshots__/profile-fetch.test.ts.snap new file mode 100644 index 0000000000..1de4f42d93 --- /dev/null +++ b/src/test/unit/__snapshots__/profile-fetch.test.ts.snap @@ -0,0 +1,55 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`fetchProfile fails if a bad profile JSON is passed in 1`] = `[Error: The profile’s JSON could not be decoded. The full error information has been printed out to the DevTool’s console.]`; + +exports[`fetchProfile fails if a bad profile JSON is passed in 2`] = ` +Array [ + Array [ + "The profile’s JSON could not be decoded.", + ], + Array [ + "JSON parsing error:", + [SyntaxError: Unexpected token 'i', "invalid" is not valid JSON], + ], + Array [ + "Fetch response:", + Response {}, + ], +] +`; + +exports[`fetchProfile fails if a bad profile JSON is passed in, with no content type 1`] = `[Error: The profile’s JSON could not be decoded. The full error information has been printed out to the DevTool’s console.]`; + +exports[`fetchProfile fails if a bad profile JSON is passed in, with no content type 2`] = ` +Array [ + Array [ + "The profile’s JSON could not be decoded.", + ], + Array [ + "JSON parsing error:", + [SyntaxError: Unexpected token 'i', "invalid" is not valid JSON], + ], + Array [ + "Fetch response:", + Response {}, + ], +] +`; + +exports[`fetchProfile fails if a bad zip file is passed in 1`] = `[Error: Unable to open the archive file. The full error information has been printed out to the DevTool’s console.]`; + +exports[`fetchProfile fails if a bad zip file is passed in 2`] = ` +Array [ + Array [ + "Unable to open the archive file.", + ], + Array [ + "Error:", + [Error: Can't find end of central directory : is this a zip file ? If it is, see https://stuk.github.io/jszip/documentation/howto/read_zip.html], + ], + Array [ + "Fetch response:", + Response {}, + ], +] +`; diff --git a/src/test/unit/profile-fetch.test.ts b/src/test/unit/profile-fetch.test.ts new file mode 100644 index 0000000000..f971b291ad --- /dev/null +++ b/src/test/unit/profile-fetch.test.ts @@ -0,0 +1,208 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import JSZip from 'jszip'; + +import { fetchProfile } from '../../utils/profile-fetch'; +import { serializeProfile } from '../../profile-logic/process-profile'; +import { getProfileFromTextSamples } from '../fixtures/profiles/processed-profile'; +import { extractArrayBuffer } from '../fixtures/utils'; + +import type { Profile } from 'firefox-profiler/types'; + +function encode(string: string): Uint8Array { + return new TextEncoder().encode(string); +} + +/** + * This profile will have a single sample, and a single thread. + */ +function _getSimpleProfile(): Profile { + return getProfileFromTextSamples('A').profile; +} + +/** + * fetchProfile has a decent amount of complexity around different issues with loading + * in different support URL formats. It's mainly testing what happens when JSON + * and zip file is sent, and what happens when things fail. + */ +describe('fetchProfile', function () { + /** + * This helper function encapsulates various configurations for the type of content + * as well and response headers. + */ + async function configureFetch(obj: { + url: string; + contentType?: string; + content: 'generated-zip' | 'generated-json' | Uint8Array; + }) { + const { url, contentType, content } = obj; + const stringProfile = serializeProfile(_getSimpleProfile()); + const profile = JSON.parse(stringProfile); + let arrayBuffer; + + switch (content) { + case 'generated-zip': { + const zip = new JSZip(); + zip.file('profile.json', stringProfile); + arrayBuffer = await zip.generateAsync({ type: 'uint8array' }); + break; + } + case 'generated-json': + arrayBuffer = encode(stringProfile); + break; + default: + arrayBuffer = content; + break; + } + + window.fetchMock.catch(403).get(url, { + body: arrayBuffer, + headers: { + 'content-type': contentType, + }, + }); + + const reportError = jest.fn(); + const args = { + url, + onTemporaryError: () => {}, + reportError, + }; + + // Return fetch's args, based on the inputs. + return { profile, args, reportError }; + } + + it('fetches a normal profile with the correct content-type headers', async function () { + const { profile, args } = await configureFetch({ + url: 'https://example.com/profile.json', + contentType: 'application/json', + content: 'generated-json', + }); + + const profileOrZip = await fetchProfile(args); + expect(profileOrZip).toEqual({ responseType: 'PROFILE', profile }); + }); + + it('fetches a zipped profile with correct content-type headers', async function () { + const { args, reportError } = await configureFetch({ + url: 'https://example.com/profile.zip', + contentType: 'application/zip', + content: 'generated-zip', + }); + + const profileOrZip = await fetchProfile(args); + expect(profileOrZip.responseType).toBe('ZIP'); + expect(reportError.mock.calls.length).toBe(0); + }); + + it('fetches a zipped profile with incorrect content-type headers, but .zip extension', async function () { + const { args, reportError } = await configureFetch({ + url: 'https://example.com/profile.zip', + content: 'generated-zip', + }); + + const profileOrZip = await fetchProfile(args); + expect(profileOrZip.responseType).toBe('ZIP'); + expect(reportError.mock.calls.length).toBe(0); + }); + + it('fetches a profile with incorrect content-type headers, but .json extension', async function () { + const { profile, args, reportError } = await configureFetch({ + url: 'https://example.com/profile.json', + content: 'generated-json', + }); + + const profileOrZip = await fetchProfile(args); + expect(profileOrZip).toEqual({ responseType: 'PROFILE', profile }); + expect(reportError.mock.calls.length).toBe(0); + }); + + it('fetches a profile with incorrect content-type headers, no known extension, and attempts to JSON parse it', async function () { + const { profile, args, reportError } = await configureFetch({ + url: 'https://example.com/profile.file', + content: 'generated-json', + }); + + const profileOrZip = await fetchProfile(args); + expect(profileOrZip).toEqual({ responseType: 'PROFILE', profile }); + expect(reportError.mock.calls.length).toBe(0); + }); + + it('fails if a bad zip file is passed in', async function () { + const { args, reportError } = await configureFetch({ + url: 'https://example.com/profile.file', + contentType: 'application/zip', + content: new Uint8Array([0, 1, 2, 3]), + }); + + let userFacingError; + try { + await fetchProfile(args); + } catch (error) { + userFacingError = error; + } + expect(userFacingError).toMatchSnapshot(); + expect(reportError.mock.calls.length).toBeGreaterThan(0); + expect(reportError.mock.calls).toMatchSnapshot(); + }); + + it('fails if a bad profile JSON is passed in', async function () { + const invalidJSON = 'invalid'; + const { args, reportError } = await configureFetch({ + url: 'https://example.com/profile.json', + contentType: 'application/json', + content: encode(invalidJSON), + }); + + let userFacingError; + try { + await fetchProfile(args); + } catch (error) { + userFacingError = error; + } + expect(userFacingError).toMatchSnapshot(); + expect(reportError.mock.calls.length).toBeGreaterThan(0); + expect(reportError.mock.calls).toMatchSnapshot(); + }); + + it('fails if a bad profile JSON is passed in, with no content type', async function () { + const invalidJSON = 'invalid'; + const { args, reportError } = await configureFetch({ + url: 'https://example.com/profile.json', + content: encode(invalidJSON), + }); + + let userFacingError; + try { + await fetchProfile(args); + } catch (error) { + userFacingError = error; + } + expect(userFacingError).toMatchSnapshot(); + expect(reportError.mock.calls.length).toBeGreaterThan(0); + expect(reportError.mock.calls).toMatchSnapshot(); + }); + + it('fallback behavior if a completely unknown file is passed in', async function () { + const invalidJSON = 'invalid'; + const profile = encode(invalidJSON); + const { args } = await configureFetch({ + url: 'https://example.com/profile.unknown', + content: profile, + }); + + let userFacingError = null; + try { + const profileOrZip = await fetchProfile(args); + expect(profileOrZip).toEqual({ + responseType: 'PROFILE', + profile: extractArrayBuffer(profile), + }); + } catch (error) { + userFacingError = error; + } + expect(userFacingError).toBeNull(); + }); +}); diff --git a/src/utils/profile-fetch.ts b/src/utils/profile-fetch.ts new file mode 100644 index 0000000000..3a7f1638ba --- /dev/null +++ b/src/utils/profile-fetch.ts @@ -0,0 +1,346 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { oneLine } from 'common-tags'; +import { assertExhaustiveCheck } from './types'; +import { TemporaryError } from './errors'; +import { decompress, isGzip } from './gz'; +import { isLocalURL } from './url'; +import { GOOGLE_STORAGE_BUCKET } from 'firefox-profiler/app-logic/constants'; +import type JSZip from 'jszip'; + +/** + * Shared utilities for fetching profiles from URLs. + * Used by both the web app (receive-profile.ts) and the CLI (profile-query). + * + * This module was extracted from receive-profile.ts to make the fetching + * logic reusable across different contexts (Redux vs CLI). + */ + +/** + * Convert a profile hash to its Google Cloud Storage URL. + * Public profiles are stored in Google Cloud Storage in the profile-store bucket. + * See https://cloud.google.com/storage/docs/access-public-data + */ +export function getProfileUrlForHash(hash: string): string { + return `https://storage.googleapis.com/${GOOGLE_STORAGE_BUCKET}/${hash}`; +} + +/** + * Extract the actual profile URL from a profiler.firefox.com URL. + * + * Parses URLs like: + * - https://profiler.firefox.com/from-url/http%3A%2F%2F127.0.0.1%3A3000%2Fprofile.json/ + * - https://profiler.firefox.com/public/g9w0fmjjx4bqrky4zg0wb90n65b8g3w0qjjx1t0/calltree/ + * + * Returns the decoded profile URL, or null if this is not a supported datasource. + * This mimics the logic in retrieveProfileFromStore and retrieveProfileForRawUrl + * from receive-profile.ts + */ +export function extractProfileUrlFromProfilerUrl( + profilerUrl: string +): string | null { + try { + // Handle both full URLs and just pathnames + let pathname: string; + if ( + profilerUrl.startsWith('http://') || + profilerUrl.startsWith('https://') + ) { + const url = new URL(profilerUrl); + pathname = url.pathname; + } else { + pathname = profilerUrl; + } + + const pathParts = pathname.split('/').filter((d) => d); + + // Check if this is a from-url datasource + // URL structure: /from-url/{encoded-profile-url}/... + if (pathParts[0] === 'from-url' && pathParts[1]) { + return decodeURIComponent(pathParts[1]); + } + + // Check if this is a public datasource + // URL structure: /public/{hash}/... + // Profile is stored in Google Cloud Storage + if (pathParts[0] === 'public' && pathParts[1]) { + const hash = pathParts[1]; + return getProfileUrlForHash(hash); + } + + return null; + } catch (error) { + console.error('Failed to parse profiler URL:', error); + return null; + } +} + +function _wait(delayMs: number): Promise { + return new Promise((resolve) => setTimeout(resolve, delayMs)); +} + +/** + * Check if a load failure is likely due to Safari's localhost HTTP restriction. + * Safari blocks mixed content (HTTP on HTTPS page) even for localhost. + * This check works in both browser and Node.js (returns false in Node). + */ +function _loadProbablyFailedDueToSafariLocalhostHTTPRestriction( + url: string, + error: Error +): boolean { + // In Node.js, navigator won't exist + if ( + typeof navigator === 'undefined' || + !navigator.userAgent.match(/Safari\/\d+\.\d+/) + ) { + return false; + } + // Check if Safari considers this mixed content. + try { + const parsedUrl = new URL(url); + return ( + error.name === 'TypeError' && + parsedUrl.protocol === 'http:' && + isLocalURL(parsedUrl) && + typeof location !== 'undefined' && + location.protocol === 'https:' + ); + } catch { + return false; + } +} + +export class SafariLocalhostHTTPLoadError extends Error { + override name = 'SafariLocalhostHTTPLoadError'; +} + +/** + * Deduce the file type from a URL and content type. + * This is used to detect zip files vs profile files. + * Exported for use in receive-profile.ts for file handling. + */ +export function deduceContentType( + url: string, + contentType: string | null +): 'application/json' | 'application/zip' | null { + if (contentType === 'application/zip' || contentType === 'application/json') { + return contentType; + } + if (url.match(/\.zip$/)) { + return 'application/zip'; + } + if (url.match(/\.json/)) { + return 'application/json'; + } + return null; +} + +/** + * Parse JSON from an optionally gzipped array buffer. + * Exported for use in receive-profile.ts for direct file processing. + */ +export async function extractJsonFromArrayBuffer( + arrayBuffer: ArrayBuffer +): Promise { + let profileBytes = new Uint8Array(arrayBuffer); + // Check for the gzip magic number in the header. + if (isGzip(profileBytes)) { + profileBytes = await decompress(profileBytes); + } + + const textDecoder = new TextDecoder(); + return JSON.parse(textDecoder.decode(profileBytes)); +} + +/** + * Don't trust third party responses, try and handle a variety of responses gracefully. + */ +async function _extractJsonFromResponse( + response: Response, + reportError: (...data: Array) => void, + fileType: 'application/json' | null +): Promise { + let arrayBuffer: ArrayBuffer | null = null; + try { + // await before returning so that we can catch JSON parse errors. + arrayBuffer = await response.arrayBuffer(); + return await extractJsonFromArrayBuffer(arrayBuffer); + } catch (error) { + // Change the error message depending on the circumstance: + let message; + if (error && typeof error === 'object' && error.name === 'AbortError') { + message = 'The network request to load the profile was aborted.'; + } else if (fileType === 'application/json') { + message = 'The profile’s JSON could not be decoded.'; + } else if (fileType === null && arrayBuffer !== null) { + // If the content type is not specified, use a raw array buffer + // to fallback to other supported profile formats. + return arrayBuffer; + } else { + message = oneLine` + The profile could not be downloaded and decoded. This does not look like a supported file + type. + `; + } + + // Provide helpful debugging information to the console. + reportError(message); + reportError('JSON parsing error:', error); + reportError('Fetch response:', response); + + throw new Error( + `${message} The full error information has been printed out to the DevTool’s console.` + ); + } +} + +/** + * Attempt to load a zip file from a third party. This process can fail, so make sure + * to handle and report the error if it does. + */ +async function _extractZipFromResponse( + response: Response, + reportError: (...data: Array) => void +): Promise { + const buffer = await response.arrayBuffer(); + // Workaround for https://github.com/Stuk/jszip/issues/941 + // When running this code in tests, `buffer` doesn't inherits from _this_ + // realm's ArrayBuffer object, and this breaks JSZip which doesn't account for + // this case. We workaround the issue by wrapping the buffer in an Uint8Array + // that comes from this realm. + const typedBuffer = new Uint8Array(buffer); + try { + const { default: JSZip } = await import('jszip'); + const zip = await JSZip.loadAsync(typedBuffer); + // Catch the error if unable to load the zip. + return zip; + } catch (error) { + const message = 'Unable to open the archive file.'; + reportError(message); + reportError('Error:', error); + reportError('Fetch response:', response); + throw new Error( + `${message} The full error information has been printed out to the DevTool’s console.` + ); + } +} + +export type ProfileOrZip = + | { responseType: 'PROFILE'; profile: unknown } + | { responseType: 'ZIP'; zip: JSZip }; + +/** + * This function guesses the correct content-type (even if one isn't sent) and then + * attempts to use the proper method to extract the response. + */ +async function _extractProfileOrZipFromResponse( + url: string, + response: Response, + reportError: (...data: Array) => void +): Promise { + const contentType = deduceContentType( + url, + response.headers.get('content-type') + ); + switch (contentType) { + case 'application/zip': + return { + responseType: 'ZIP', + zip: await _extractZipFromResponse(response, reportError), + }; + case 'application/json': + case null: + // The content type is null if it is unknown, or an unsupported type. Go ahead + // and try to process it as a profile. + return { + responseType: 'PROFILE', + profile: await _extractJsonFromResponse( + response, + reportError, + contentType + ), + }; + default: + throw assertExhaustiveCheck(contentType); + } +} + +export type FetchProfileArgs = { + url: string; + onTemporaryError: (param: TemporaryError) => void; + // Allow tests to capture the reported error, but normally use console.error. + reportError?: (...data: Array) => void; +}; + +/** + * Tries to fetch a profile on `url`. If the profile is not found, + * `onTemporaryError` is called with an appropriate error, we wait 1 second, and + * then tries again. If we still can't find the profile after 11 tries, the + * returned promise is rejected with a fatal error. + * If we can retrieve the profile properly, the returned promise is resolved + * with the parsed profile or zip file. + * + * This function was moved from receive-profile.ts to make it reusable by + * both the web app and CLI. + */ +export async function fetchProfile( + args: FetchProfileArgs +): Promise { + const MAX_WAIT_SECONDS = 10; + let i = 0; + const { url, onTemporaryError } = args; + // Allow tests to capture the reported error, but normally use console.error. + const reportError = args.reportError || console.error; + + while (true) { + let response; + try { + response = await fetch(url); + } catch (e) { + // Case 1: Exception. + if ( + _loadProbablyFailedDueToSafariLocalhostHTTPRestriction(url, e as Error) + ) { + throw new SafariLocalhostHTTPLoadError(); + } + throw e; + } + + // Case 2: successful answer. + if (response.ok) { + return _extractProfileOrZipFromResponse(url, response, reportError); + } + + // case 3: unrecoverable error. + if (response.status !== 403) { + throw new Error(oneLine` + Could not fetch the profile on remote server. + Response was: ${response.status} ${response.statusText}. + `); + } + + // case 4: 403 errors can be transient while a profile is uploaded. + + if (i++ === MAX_WAIT_SECONDS) { + // In the last iteration we don't send a temporary error because we'll + // throw an error right after the while loop. + break; + } + + onTemporaryError( + new TemporaryError( + 'Profile not found on remote server.', + { count: i, total: MAX_WAIT_SECONDS + 1 } // 11 tries during 10 seconds + ) + ); + + await _wait(1000); + } + + throw new Error(oneLine` + Could not fetch the profile on remote server: + still not found after ${MAX_WAIT_SECONDS} seconds. + `); +} From a4b9d4ed88019048d8de3dd7dbf06a6ae3b56db9 Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:58:38 +0200 Subject: [PATCH 24/64] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20esbuild=20t?= =?UTF-8?q?o=20version=200.28.0=20(#5941)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 418 ++++++++++++++++++++------------------------------- 2 files changed, 165 insertions(+), 255 deletions(-) diff --git a/package.json b/package.json index 43354850f9..532cbb3747 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,7 @@ "caniuse-lite": "^1.0.30001770", "cross-env": "^10.1.0", "devtools-license-check": "^0.9.0", - "esbuild": "^0.27.0", + "esbuild": "^0.28.0", "esbuild-plugin-copy": "^2.1.1", "esbuild-plugin-wasm": "^1.1.0", "eslint": "^9.39.4", diff --git a/yarn.lock b/yarn.lock index dd43ea0c55..92e0fdb88d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1238,135 +1238,135 @@ resolved "https://registry.yarnpkg.com/@epic-web/invariant/-/invariant-1.0.0.tgz#1073e5dee6dd540410784990eb73e4acd25c9813" integrity sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA== -"@esbuild/aix-ppc64@0.27.3": - version "0.27.3" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz#815b39267f9bffd3407ea6c376ac32946e24f8d2" - integrity sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg== - -"@esbuild/android-arm64@0.27.3": - version "0.27.3" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz#19b882408829ad8e12b10aff2840711b2da361e8" - integrity sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg== - -"@esbuild/android-arm@0.27.3": - version "0.27.3" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.27.3.tgz#90be58de27915efa27b767fcbdb37a4470627d7b" - integrity sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA== - -"@esbuild/android-x64@0.27.3": - version "0.27.3" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.27.3.tgz#d7dcc976f16e01a9aaa2f9b938fbec7389f895ac" - integrity sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ== - -"@esbuild/darwin-arm64@0.27.3": - version "0.27.3" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz#9f6cac72b3a8532298a6a4493ed639a8988e8abd" - integrity sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg== - -"@esbuild/darwin-x64@0.27.3": - version "0.27.3" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz#ac61d645faa37fd650340f1866b0812e1fb14d6a" - integrity sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg== - -"@esbuild/freebsd-arm64@0.27.3": - version "0.27.3" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz#b8625689d73cf1830fe58c39051acdc12474ea1b" - integrity sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w== - -"@esbuild/freebsd-x64@0.27.3": - version "0.27.3" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz#07be7dd3c9d42fe0eccd2ab9f9ded780bc53bead" - integrity sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA== - -"@esbuild/linux-arm64@0.27.3": - version "0.27.3" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz#bf31918fe5c798586460d2b3d6c46ed2c01ca0b6" - integrity sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg== - -"@esbuild/linux-arm@0.27.3": - version "0.27.3" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz#28493ee46abec1dc3f500223cd9f8d2df08f9d11" - integrity sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw== - -"@esbuild/linux-ia32@0.27.3": - version "0.27.3" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz#750752a8b30b43647402561eea764d0a41d0ee29" - integrity sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg== - -"@esbuild/linux-loong64@0.27.3": - version "0.27.3" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz#a5a92813a04e71198c50f05adfaf18fc1e95b9ed" - integrity sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA== - -"@esbuild/linux-mips64el@0.27.3": - version "0.27.3" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz#deb45d7fd2d2161eadf1fbc593637ed766d50bb1" - integrity sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw== - -"@esbuild/linux-ppc64@0.27.3": - version "0.27.3" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz#6f39ae0b8c4d3d2d61a65b26df79f6e12a1c3d78" - integrity sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA== - -"@esbuild/linux-riscv64@0.27.3": - version "0.27.3" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz#4c5c19c3916612ec8e3915187030b9df0b955c1d" - integrity sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ== - -"@esbuild/linux-s390x@0.27.3": - version "0.27.3" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz#9ed17b3198fa08ad5ccaa9e74f6c0aff7ad0156d" - integrity sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw== - -"@esbuild/linux-x64@0.27.3": - version "0.27.3" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz#12383dcbf71b7cf6513e58b4b08d95a710bf52a5" - integrity sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA== - -"@esbuild/netbsd-arm64@0.27.3": - version "0.27.3" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz#dd0cb2fa543205fcd931df44f4786bfcce6df7d7" - integrity sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA== - -"@esbuild/netbsd-x64@0.27.3": - version "0.27.3" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz#028ad1807a8e03e155153b2d025b506c3787354b" - integrity sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA== - -"@esbuild/openbsd-arm64@0.27.3": - version "0.27.3" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz#e3c16ff3490c9b59b969fffca87f350ffc0e2af5" - integrity sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw== - -"@esbuild/openbsd-x64@0.27.3": - version "0.27.3" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz#c5a4693fcb03d1cbecbf8b422422468dfc0d2a8b" - integrity sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ== - -"@esbuild/openharmony-arm64@0.27.3": - version "0.27.3" - resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz#082082444f12db564a0775a41e1991c0e125055e" - integrity sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g== - -"@esbuild/sunos-x64@0.27.3": - version "0.27.3" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz#5ab036c53f929e8405c4e96e865a424160a1b537" - integrity sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA== - -"@esbuild/win32-arm64@0.27.3": - version "0.27.3" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz#38de700ef4b960a0045370c171794526e589862e" - integrity sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA== - -"@esbuild/win32-ia32@0.27.3": - version "0.27.3" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz#451b93dc03ec5d4f38619e6cd64d9f9eff06f55c" - integrity sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q== - -"@esbuild/win32-x64@0.27.3": - version "0.27.3" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz#0eaf705c941a218a43dba8e09f1df1d6cd2f1f17" - integrity sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA== +"@esbuild/aix-ppc64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz#7a289c158e29cbf59ea0afc83cc80f06d1c89402" + integrity sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA== + +"@esbuild/android-arm64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz#b8828d9edfa3a92660644eb8de6e4f3c203d7b17" + integrity sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw== + +"@esbuild/android-arm@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.28.0.tgz#5ec1847605e05b5dbe5df90db9ff7e3e4c58dca7" + integrity sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ== + +"@esbuild/android-x64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.28.0.tgz#390642175b88ef82bad4cce03f8ab13fe9b1912e" + integrity sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA== + +"@esbuild/darwin-arm64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz#ae45325960d5950cd6951e4f97396f4e1ff7d8d3" + integrity sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q== + +"@esbuild/darwin-x64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz#c079247d589b6b99449659d94f06951b84bff2e4" + integrity sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ== + +"@esbuild/freebsd-arm64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz#45c456215a486593c94900297202dc11c880a37a" + integrity sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q== + +"@esbuild/freebsd-x64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz#0399494c1c85e4388e9b7040bd60d48f2a5b0d2c" + integrity sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw== + +"@esbuild/linux-arm64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz#d6d9f09ef0de54116bf459a4d53cac7e0952fe39" + integrity sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A== + +"@esbuild/linux-arm@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz#7b42ffa84c288ae94fdc431c1b28a89e3c3b9278" + integrity sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw== + +"@esbuild/linux-ia32@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz#deb15d112ed8dd605346b6b953d23a21ff81253f" + integrity sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ== + +"@esbuild/linux-loong64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz#81fb89d07eecc79b157dea61033757726fce0ca4" + integrity sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg== + +"@esbuild/linux-mips64el@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz#d0e42691b3ff7af9fb2217b70fc01f343bdb62bb" + integrity sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w== + +"@esbuild/linux-ppc64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz#389f3e5e98f17d477c467cc87136e1a076eead87" + integrity sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg== + +"@esbuild/linux-riscv64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz#763bd60d59b242be12da1e67d5729f3024c605fa" + integrity sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ== + +"@esbuild/linux-s390x@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz#aac6061634872e4677de693bce8030d73b1fd055" + integrity sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q== + +"@esbuild/linux-x64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz#4f2917747188fe77632bcec65b2d84b422419779" + integrity sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ== + +"@esbuild/netbsd-arm64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz#814df0ae57a0c386814491b8397eeba82094a947" + integrity sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw== + +"@esbuild/netbsd-x64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz#e01bdf7e60fa1a08e46d46d960b0d9bb8ac210af" + integrity sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw== + +"@esbuild/openbsd-arm64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz#4a15c36aacca68d2d5a4c90b710c06759f4c1ffa" + integrity sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g== + +"@esbuild/openbsd-x64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz#475e6101498a8ecce3008d7c388111d7a27c17bd" + integrity sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA== + +"@esbuild/openharmony-arm64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz#cfdc3957f0b7a69f1bde129aad17fcc2f6fa033e" + integrity sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w== + +"@esbuild/sunos-x64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz#a013c856fecacd1c3aec985c8afe1d1cb017497d" + integrity sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw== + +"@esbuild/win32-arm64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz#eae05e0f35271cad3898b43168d3e9a3bbaf47e5" + integrity sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA== + +"@esbuild/win32-ia32@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz#06161ebc5bf75c08d69feb3c6b22560515913998" + integrity sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA== + +"@esbuild/win32-x64@0.28.0": + version "0.28.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz#04d90d5752b4ce65d2b6ac25eba08ff7624fe07c" + integrity sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw== "@eslint-community/eslint-utils@^4.8.0", "@eslint-community/eslint-utils@^4.9.1": version "4.9.1" @@ -2514,15 +2514,6 @@ "@typescript-eslint/visitor-keys" "8.58.0" debug "^4.4.3" -"@typescript-eslint/project-service@8.56.1": - version "8.56.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.56.1.tgz#65c8d645f028b927bfc4928593b54e2ecd809244" - integrity sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ== - dependencies: - "@typescript-eslint/tsconfig-utils" "^8.56.1" - "@typescript-eslint/types" "^8.56.1" - debug "^4.4.3" - "@typescript-eslint/project-service@8.58.0": version "8.58.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.58.0.tgz#66ceda0aabf7427aec3e2713fa43eb278dead2aa" @@ -2532,15 +2523,7 @@ "@typescript-eslint/types" "^8.58.0" debug "^4.4.3" -"@typescript-eslint/scope-manager@8.56.1", "@typescript-eslint/scope-manager@^8.56.0": - version "8.56.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz#254df93b5789a871351335dd23e20bc164060f24" - integrity sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w== - dependencies: - "@typescript-eslint/types" "8.56.1" - "@typescript-eslint/visitor-keys" "8.56.1" - -"@typescript-eslint/scope-manager@8.58.0": +"@typescript-eslint/scope-manager@8.58.0", "@typescript-eslint/scope-manager@^8.56.0": version "8.58.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz#e304142775e49a1b7ac3c8bf2536714447c72cab" integrity sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ== @@ -2548,11 +2531,6 @@ "@typescript-eslint/types" "8.58.0" "@typescript-eslint/visitor-keys" "8.58.0" -"@typescript-eslint/tsconfig-utils@8.56.1", "@typescript-eslint/tsconfig-utils@^8.56.1": - version "8.56.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz#1afa830b0fada5865ddcabdc993b790114a879b7" - integrity sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ== - "@typescript-eslint/tsconfig-utils@8.58.0", "@typescript-eslint/tsconfig-utils@^8.58.0": version "8.58.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz#c5a8edb21f31e0fdee565724e1b984171c559482" @@ -2569,31 +2547,11 @@ debug "^4.4.3" ts-api-utils "^2.5.0" -"@typescript-eslint/types@8.56.1", "@typescript-eslint/types@^8.56.1": - version "8.56.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.56.1.tgz#975e5942bf54895291337c91b9191f6eb0632ab9" - integrity sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw== - "@typescript-eslint/types@8.58.0", "@typescript-eslint/types@^8.58.0": version "8.58.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.58.0.tgz#e94ae7abdc1c6530e71183c1007b61fa93112a5a" integrity sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww== -"@typescript-eslint/typescript-estree@8.56.1": - version "8.56.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz#3b9e57d8129a860c50864c42188f761bdef3eab0" - integrity sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg== - dependencies: - "@typescript-eslint/project-service" "8.56.1" - "@typescript-eslint/tsconfig-utils" "8.56.1" - "@typescript-eslint/types" "8.56.1" - "@typescript-eslint/visitor-keys" "8.56.1" - debug "^4.4.3" - minimatch "^10.2.2" - semver "^7.7.3" - tinyglobby "^0.2.15" - ts-api-utils "^2.4.0" - "@typescript-eslint/typescript-estree@8.58.0": version "8.58.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz#ed233faa8e2f2a2e1357c3e7d553d6465a0ee59a" @@ -2609,7 +2567,7 @@ tinyglobby "^0.2.15" ts-api-utils "^2.5.0" -"@typescript-eslint/utils@8.58.0": +"@typescript-eslint/utils@8.58.0", "@typescript-eslint/utils@^8.0.0", "@typescript-eslint/utils@^8.56.0": version "8.58.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.58.0.tgz#21a74a7963b0d288b719a4121c7dd555adaab3c3" integrity sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA== @@ -2619,24 +2577,6 @@ "@typescript-eslint/types" "8.58.0" "@typescript-eslint/typescript-estree" "8.58.0" -"@typescript-eslint/utils@^8.0.0", "@typescript-eslint/utils@^8.56.0": - version "8.56.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.56.1.tgz#5a86acaf9f1b4c4a85a42effb217f73059f6deb7" - integrity sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA== - dependencies: - "@eslint-community/eslint-utils" "^4.9.1" - "@typescript-eslint/scope-manager" "8.56.1" - "@typescript-eslint/types" "8.56.1" - "@typescript-eslint/typescript-estree" "8.56.1" - -"@typescript-eslint/visitor-keys@8.56.1": - version "8.56.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz#50e03475c33a42d123dc99e63acf1841c0231f87" - integrity sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw== - dependencies: - "@typescript-eslint/types" "8.56.1" - eslint-visitor-keys "^5.0.0" - "@typescript-eslint/visitor-keys@8.58.0": version "8.58.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz#2abd55a4be70fd55967aceaba4330b9ba9f45189" @@ -4489,37 +4429,37 @@ esbuild-plugin-wasm@^1.1.0: resolved "https://registry.yarnpkg.com/esbuild-plugin-wasm/-/esbuild-plugin-wasm-1.1.0.tgz#062c0e62c266e94165c66ebcbb5852a1cdbfd7cd" integrity sha512-0bQ6+1tUbySSnxzn5jnXHMDvYnT0cN/Wd4Syk8g/sqAIJUg7buTIi22svS3Qz6ssx895NT+TgLPb33xi1OkZig== -esbuild@^0.27.0: - version "0.27.3" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.27.3.tgz#5859ca8e70a3af956b26895ce4954d7e73bd27a8" - integrity sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg== +esbuild@^0.28.0: + version "0.28.0" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.28.0.tgz#5dee347ffb3e3874212a35a69836b077b1ce6d96" + integrity sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw== optionalDependencies: - "@esbuild/aix-ppc64" "0.27.3" - "@esbuild/android-arm" "0.27.3" - "@esbuild/android-arm64" "0.27.3" - "@esbuild/android-x64" "0.27.3" - "@esbuild/darwin-arm64" "0.27.3" - "@esbuild/darwin-x64" "0.27.3" - "@esbuild/freebsd-arm64" "0.27.3" - "@esbuild/freebsd-x64" "0.27.3" - "@esbuild/linux-arm" "0.27.3" - "@esbuild/linux-arm64" "0.27.3" - "@esbuild/linux-ia32" "0.27.3" - "@esbuild/linux-loong64" "0.27.3" - "@esbuild/linux-mips64el" "0.27.3" - "@esbuild/linux-ppc64" "0.27.3" - "@esbuild/linux-riscv64" "0.27.3" - "@esbuild/linux-s390x" "0.27.3" - "@esbuild/linux-x64" "0.27.3" - "@esbuild/netbsd-arm64" "0.27.3" - "@esbuild/netbsd-x64" "0.27.3" - "@esbuild/openbsd-arm64" "0.27.3" - "@esbuild/openbsd-x64" "0.27.3" - "@esbuild/openharmony-arm64" "0.27.3" - "@esbuild/sunos-x64" "0.27.3" - "@esbuild/win32-arm64" "0.27.3" - "@esbuild/win32-ia32" "0.27.3" - "@esbuild/win32-x64" "0.27.3" + "@esbuild/aix-ppc64" "0.28.0" + "@esbuild/android-arm" "0.28.0" + "@esbuild/android-arm64" "0.28.0" + "@esbuild/android-x64" "0.28.0" + "@esbuild/darwin-arm64" "0.28.0" + "@esbuild/darwin-x64" "0.28.0" + "@esbuild/freebsd-arm64" "0.28.0" + "@esbuild/freebsd-x64" "0.28.0" + "@esbuild/linux-arm" "0.28.0" + "@esbuild/linux-arm64" "0.28.0" + "@esbuild/linux-ia32" "0.28.0" + "@esbuild/linux-loong64" "0.28.0" + "@esbuild/linux-mips64el" "0.28.0" + "@esbuild/linux-ppc64" "0.28.0" + "@esbuild/linux-riscv64" "0.28.0" + "@esbuild/linux-s390x" "0.28.0" + "@esbuild/linux-x64" "0.28.0" + "@esbuild/netbsd-arm64" "0.28.0" + "@esbuild/netbsd-x64" "0.28.0" + "@esbuild/openbsd-arm64" "0.28.0" + "@esbuild/openbsd-x64" "0.28.0" + "@esbuild/openharmony-arm64" "0.28.0" + "@esbuild/sunos-x64" "0.28.0" + "@esbuild/win32-arm64" "0.28.0" + "@esbuild/win32-ia32" "0.28.0" + "@esbuild/win32-x64" "0.28.0" escalade@^3.1.1, escalade@^3.2.0: version "3.2.0" @@ -10364,16 +10304,7 @@ string-length@^4.0.2: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -10503,7 +10434,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -10517,13 +10448,6 @@ strip-ansi@^0.3.0: dependencies: ansi-regex "^0.2.1" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1, strip-ansi@^7.1.0, strip-ansi@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba" @@ -10923,11 +10847,6 @@ trough@^2.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-2.0.2.tgz#94a3aa9d5ce379fc561f6244905b3f36b7458d96" integrity sha512-FnHq5sTMxC0sk957wHDzRnemFnNBvt/gSY99HzK8F7UP5WAbvP70yX5bd7CjEQkN+TjdxwI7g7lJ6podqrG2/w== -ts-api-utils@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.4.0.tgz#2690579f96d2790253bdcf1ca35d569ad78f9ad8" - integrity sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA== - ts-api-utils@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz#4acd4a155e22734990a5ed1fe9e97f113bcb37c1" @@ -11905,16 +11824,7 @@ workbox-window@7.4.0, workbox-window@^7.4.0: "@types/trusted-types" "^2.0.2" workbox-core "7.4.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From 38336c8a6db44478f670f5e75db092d5a652a93b Mon Sep 17 00:00:00 2001 From: Markus Stange Date: Thu, 9 Apr 2026 23:08:47 -0400 Subject: [PATCH 25/64] Remove panelLayoutGeneration. We originally introduced this to resolve #1205 and #736, but these days it's not needed anymore because we have ResizeObservers in the relevant places. --- src/actions/app.ts | 4 -- src/components/app/DetailsContainer.tsx | 20 ++------- src/components/app/ProfileViewer.tsx | 6 --- src/components/shared/chart/Viewport.tsx | 11 +---- src/components/timeline/FullTimeline.tsx | 5 --- .../timeline/OverflowEdgeIndicator.tsx | 1 - src/reducers/app.ts | 42 ------------------- src/selectors/app.tsx | 2 - src/test/components/Viewport.test.tsx | 12 ++---- src/test/store/app.test.ts | 31 +------------- src/types/actions.ts | 3 -- src/types/state.ts | 1 - 12 files changed, 10 insertions(+), 128 deletions(-) diff --git a/src/actions/app.ts b/src/actions/app.ts index aabb1706b5..5792312927 100644 --- a/src/actions/app.ts +++ b/src/actions/app.ts @@ -109,10 +109,6 @@ export function changeSidebarOpenState(tab: TabSlug, isOpen: boolean): Action { return { type: 'CHANGE_SIDEBAR_OPEN_STATE', tab, isOpen }; } -export function invalidatePanelLayout(): Action { - return { type: 'INCREMENT_PANEL_LAYOUT_GENERATION' as const }; -} - /** * The viewport component provides a hint to use shift to zoom scroll. The first * time a user does this, the hint goes away. diff --git a/src/components/app/DetailsContainer.tsx b/src/components/app/DetailsContainer.tsx index 5f3911efbd..b1fe74f00d 100644 --- a/src/components/app/DetailsContainer.tsx +++ b/src/components/app/DetailsContainer.tsx @@ -7,7 +7,6 @@ import SplitterLayout from 'react-splitter-layout'; import { Details } from './Details'; import { selectSidebar } from 'firefox-profiler/components/sidebar'; -import { invalidatePanelLayout } from 'firefox-profiler/actions/app'; import { getSelectedTab } from 'firefox-profiler/selectors/url-state'; import { getIsSidebarOpen } from 'firefox-profiler/selectors/app'; import explicitConnect from 'firefox-profiler/utils/connect'; @@ -22,17 +21,9 @@ type StateProps = { readonly isSidebarOpen: boolean; }; -type DispatchProps = { - readonly invalidatePanelLayout: typeof invalidatePanelLayout; -}; - -type Props = ConnectedProps<{}, StateProps, DispatchProps>; +type Props = ConnectedProps<{}, StateProps, {}>; -function DetailsContainerImpl({ - selectedTab, - isSidebarOpen, - invalidatePanelLayout, -}: Props) { +function DetailsContainerImpl({ selectedTab, isSidebarOpen }: Props) { const Sidebar = selectSidebar(selectedTab); return ( @@ -40,7 +31,6 @@ function DetailsContainerImpl({ customClassName="DetailsContainer" percentage secondaryInitialSize={20} - onDragEnd={invalidatePanelLayout} >
{Sidebar && isSidebarOpen ? : null} @@ -48,13 +38,11 @@ function DetailsContainerImpl({ ); } -export const DetailsContainer = explicitConnect<{}, StateProps, DispatchProps>({ +export const DetailsContainer = explicitConnect<{}, StateProps, {}>({ mapStateToProps: (state) => ({ selectedTab: getSelectedTab(state), isSidebarOpen: getIsSidebarOpen(state), }), - mapDispatchToProps: { - invalidatePanelLayout, - }, + mapDispatchToProps: {}, component: DetailsContainerImpl, }); diff --git a/src/components/app/ProfileViewer.tsx b/src/components/app/ProfileViewer.tsx index e00a87117d..bc140e3af4 100644 --- a/src/components/app/ProfileViewer.tsx +++ b/src/components/app/ProfileViewer.tsx @@ -21,7 +21,6 @@ import { returnToZipFileList } from 'firefox-profiler/actions/zipped-profiles'; import { Timeline } from 'firefox-profiler/components/timeline'; import { getHasZipFile } from 'firefox-profiler/selectors/zipped-profiles'; import SplitterLayout from 'react-splitter-layout'; -import { invalidatePanelLayout } from 'firefox-profiler/actions/app'; import { getTimelineHeight } from 'firefox-profiler/selectors/app'; import { getIsBottomBoxOpen } from 'firefox-profiler/selectors/url-state'; import { @@ -54,7 +53,6 @@ type StateProps = { type DispatchProps = { readonly returnToZipFileList: typeof returnToZipFileList; - readonly invalidatePanelLayout: typeof invalidatePanelLayout; }; type Props = ConnectedProps<{}, StateProps, DispatchProps>; @@ -64,7 +62,6 @@ class ProfileViewerImpl extends PureComponent { const { hasZipFile, returnToZipFileList, - invalidatePanelLayout, timelineHeight, isUploading, uploadProgress, @@ -136,7 +133,6 @@ class ProfileViewerImpl extends PureComponent { primaryIndex={1} // The Timeline is secondary. secondaryInitialSize={270} - onDragEnd={invalidatePanelLayout} > { primaryIndex={0} // The BottomBox is secondary. secondaryInitialSize={40} - onDragEnd={invalidatePanelLayout} > {isBottomBoxOpen ? : null} @@ -178,7 +173,6 @@ export const ProfileViewer = explicitConnect<{}, StateProps, DispatchProps>({ }), mapDispatchToProps: { returnToZipFileList, - invalidatePanelLayout, }, component: ProfileViewerImpl, }); diff --git a/src/components/shared/chart/Viewport.tsx b/src/components/shared/chart/Viewport.tsx index 4471702639..8d732deeda 100644 --- a/src/components/shared/chart/Viewport.tsx +++ b/src/components/shared/chart/Viewport.tsx @@ -6,10 +6,7 @@ import * as React from 'react'; import classNames from 'classnames'; import explicitConnect from 'firefox-profiler/utils/connect'; import { getResizeObserverWrapper } from 'firefox-profiler/utils/resize-observer-wrapper'; -import { - getHasZoomedViaMousewheel, - getPanelLayoutGeneration, -} from 'firefox-profiler/selectors/app'; +import { getHasZoomedViaMousewheel } from 'firefox-profiler/selectors/app'; import { setHasZoomedViaMousewheel } from 'firefox-profiler/actions/app'; import { updatePreviewSelection } from 'firefox-profiler/actions/profile-view'; @@ -132,7 +129,6 @@ export type Viewport = { }; type ChartViewportImplStateProps = { - readonly panelLayoutGeneration: number; readonly hasZoomedViaMousewheel?: boolean; }; @@ -316,10 +312,6 @@ class ChartViewportImpl extends React.PureComponent< this.setState({ horizontalViewport, }); - } else if ( - this.props.panelLayoutGeneration !== newProps.panelLayoutGeneration - ) { - this._setSizeNextFrame(); } } @@ -871,7 +863,6 @@ export function withChartViewport( ChartViewportImplDispatchProps >({ mapStateToProps: (state) => ({ - panelLayoutGeneration: getPanelLayoutGeneration(state), hasZoomedViaMousewheel: getHasZoomedViaMousewheel(state), }), mapDispatchToProps: { setHasZoomedViaMousewheel, updatePreviewSelection }, diff --git a/src/components/timeline/FullTimeline.tsx b/src/components/timeline/FullTimeline.tsx index 97eb3b65d3..cf1729f8e6 100644 --- a/src/components/timeline/FullTimeline.tsx +++ b/src/components/timeline/FullTimeline.tsx @@ -21,7 +21,6 @@ import { getGlobalTrackReferences, getTrackCount, getGlobalTrackOrder, - getPanelLayoutGeneration, } from 'firefox-profiler/selectors'; import { TimelineTrackContextMenu } from './TrackContextMenu'; @@ -57,7 +56,6 @@ type StateProps = { readonly globalTracks: GlobalTrack[]; readonly globalTrackOrder: TrackIndex[]; readonly globalTrackReferences: GlobalTrackReference[]; - readonly panelLayoutGeneration: number; readonly zeroAt: Milliseconds; readonly profileTimelineUnit: TimelineUnit; readonly trackCount: TrackCount; @@ -145,7 +143,6 @@ class FullTimelineImpl extends React.PureComponent { profileTimelineUnit, width, globalTrackReferences, - panelLayoutGeneration, trackCount, changeRightClickedTrack, innerElementRef, @@ -173,7 +170,6 @@ class FullTimelineImpl extends React.PureComponent {

= ( } }; -/** - * The panels that make up the timeline, details view, and sidebar can all change - * their sizes depending on the state that is fed to them. In order to control - * the invalidations of this sizing information, provide a "generation" value that - * increases monotonically for any change that potentially changes the sizing of - * any of the panels. This provides a mechanism for subscribing components to - * deterministically update their sizing correctly. - */ -const panelLayoutGeneration: Reducer = (state = 0, action) => { - switch (action.type) { - case 'INCREMENT_PANEL_LAYOUT_GENERATION': - // Sidebar: (fallthrough) - case 'CHANGE_SIDEBAR_OPEN_STATE': - // Timeline: (fallthrough) - case 'HIDE_GLOBAL_TRACK': - case 'SHOW_ALL_TRACKS': - case 'SHOW_PROVIDED_TRACKS': - case 'HIDE_PROVIDED_TRACKS': - case 'SHOW_GLOBAL_TRACK': - case 'SHOW_GLOBAL_TRACK_INCLUDING_LOCAL_TRACKS': - case 'ISOLATE_PROCESS': - case 'ISOLATE_PROCESS_MAIN_THREAD': - case 'HIDE_LOCAL_TRACK': - case 'SHOW_LOCAL_TRACK': - case 'ISOLATE_LOCAL_TRACK': - case 'TOGGLE_RESOURCES_PANEL': - case 'ENABLE_EXPERIMENTAL_CPU_GRAPHS': - case 'ENABLE_EXPERIMENTAL_PROCESS_CPU_TRACKS': - case 'CHANGE_TAB_FILTER': - // Committed range changes: (fallthrough) - case 'COMMIT_RANGE': - case 'POP_COMMITTED_RANGES': - // Bottom box: (fallthrough) - case 'UPDATE_BOTTOM_BOX': - case 'CLOSE_BOTTOM_BOX_FOR_TAB': - return state + 1; - default: - return state; - } -}; - /** * Clicking on tracks can switch between different tabs. This piece of state holds * on to the last relevant thread-based tab that was viewed. This makes the UX nicer @@ -365,7 +324,6 @@ const appStateReducer: Reducer = combineReducers({ urlSetupPhase, hasZoomedViaMousewheel, isSidebarOpenPerPanel, - panelLayoutGeneration, lastVisibleThreadTabSlug, trackThreadHeights, isNewlyPublished, diff --git a/src/selectors/app.tsx b/src/selectors/app.tsx index d46d71eb4e..ec78760d84 100644 --- a/src/selectors/app.tsx +++ b/src/selectors/app.tsx @@ -55,8 +55,6 @@ export const getHasZoomedViaMousewheel: Selector = (state) => { }; export const getIsSidebarOpen: Selector = (state) => getApp(state).isSidebarOpenPerPanel[getSelectedTab(state)]; -export const getPanelLayoutGeneration: Selector = (state) => - getApp(state).panelLayoutGeneration; export const getLastVisibleThreadTabSlug: Selector = (state) => getApp(state).lastVisibleThreadTabSlug; export const getTrackThreadHeights: Selector<{ diff --git a/src/test/components/Viewport.test.tsx b/src/test/components/Viewport.test.tsx index b32f27be94..b0f822007f 100644 --- a/src/test/components/Viewport.test.tsx +++ b/src/test/components/Viewport.test.tsx @@ -17,8 +17,6 @@ import { getPreviewSelection, } from '../../selectors/profile'; -import { changeSidebarOpenState } from '../../actions/app'; - import explicitConnect from '../../utils/connect'; import { ensureExists } from '../../utils/types'; @@ -27,6 +25,7 @@ import { autoMockElementSize, setMockedElementSize, } from '../fixtures/mocks/element-size'; +import { triggerResizeObservers } from '../fixtures/mocks/resize-observer'; import { mockRaf } from '../fixtures/mocks/request-animation-frame'; import { storeWithProfile } from '../fixtures/stores'; import { getProfileFromTextSamples } from '../fixtures/profiles/processed-profile'; @@ -589,8 +588,8 @@ describe('Viewport', function () { }); }); - it('reacts to changes to the panel layout generation', function () { - const { dispatch, getChartViewport, flushRafCalls } = setup(); + it('reacts to container size changes', function () { + const { getChartViewport } = setup(); expect(getChartViewport()).toMatchObject({ containerWidth: BOUNDING_BOX_WIDTH, @@ -606,10 +605,7 @@ describe('Viewport', function () { ...INITIAL_ELEMENT_SIZE, width: BOUNDING_BOX_WIDTH - boundingWidthDiff, }); - act(() => { - dispatch(changeSidebarOpenState('calltree', true)); - }); - flushRafCalls(); + triggerResizeObservers(); expect(getChartViewport()).toMatchObject({ containerWidth: BOUNDING_BOX_WIDTH - boundingWidthDiff, diff --git a/src/test/store/app.test.ts b/src/test/store/app.test.ts index 1ff0040aab..42542a8787 100644 --- a/src/test/store/app.test.ts +++ b/src/test/store/app.test.ts @@ -1,13 +1,11 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { storeWithSimpleProfile, storeWithProfile } from '../fixtures/stores'; +import { storeWithSimpleProfile } from '../fixtures/stores'; import * as UrlStateSelectors from '../../selectors/url-state'; import * as AppSelectors from '../../selectors/app'; import createStore from '../../app-logic/create-store'; import { withAnalyticsMock } from '../fixtures/mocks/analytics'; -import { isolateProcess } from '../../actions/profile-view'; -import { getProfileWithNiceTracks } from '../fixtures/profiles/tracks'; import * as AppActions from '../../actions/app'; @@ -148,31 +146,4 @@ describe('app actions', function () { expect(UrlStateSelectors.getSelectedTab(getState())).toEqual('calltree'); }); }); - - describe('panelLayoutGeneration', function () { - it('can be manually updated using an action', function () { - const { dispatch, getState } = storeWithSimpleProfile(); - expect(AppSelectors.getPanelLayoutGeneration(getState())).toBe(0); - dispatch(AppActions.invalidatePanelLayout()); - expect(AppSelectors.getPanelLayoutGeneration(getState())).toBe(1); - dispatch(AppActions.invalidatePanelLayout()); - expect(AppSelectors.getPanelLayoutGeneration(getState())).toBe(2); - }); - - it('will be updated when working with the sidebar', function () { - const { dispatch, getState } = storeWithSimpleProfile(); - expect(AppSelectors.getPanelLayoutGeneration(getState())).toBe(0); - dispatch(AppActions.changeSidebarOpenState('flame-graph', false)); - expect(AppSelectors.getPanelLayoutGeneration(getState())).toBe(1); - }); - - it('will be updated when working with the timeline', function () { - const { dispatch, getState } = storeWithProfile( - getProfileWithNiceTracks() - ); - expect(AppSelectors.getPanelLayoutGeneration(getState())).toBe(0); - dispatch(isolateProcess(0)); - expect(AppSelectors.getPanelLayoutGeneration(getState())).toBe(1); - }); - }); }); diff --git a/src/types/actions.ts b/src/types/actions.ts index 6f11b70e73..6f8e0bd1ed 100644 --- a/src/types/actions.ts +++ b/src/types/actions.ts @@ -321,9 +321,6 @@ type ProfileAction = readonly type: 'SET_CONTEXT_MENU_VISIBILITY'; readonly isVisible: boolean; } - | { - readonly type: 'INCREMENT_PANEL_LAYOUT_GENERATION'; - } | { readonly type: 'HAS_ZOOMED_VIA_MOUSEWHEEL' } | { readonly type: 'DISMISS_NEWLY_PUBLISHED' } | { diff --git a/src/types/state.ts b/src/types/state.ts index 6e18bb2630..b7fbf9e6c2 100644 --- a/src/types/state.ts +++ b/src/types/state.ts @@ -186,7 +186,6 @@ export type AppState = { readonly hasZoomedViaMousewheel: boolean; readonly isSidebarOpenPerPanel: IsOpenPerPanelState; readonly sidebarOpenCategories: Map>; - readonly panelLayoutGeneration: number; readonly lastVisibleThreadTabSlug: TabSlug; readonly trackThreadHeights: Record; readonly isNewlyPublished: boolean; From 8115beb1c5f77e257b9cd8c9a74fad9f1ce96023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Sun, 12 Apr 2026 14:17:13 +0200 Subject: [PATCH 26/64] Fix output-fixing-commands.js silently failing on Windows On Windows, npm binaries (cross-env, jest, run-p, etc.) are installed as .cmd files and can't be found by spawnSync without shell: true. This caused all scripts using this wrapper to silently succeed without running anything, since spawnSync returns status: null on ENOENT, which Node.js treats as exit code 0. Fix by passing the command as a single joined string with shell: true, and add an explicit error check for result.error to catch spawn failures in the future. --- bin/output-fixing-commands.js | 18 +++++++++++++----- package.json | 1 + 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/bin/output-fixing-commands.js b/bin/output-fixing-commands.js index 262437fd81..22204d3dce 100644 --- a/bin/output-fixing-commands.js +++ b/bin/output-fixing-commands.js @@ -5,7 +5,7 @@ // This file runs before linting commands and intercept errors so that more // friendly errors can be output. -const cp = require('child_process'); +const spawn = require('cross-spawn'); const fixingCommands = { lint: 'lint-fix', @@ -21,14 +21,22 @@ const currentScriptName = process.env.npm_lifecycle_event; // Redirect the main lint command, but not individual commands. if (currentScriptName === 'lint' && command.includes('--fix')) { console.log(`🔧 Detected --fix flag, running: yarn lint-fix`); - const result = cp.spawnSync('yarn', ['lint-fix'], { stdio: 'inherit' }); - process.exitCode = result.status; + const result = spawn.sync('yarn', ['lint-fix'], { stdio: 'inherit' }); + if (result.error) { + console.error(`❌ Failed to spawn command: ${result.error.message}`); + process.exitCode = 1; + } else { + process.exitCode = result.status; + } process.exit(); } -const result = cp.spawnSync(command[0], command.slice(1), { stdio: 'inherit' }); +const result = spawn.sync(command[0], command.slice(1), { stdio: 'inherit' }); -if (result.status !== 0) { +if (result.error) { + console.error(`❌ Failed to spawn command: ${result.error.message}`); + process.exitCode = 1; +} else if (result.status !== 0) { process.exitCode = result.status; if (currentScriptName && currentScriptName in fixingCommands) { console.log( diff --git a/package.json b/package.json index 67ff4c974e..df7257899f 100644 --- a/package.json +++ b/package.json @@ -141,6 +141,7 @@ "browserslist-to-esbuild": "^2.1.1", "caniuse-lite": "^1.0.30001770", "cross-env": "^10.1.0", + "cross-spawn": "^7.0.6", "devtools-license-check": "^0.9.0", "esbuild": "^0.28.0", "esbuild-plugin-copy": "^2.1.1", From c678a01ff753613c4da63b3c938a24adf4ad3bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Sun, 12 Apr 2026 14:30:31 +0200 Subject: [PATCH 27/64] Fix line ending issues on GitHub Actions in CI It looks like GitHub Actions always converts line endings to CRLF, which was triggering an error for our linter. This makes sure that all line endings are normalized to LF --- .gitattributes | 3 +++ .prettierrc.js | 1 + 2 files changed, 4 insertions(+) diff --git a/.gitattributes b/.gitattributes index 02a029af7f..2db04380d0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,6 @@ +# Normalize line endings to LF on all platforms. +* text=auto eol=lf + # The following are not technically binary, but this makes it so that git does not try # and treat them like normal text. *.min.js binary diff --git a/.prettierrc.js b/.prettierrc.js index a425d3f761..ed20b91246 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,4 +1,5 @@ module.exports = { singleQuote: true, trailingComma: 'es5', + endOfLine: 'lf', }; From 855eaf1f296bd1730c74157c53e8b32f26114003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Mon, 13 Apr 2026 14:14:57 +0200 Subject: [PATCH 28/64] Convert profile-logic/js-tracer.tsx to a ts file (#5942) It looks like this was made a tsx file by mistake. Let's convert it back. It will help with the pq tool. --- src/profile-logic/{js-tracer.tsx => js-tracer.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/profile-logic/{js-tracer.tsx => js-tracer.ts} (100%) diff --git a/src/profile-logic/js-tracer.tsx b/src/profile-logic/js-tracer.ts similarity index 100% rename from src/profile-logic/js-tracer.tsx rename to src/profile-logic/js-tracer.ts From fde7ca6065c8953129f1dfb61787a731aab94278 Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:49:02 +0200 Subject: [PATCH 29/64] Update all Yarn dependencies (2026-04-13) (#5911) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> --- package.json | 42 +- yarn.lock | 1113 +++++++++++++++++++++++++------------------------- 2 files changed, 568 insertions(+), 587 deletions(-) diff --git a/package.json b/package.json index df7257899f..08a9fb32c3 100644 --- a/package.json +++ b/package.json @@ -63,9 +63,9 @@ "@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-javascript": "^6.2.5", "@codemirror/lang-rust": "^6.0.2", - "@codemirror/language": "^6.12.2", - "@codemirror/state": "^6.5.4", - "@codemirror/view": "^6.39.14", + "@codemirror/language": "^6.12.3", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.41.0", "@firefox-devtools/react-contextmenu": "^5.2.3", "@fluent/bundle": "^0.19.1", "@fluent/langneg": "^0.7.0", @@ -78,8 +78,8 @@ "classnames": "^2.5.1", "common-tags": "^1.8.2", "copy-to-clipboard": "^3.3.3", - "core-js": "^3.48.0", - "devtools-reps": "^0.27.4", + "core-js": "^3.49.0", + "devtools-reps": "^0.27.6", "escape-string-regexp": "^4.0.0", "gecko-profiler-demangle": "^0.4.0", "idb": "^8.0.3", @@ -92,7 +92,7 @@ "mixedtuplemap": "^1.0.0", "namedtuplemap": "^1.0.0", "photon-colors": "^3.3.2", - "protobufjs": "^8.0.0", + "protobufjs": "^8.0.1", "query-string": "^9.3.1", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -104,7 +104,7 @@ "redux-thunk": "^3.1.0", "reselect": "^4.1.8", "url": "^0.11.4", - "valibot": "^1.2.0", + "valibot": "^1.3.1", "weaktuplemap": "^1.0.0", "workbox-window": "^7.4.0" }, @@ -114,7 +114,7 @@ "@babel/eslint-parser": "^7.28.6", "@babel/eslint-plugin": "^7.27.1", "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/preset-env": "^7.29.0", + "@babel/preset-env": "^7.29.2", "@babel/preset-react": "^7.28.5", "@babel/preset-typescript": "^7.28.5", "@eslint/js": "^9.39.4", @@ -125,21 +125,21 @@ "@types/common-tags": "^1.8.4", "@types/jest": "^30.0.0", "@types/minimist": "^1.2.5", - "@types/node": "^22.19.15", + "@types/node": "^22.19.17", "@types/query-string": "^6.3.0", "@types/react": "^18.3.28", "@types/react-dom": "^18.3.1", "@types/react-splitter-layout": "^4.0.0", "@types/redux-logger": "^3.0.6", "@types/tgwf__co2": "^0.14.2", - "@typescript-eslint/eslint-plugin": "^8.58.0", - "@typescript-eslint/parser": "^8.58.0", + "@typescript-eslint/eslint-plugin": "^8.58.1", + "@typescript-eslint/parser": "^8.58.1", "alex": "^11.0.1", - "babel-jest": "^30.2.0", - "babel-plugin-module-resolver": "^5.0.2", - "browserslist": "^4.28.1", + "babel-jest": "^30.3.0", + "babel-plugin-module-resolver": "^5.0.3", + "browserslist": "^4.28.2", "browserslist-to-esbuild": "^2.1.1", - "caniuse-lite": "^1.0.30001770", + "caniuse-lite": "^1.0.30001780", "cross-env": "^10.1.0", "cross-spawn": "^7.0.6", "devtools-license-check": "^0.9.0", @@ -150,17 +150,17 @@ "eslint-config-prettier": "^10.1.8", "eslint-import-resolver-alias": "^1.1.2", "eslint-plugin-import": "^2.32.0", - "eslint-plugin-jest": "^29.15.0", + "eslint-plugin-jest": "^29.15.1", "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.0", + "eslint-plugin-testing-library": "^7.16.2", "fake-indexeddb": "^6.2.5", "fetch-mock": "^12.6.0", "globals": "^17.4.0", "husky": "^4.3.8", - "jest": "^30.2.0", - "jest-environment-jsdom": "^30.2.0", + "jest": "^30.3.0", + "jest-environment-jsdom": "^30.3.0", "jest-extended": "^7.0.0", "local-web-server": "^5.4.0", "lockfile-lint": "^5.0.0", @@ -172,11 +172,11 @@ "postinstall-postinstall": "^2.1.0", "prettier": "^3.8.1", "rimraf": "^6.1.3", - "stylelint": "^17.3.0", + "stylelint": "^17.6.0", "stylelint-config-idiomatic-order": "^10.0.0", "stylelint-config-standard": "^40.0.0", "typescript": "^6.0.2", - "typescript-eslint": "^8.58.0", + "typescript-eslint": "^8.58.1", "workbox-cli": "^7.4.0", "yargs": "^18.0.0" }, diff --git a/yarn.lock b/yarn.lock index 6303ce04b5..c1354c1ef9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -903,10 +903,10 @@ "@babel/helper-create-regexp-features-plugin" "^7.28.5" "@babel/helper-plugin-utils" "^7.28.6" -"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.29.0": - version "7.29.0" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.29.0.tgz#c55db400c515a303662faaefd2d87e796efa08d0" - integrity sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w== +"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.29.2": + version "7.29.2" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.29.2.tgz#5a173f22c7d8df362af1c9fe31facd320de4a86c" + integrity sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw== dependencies: "@babel/compat-data" "^7.29.0" "@babel/helper-compilation-targets" "^7.28.6" @@ -1110,10 +1110,10 @@ "@codemirror/language" "^6.0.0" "@lezer/rust" "^1.0.0" -"@codemirror/language@^6.0.0", "@codemirror/language@^6.12.2", "@codemirror/language@^6.6.0": - version "6.12.2" - resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.12.2.tgz#7db5a46757411cf251e8f450474c05710c27d42c" - integrity sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg== +"@codemirror/language@^6.0.0", "@codemirror/language@^6.12.3", "@codemirror/language@^6.6.0": + version "6.12.3" + resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.12.3.tgz#0b220182973a4c19850b29f7dd82aec1bbae3d7e" + integrity sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA== dependencies: "@codemirror/state" "^6.0.0" "@codemirror/view" "^6.23.0" @@ -1131,19 +1131,19 @@ "@codemirror/view" "^6.0.0" crelt "^1.0.5" -"@codemirror/state@^6.0.0", "@codemirror/state@^6.5.0", "@codemirror/state@^6.5.4": - version "6.5.4" - resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.5.4.tgz#f5be4b8c0d2310180d5f15a9f641c21ca69faf19" - integrity sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw== +"@codemirror/state@^6.0.0", "@codemirror/state@^6.6.0": + version "6.6.0" + resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.6.0.tgz#b88dbdc14aea4ace3c6d67bb77fe28bb84e4394e" + integrity sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ== dependencies: "@marijn/find-cluster-break" "^1.0.0" -"@codemirror/view@^6.0.0", "@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0", "@codemirror/view@^6.39.14": - version "6.39.14" - resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.39.14.tgz#ebecaee4c76566c3eb61dfe4d6bf386c617de5cf" - integrity sha512-WJcvgHm/6Q7dvGT0YFv/6PSkoc36QlR0VCESS6x9tGsnF1lWLmmYxOgX3HH6v8fo6AvSLgpcs+H0Olre6MKXlg== +"@codemirror/view@^6.0.0", "@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0", "@codemirror/view@^6.41.0": + version "6.41.0" + resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.41.0.tgz#482cbcb5f8f90131908e5f9c0e2f4681e0684db0" + integrity sha512-6H/qadXsVuDY219Yljhohglve8xf4B8xJkVOEWfA5uiYKiTFppjqsvsfR5iPA0RbvRBoOyTZpbLIxe9+0UR8xA== dependencies: - "@codemirror/state" "^6.5.0" + "@codemirror/state" "^6.6.0" crelt "^1.0.6" style-mod "^4.1.0" w3c-keyname "^2.2.4" @@ -1181,10 +1181,10 @@ resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz#e1c65dc09378b42f26a111fca7f7075fc2c26164" integrity sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w== -"@csstools/css-syntax-patches-for-csstree@^1.0.26": - version "1.0.28" - resolved "https://registry.yarnpkg.com/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.28.tgz#cd239a16f95c0ed7c6d74315da4e38f2e93bbf19" - integrity sha512-1NRf1CUBjnr3K7hu8BLxjQrKCxEe8FP/xmPTenAxCRZWVLbmGotkFvG9mfNpjA6k7Bw1bw4BilZq9cu19RA5pg== +"@csstools/css-syntax-patches-for-csstree@^1.1.1": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz#3204cf40deb97db83e225b0baa9e37d9c3bd344d" + integrity sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg== "@csstools/css-tokenizer@^3.0.3": version "3.0.4" @@ -1522,121 +1522,120 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== -"@jest/console@30.2.0": - version "30.2.0" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-30.2.0.tgz#c52fcd5b58fdd2e8eb66b2fd8ae56f2f64d05b28" - integrity sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ== +"@jest/console@30.3.0": + version "30.3.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-30.3.0.tgz#42ccc3f995d400a8fe35b8850cfe10a8d4804cdf" + integrity sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww== dependencies: - "@jest/types" "30.2.0" + "@jest/types" "30.3.0" "@types/node" "*" chalk "^4.1.2" - jest-message-util "30.2.0" - jest-util "30.2.0" + jest-message-util "30.3.0" + jest-util "30.3.0" slash "^3.0.0" -"@jest/core@30.2.0": - version "30.2.0" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-30.2.0.tgz#813d59faa5abd5510964a8b3a7b17cc77b775275" - integrity sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ== +"@jest/core@30.3.0": + version "30.3.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-30.3.0.tgz#d06bb8456f35350f6494fd2405bcec4abb97b994" + integrity sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw== dependencies: - "@jest/console" "30.2.0" + "@jest/console" "30.3.0" "@jest/pattern" "30.0.1" - "@jest/reporters" "30.2.0" - "@jest/test-result" "30.2.0" - "@jest/transform" "30.2.0" - "@jest/types" "30.2.0" + "@jest/reporters" "30.3.0" + "@jest/test-result" "30.3.0" + "@jest/transform" "30.3.0" + "@jest/types" "30.3.0" "@types/node" "*" ansi-escapes "^4.3.2" chalk "^4.1.2" ci-info "^4.2.0" exit-x "^0.2.2" graceful-fs "^4.2.11" - jest-changed-files "30.2.0" - jest-config "30.2.0" - jest-haste-map "30.2.0" - jest-message-util "30.2.0" + jest-changed-files "30.3.0" + jest-config "30.3.0" + jest-haste-map "30.3.0" + jest-message-util "30.3.0" jest-regex-util "30.0.1" - jest-resolve "30.2.0" - jest-resolve-dependencies "30.2.0" - jest-runner "30.2.0" - jest-runtime "30.2.0" - jest-snapshot "30.2.0" - jest-util "30.2.0" - jest-validate "30.2.0" - jest-watcher "30.2.0" - micromatch "^4.0.8" - pretty-format "30.2.0" + jest-resolve "30.3.0" + jest-resolve-dependencies "30.3.0" + jest-runner "30.3.0" + jest-runtime "30.3.0" + jest-snapshot "30.3.0" + jest-util "30.3.0" + jest-validate "30.3.0" + jest-watcher "30.3.0" + pretty-format "30.3.0" slash "^3.0.0" -"@jest/diff-sequences@30.0.1": - version "30.0.1" - resolved "https://registry.yarnpkg.com/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz#0ededeae4d071f5c8ffe3678d15f3a1be09156be" - integrity sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw== +"@jest/diff-sequences@30.3.0": + version "30.3.0" + resolved "https://registry.yarnpkg.com/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz#25b0818d3d83f00b9c7b04e069b8810f9014b143" + integrity sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA== -"@jest/environment-jsdom-abstract@30.2.0": - version "30.2.0" - resolved "https://registry.yarnpkg.com/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.2.0.tgz#1313f9b3b509c31298c241203161b36622865181" - integrity sha512-kazxw2L9IPuZpQ0mEt9lu9Z98SqR74xcagANmMBU16X0lS23yPc0+S6hGLUz8kVRlomZEs/5S/Zlpqwf5yu6OQ== +"@jest/environment-jsdom-abstract@30.3.0": + version "30.3.0" + resolved "https://registry.yarnpkg.com/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.3.0.tgz#c97b2bf3ec35336d543c174a758f3dc07478c172" + integrity sha512-0hNFs5N6We3DMCwobzI0ydhkY10sT1tZSC0AAiy+0g2Dt/qEWgrcV5BrMxPczhe41cxW4qm6X+jqZaUdpZIajA== dependencies: - "@jest/environment" "30.2.0" - "@jest/fake-timers" "30.2.0" - "@jest/types" "30.2.0" + "@jest/environment" "30.3.0" + "@jest/fake-timers" "30.3.0" + "@jest/types" "30.3.0" "@types/jsdom" "^21.1.7" "@types/node" "*" - jest-mock "30.2.0" - jest-util "30.2.0" + jest-mock "30.3.0" + jest-util "30.3.0" -"@jest/environment@30.2.0": - version "30.2.0" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-30.2.0.tgz#1e673cdb8b93ded707cf6631b8353011460831fa" - integrity sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g== +"@jest/environment@30.3.0": + version "30.3.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-30.3.0.tgz#b0657c2944b6ef3352f7b25903cc3a23e6ab70f6" + integrity sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw== dependencies: - "@jest/fake-timers" "30.2.0" - "@jest/types" "30.2.0" + "@jest/fake-timers" "30.3.0" + "@jest/types" "30.3.0" "@types/node" "*" - jest-mock "30.2.0" + jest-mock "30.3.0" -"@jest/expect-utils@30.2.0": - version "30.2.0" - resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-30.2.0.tgz#4f95413d4748454fdb17404bf1141827d15e6011" - integrity sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA== +"@jest/expect-utils@30.3.0": + version "30.3.0" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-30.3.0.tgz#c45b2da9802ffed33bf43b3e019ddb95e5ad95e8" + integrity sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA== dependencies: "@jest/get-type" "30.1.0" -"@jest/expect@30.2.0": - version "30.2.0" - resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-30.2.0.tgz#9a5968499bb8add2bbb09136f69f7df5ddbf3185" - integrity sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA== +"@jest/expect@30.3.0": + version "30.3.0" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-30.3.0.tgz#08ee7f5b610167b0068743246c0b568f4c40c773" + integrity sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg== dependencies: - expect "30.2.0" - jest-snapshot "30.2.0" + expect "30.3.0" + jest-snapshot "30.3.0" -"@jest/fake-timers@30.2.0": - version "30.2.0" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-30.2.0.tgz#0941ddc28a339b9819542495b5408622dc9e94ec" - integrity sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw== +"@jest/fake-timers@30.3.0": + version "30.3.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-30.3.0.tgz#2b2868130c1d28233a79566874c42cae1c5a70bc" + integrity sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ== dependencies: - "@jest/types" "30.2.0" - "@sinonjs/fake-timers" "^13.0.0" + "@jest/types" "30.3.0" + "@sinonjs/fake-timers" "^15.0.0" "@types/node" "*" - jest-message-util "30.2.0" - jest-mock "30.2.0" - jest-util "30.2.0" + jest-message-util "30.3.0" + jest-mock "30.3.0" + jest-util "30.3.0" "@jest/get-type@30.1.0": version "30.1.0" resolved "https://registry.yarnpkg.com/@jest/get-type/-/get-type-30.1.0.tgz#4fcb4dc2ebcf0811be1c04fd1cb79c2dba431cbc" integrity sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA== -"@jest/globals@30.2.0": - version "30.2.0" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-30.2.0.tgz#2f4b696d5862664b89c4ee2e49ae24d2bb7e0988" - integrity sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw== +"@jest/globals@30.3.0": + version "30.3.0" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-30.3.0.tgz#40f4c90e5602629ecda1ca773a8fb21575bb64ea" + integrity sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA== dependencies: - "@jest/environment" "30.2.0" - "@jest/expect" "30.2.0" - "@jest/types" "30.2.0" - jest-mock "30.2.0" + "@jest/environment" "30.3.0" + "@jest/expect" "30.3.0" + "@jest/types" "30.3.0" + jest-mock "30.3.0" "@jest/pattern@30.0.1": version "30.0.1" @@ -1646,31 +1645,31 @@ "@types/node" "*" jest-regex-util "30.0.1" -"@jest/reporters@30.2.0": - version "30.2.0" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-30.2.0.tgz#a36b28fcbaf0c4595250b108e6f20e363348fd91" - integrity sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ== +"@jest/reporters@30.3.0": + version "30.3.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-30.3.0.tgz#0c1065f6c892665e5a051df22b19df4466ed816b" + integrity sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw== dependencies: "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "30.2.0" - "@jest/test-result" "30.2.0" - "@jest/transform" "30.2.0" - "@jest/types" "30.2.0" + "@jest/console" "30.3.0" + "@jest/test-result" "30.3.0" + "@jest/transform" "30.3.0" + "@jest/types" "30.3.0" "@jridgewell/trace-mapping" "^0.3.25" "@types/node" "*" chalk "^4.1.2" collect-v8-coverage "^1.0.2" exit-x "^0.2.2" - glob "^10.3.10" + glob "^10.5.0" graceful-fs "^4.2.11" istanbul-lib-coverage "^3.0.0" istanbul-lib-instrument "^6.0.0" istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^5.0.0" istanbul-reports "^3.1.3" - jest-message-util "30.2.0" - jest-util "30.2.0" - jest-worker "30.2.0" + jest-message-util "30.3.0" + jest-util "30.3.0" + jest-worker "30.3.0" slash "^3.0.0" string-length "^4.0.2" v8-to-istanbul "^9.0.1" @@ -1682,12 +1681,12 @@ dependencies: "@sinclair/typebox" "^0.34.0" -"@jest/snapshot-utils@30.2.0": - version "30.2.0" - resolved "https://registry.yarnpkg.com/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz#387858eb90c2f98f67bff327435a532ac5309fbe" - integrity sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug== +"@jest/snapshot-utils@30.3.0": + version "30.3.0" + resolved "https://registry.yarnpkg.com/@jest/snapshot-utils/-/snapshot-utils-30.3.0.tgz#ca003c91a3e1e4e4956dee716a2aaf04b6707f31" + integrity sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g== dependencies: - "@jest/types" "30.2.0" + "@jest/types" "30.3.0" chalk "^4.1.2" graceful-fs "^4.2.11" natural-compare "^1.4.0" @@ -1701,51 +1700,50 @@ callsites "^3.1.0" graceful-fs "^4.2.11" -"@jest/test-result@30.2.0": - version "30.2.0" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-30.2.0.tgz#9c0124377fb7996cdffb86eda3dbc56eacab363d" - integrity sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg== +"@jest/test-result@30.3.0": + version "30.3.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-30.3.0.tgz#cd8882d683d467fcffb98c09501a65687a76aae9" + integrity sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ== dependencies: - "@jest/console" "30.2.0" - "@jest/types" "30.2.0" + "@jest/console" "30.3.0" + "@jest/types" "30.3.0" "@types/istanbul-lib-coverage" "^2.0.6" collect-v8-coverage "^1.0.2" -"@jest/test-sequencer@30.2.0": - version "30.2.0" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz#bf0066bc72e176d58f5dfa7f212b6e7eee44f221" - integrity sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q== +"@jest/test-sequencer@30.3.0": + version "30.3.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-30.3.0.tgz#27002b2093f4e0d9e0e1ebb0bc274a242fdadc14" + integrity sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA== dependencies: - "@jest/test-result" "30.2.0" + "@jest/test-result" "30.3.0" graceful-fs "^4.2.11" - jest-haste-map "30.2.0" + jest-haste-map "30.3.0" slash "^3.0.0" -"@jest/transform@30.2.0": - version "30.2.0" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-30.2.0.tgz#54bef1a4510dcbd58d5d4de4fe2980a63077ef2a" - integrity sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA== +"@jest/transform@30.3.0": + version "30.3.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-30.3.0.tgz#9e6f78ffa205449bf956e269fd707c160f47ce2f" + integrity sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A== dependencies: "@babel/core" "^7.27.4" - "@jest/types" "30.2.0" + "@jest/types" "30.3.0" "@jridgewell/trace-mapping" "^0.3.25" babel-plugin-istanbul "^7.0.1" chalk "^4.1.2" convert-source-map "^2.0.0" fast-json-stable-stringify "^2.1.0" graceful-fs "^4.2.11" - jest-haste-map "30.2.0" + jest-haste-map "30.3.0" jest-regex-util "30.0.1" - jest-util "30.2.0" - micromatch "^4.0.8" + jest-util "30.3.0" pirates "^4.0.7" slash "^3.0.0" write-file-atomic "^5.0.1" -"@jest/types@30.2.0": - version "30.2.0" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-30.2.0.tgz#1c678a7924b8f59eafd4c77d56b6d0ba976d62b8" - integrity sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg== +"@jest/types@30.3.0": + version "30.3.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-30.3.0.tgz#cada800d323cb74945c24ac74615fdb312a6c85f" + integrity sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw== dependencies: "@jest/pattern" "30.0.1" "@jest/schemas" "30.0.5" @@ -2102,10 +2100,10 @@ dependencies: type-detect "4.0.8" -"@sinonjs/fake-timers@^13.0.0": - version "13.0.5" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz#36b9dbc21ad5546486ea9173d6bea063eb1717d5" - integrity sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw== +"@sinonjs/fake-timers@^15.0.0": + version "15.3.2" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-15.3.2.tgz#afecc36681e26aab9e0fe809fd9ad578096a3058" + integrity sha512-mrn35Jl2pCpns+mE3HaZa1yPN5EYCRgiMI+135COjr2hr8Cls9DXqIZ57vZe2cz7y2XVSq92tcs6kGQcT1J8Rw== dependencies: "@sinonjs/commons" "^3.0.1" @@ -2366,10 +2364,10 @@ dependencies: "@types/unist" "*" -"@types/node@*", "@types/node@>=13.7.0", "@types/node@^22.19.15": - version "22.19.15" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.19.15.tgz#6091d99fdf7c08cb57dc8b1345d407ba9a1df576" - integrity sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg== +"@types/node@*", "@types/node@>=13.7.0", "@types/node@^22.19.17": + version "22.19.17" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.19.17.tgz#09c71fb34ba2510f8ac865361b1fcb9552b8a581" + integrity sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q== dependencies: undici-types "~6.21.0" @@ -2484,100 +2482,100 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@8.58.0", "@typescript-eslint/eslint-plugin@^8.58.0": - version "8.58.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz#ad40e492f1931f46da1bd888e52b9e56df9063aa" - integrity sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg== +"@typescript-eslint/eslint-plugin@8.58.1", "@typescript-eslint/eslint-plugin@^8.58.1": + version "8.58.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz#cb53038b83d165ca0ef96d67d875efbd56c50fa8" + integrity sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ== dependencies: "@eslint-community/regexpp" "^4.12.2" - "@typescript-eslint/scope-manager" "8.58.0" - "@typescript-eslint/type-utils" "8.58.0" - "@typescript-eslint/utils" "8.58.0" - "@typescript-eslint/visitor-keys" "8.58.0" + "@typescript-eslint/scope-manager" "8.58.1" + "@typescript-eslint/type-utils" "8.58.1" + "@typescript-eslint/utils" "8.58.1" + "@typescript-eslint/visitor-keys" "8.58.1" ignore "^7.0.5" natural-compare "^1.4.0" ts-api-utils "^2.5.0" -"@typescript-eslint/parser@8.58.0", "@typescript-eslint/parser@^8.58.0": - version "8.58.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.58.0.tgz#da04ece1967b6c2fe8f10c3473dabf3825795ef7" - integrity sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA== +"@typescript-eslint/parser@8.58.1", "@typescript-eslint/parser@^8.58.1": + version "8.58.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.58.1.tgz#0943eca522ac408bcdd649882c3d95b10ff00f62" + integrity sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw== dependencies: - "@typescript-eslint/scope-manager" "8.58.0" - "@typescript-eslint/types" "8.58.0" - "@typescript-eslint/typescript-estree" "8.58.0" - "@typescript-eslint/visitor-keys" "8.58.0" + "@typescript-eslint/scope-manager" "8.58.1" + "@typescript-eslint/types" "8.58.1" + "@typescript-eslint/typescript-estree" "8.58.1" + "@typescript-eslint/visitor-keys" "8.58.1" debug "^4.4.3" -"@typescript-eslint/project-service@8.58.0": - version "8.58.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.58.0.tgz#66ceda0aabf7427aec3e2713fa43eb278dead2aa" - integrity sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg== +"@typescript-eslint/project-service@8.58.1": + version "8.58.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.58.1.tgz#c78781b1ca1ec1e7bc6522efba89318c6d249feb" + integrity sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g== dependencies: - "@typescript-eslint/tsconfig-utils" "^8.58.0" - "@typescript-eslint/types" "^8.58.0" + "@typescript-eslint/tsconfig-utils" "^8.58.1" + "@typescript-eslint/types" "^8.58.1" debug "^4.4.3" -"@typescript-eslint/scope-manager@8.58.0", "@typescript-eslint/scope-manager@^8.56.0": - version "8.58.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz#e304142775e49a1b7ac3c8bf2536714447c72cab" - integrity sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ== +"@typescript-eslint/scope-manager@8.58.1", "@typescript-eslint/scope-manager@^8.56.0": + version "8.58.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.58.1.tgz#35168f561bab4e3fd10dd6b03e8b83c157479211" + integrity sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w== dependencies: - "@typescript-eslint/types" "8.58.0" - "@typescript-eslint/visitor-keys" "8.58.0" + "@typescript-eslint/types" "8.58.1" + "@typescript-eslint/visitor-keys" "8.58.1" -"@typescript-eslint/tsconfig-utils@8.58.0", "@typescript-eslint/tsconfig-utils@^8.58.0": - version "8.58.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz#c5a8edb21f31e0fdee565724e1b984171c559482" - integrity sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A== +"@typescript-eslint/tsconfig-utils@8.58.1", "@typescript-eslint/tsconfig-utils@^8.58.1": + version "8.58.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.1.tgz#eb16792c579300c7bfb3c74b0f5e1dfbb0a2454d" + integrity sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw== -"@typescript-eslint/type-utils@8.58.0": - version "8.58.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.58.0.tgz#ce0e72cd967ffbbe8de322db6089bd4374be352f" - integrity sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg== +"@typescript-eslint/type-utils@8.58.1": + version "8.58.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.58.1.tgz#b21085a233087bde94c92ba6f5b4dfb77ca56730" + integrity sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w== dependencies: - "@typescript-eslint/types" "8.58.0" - "@typescript-eslint/typescript-estree" "8.58.0" - "@typescript-eslint/utils" "8.58.0" + "@typescript-eslint/types" "8.58.1" + "@typescript-eslint/typescript-estree" "8.58.1" + "@typescript-eslint/utils" "8.58.1" debug "^4.4.3" ts-api-utils "^2.5.0" -"@typescript-eslint/types@8.58.0", "@typescript-eslint/types@^8.58.0": - version "8.58.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.58.0.tgz#e94ae7abdc1c6530e71183c1007b61fa93112a5a" - integrity sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww== +"@typescript-eslint/types@8.58.1", "@typescript-eslint/types@^8.58.1": + version "8.58.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.58.1.tgz#9dfb4723fcd2b13737d8b03d941354cf73190313" + integrity sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw== -"@typescript-eslint/typescript-estree@8.58.0": - version "8.58.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz#ed233faa8e2f2a2e1357c3e7d553d6465a0ee59a" - integrity sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA== +"@typescript-eslint/typescript-estree@8.58.1": + version "8.58.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.1.tgz#8230cc9628d2cffef101e298c62807c4b9bf2fe9" + integrity sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg== dependencies: - "@typescript-eslint/project-service" "8.58.0" - "@typescript-eslint/tsconfig-utils" "8.58.0" - "@typescript-eslint/types" "8.58.0" - "@typescript-eslint/visitor-keys" "8.58.0" + "@typescript-eslint/project-service" "8.58.1" + "@typescript-eslint/tsconfig-utils" "8.58.1" + "@typescript-eslint/types" "8.58.1" + "@typescript-eslint/visitor-keys" "8.58.1" debug "^4.4.3" minimatch "^10.2.2" semver "^7.7.3" tinyglobby "^0.2.15" ts-api-utils "^2.5.0" -"@typescript-eslint/utils@8.58.0", "@typescript-eslint/utils@^8.0.0", "@typescript-eslint/utils@^8.56.0": - version "8.58.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.58.0.tgz#21a74a7963b0d288b719a4121c7dd555adaab3c3" - integrity sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA== +"@typescript-eslint/utils@8.58.1", "@typescript-eslint/utils@^8.0.0", "@typescript-eslint/utils@^8.56.0": + version "8.58.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.58.1.tgz#099a327b04ed921e6ee3988cde9ef34bc4b5435a" + integrity sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ== dependencies: "@eslint-community/eslint-utils" "^4.9.1" - "@typescript-eslint/scope-manager" "8.58.0" - "@typescript-eslint/types" "8.58.0" - "@typescript-eslint/typescript-estree" "8.58.0" + "@typescript-eslint/scope-manager" "8.58.1" + "@typescript-eslint/types" "8.58.1" + "@typescript-eslint/typescript-estree" "8.58.1" -"@typescript-eslint/visitor-keys@8.58.0": - version "8.58.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz#2abd55a4be70fd55967aceaba4330b9ba9f45189" - integrity sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ== +"@typescript-eslint/visitor-keys@8.58.1": + version "8.58.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.1.tgz#7c197533177f1ba9b8249f55f7f685e32bb6f204" + integrity sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ== dependencies: - "@typescript-eslint/types" "8.58.0" + "@typescript-eslint/types" "8.58.1" eslint-visitor-keys "^5.0.0" "@ungap/structured-clone@^1.3.0": @@ -3038,15 +3036,15 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" -babel-jest@30.2.0, babel-jest@^30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-30.2.0.tgz#fd44a1ec9552be35ead881f7381faa7d8f3b95ac" - integrity sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw== +babel-jest@30.3.0, babel-jest@^30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-30.3.0.tgz#3ff5553fa3bcbb8738d2d7335a4dbdc3bd1a0eb5" + integrity sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ== dependencies: - "@jest/transform" "30.2.0" + "@jest/transform" "30.3.0" "@types/babel__core" "^7.20.5" babel-plugin-istanbul "^7.0.1" - babel-preset-jest "30.2.0" + babel-preset-jest "30.3.0" chalk "^4.1.2" graceful-fs "^4.2.11" slash "^3.0.0" @@ -3062,17 +3060,17 @@ babel-plugin-istanbul@^7.0.1: istanbul-lib-instrument "^6.0.2" test-exclude "^6.0.0" -babel-plugin-jest-hoist@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz#94c250d36b43f95900f3a219241e0f4648191ce2" - integrity sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA== +babel-plugin-jest-hoist@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.3.0.tgz#235ad714a45c18b12566becf439e1c604e277015" + integrity sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg== dependencies: "@types/babel__core" "^7.20.5" -babel-plugin-module-resolver@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/babel-plugin-module-resolver/-/babel-plugin-module-resolver-5.0.2.tgz#cdeac5d4aaa3b08dd1ac23ddbf516660ed2d293e" - integrity sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg== +babel-plugin-module-resolver@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/babel-plugin-module-resolver/-/babel-plugin-module-resolver-5.0.3.tgz#13f03cf29048ad7e0239e6a1c4dcc669d199f394" + integrity sha512-h8h6H71ZvdLJZxZrYkaeR30BojTaV7O9GfqacY14SNj5CNB8ocL9tydNzTC0JrnNN7vY3eJhwCmkDj7tuEUaqQ== dependencies: find-babel-config "^2.1.1" glob "^9.3.3" @@ -3125,12 +3123,12 @@ babel-preset-current-node-syntax@^1.2.0: "@babel/plugin-syntax-private-property-in-object" "^7.14.5" "@babel/plugin-syntax-top-level-await" "^7.14.5" -babel-preset-jest@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz#04717843e561347781d6d7f69c81e6bcc3ed11ce" - integrity sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ== +babel-preset-jest@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-30.3.0.tgz#21cf3d19a6f5e9924426c879ee0b7f092636d043" + integrity sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ== dependencies: - babel-plugin-jest-hoist "30.2.0" + babel-plugin-jest-hoist "30.3.0" babel-preset-current-node-syntax "^1.2.0" bail@^2.0.0: @@ -3143,11 +3141,6 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -balanced-match@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-3.0.1.tgz#e854b098724b15076384266497392a271f4a26a0" - integrity sha512-vjtV3hiLqYDNRoiAv0zC4QaGAMPomEoq83PRmYIofPswwZurCeWR5LByXm7SyoL0Zh5+2z0+HC7jG8gSZJUh0w== - balanced-match@^4.0.2: version "4.0.4" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a" @@ -3158,10 +3151,10 @@ base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -baseline-browser-mapping@^2.9.0: - version "2.9.10" - resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.10.tgz#099221e89b30ec784675af076fbd4a93e58b53c3" - integrity sha512-2VIKvDx8Z1a9rTB2eCkdPE5nSe28XnA+qivGnWHoB40hMMt/h1hSz0960Zqsn6ZyxWXUie0EBdElKv8may20AA== +baseline-browser-mapping@^2.10.12: + version "2.10.18" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.18.tgz#565745085ba7743af7d4072707ad132db3a5a42f" + integrity sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A== basic-auth@^2.0.1, basic-auth@~2.0.1: version "2.0.1" @@ -3253,16 +3246,16 @@ browserslist-to-esbuild@^2.1.1: dependencies: meow "^13.0.0" -browserslist@^4.24.0, browserslist@^4.28.1: - version "4.28.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.1.tgz#7f534594628c53c63101079e27e40de490456a95" - integrity sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA== +browserslist@^4.24.0, browserslist@^4.28.1, browserslist@^4.28.2: + version "4.28.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.2.tgz#f50b65362ef48974ca9f50b3680566d786b811d2" + integrity sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg== dependencies: - baseline-browser-mapping "^2.9.0" - caniuse-lite "^1.0.30001759" - electron-to-chromium "^1.5.263" - node-releases "^2.0.27" - update-browserslist-db "^1.2.0" + baseline-browser-mapping "^2.10.12" + caniuse-lite "^1.0.30001782" + electron-to-chromium "^1.5.328" + node-releases "^2.0.36" + update-browserslist-db "^1.2.3" bser@2.1.1: version "2.1.1" @@ -3436,10 +3429,10 @@ camelcase@^8.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-8.0.0.tgz#c0d36d418753fb6ad9c5e0437579745c1c14a534" integrity sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA== -caniuse-lite@^1.0.30001759, caniuse-lite@^1.0.30001770: - version "1.0.30001770" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz#4dc47d3b263a50fbb243448034921e0a88591a84" - integrity sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw== +caniuse-lite@^1.0.30001780, caniuse-lite@^1.0.30001782: + version "1.0.30001787" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz#fd25c5e42e2d35df5c75eddda00d15d9c0c68f81" + integrity sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg== ccount@^2.0.0: version "2.0.1" @@ -3791,10 +3784,10 @@ core-js-compat@^3.48.0: dependencies: browserslist "^4.28.1" -core-js@^3.48.0: - version "3.48.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.48.0.tgz#1f813220a47bbf0e667e3885c36cd6f0593bf14d" - integrity sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ== +core-js@^3.49.0: + version "3.49.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.49.0.tgz#8b4d520ac034311fa21aa616f017ada0e0dbbddd" + integrity sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg== core-util-is@~1.0.0: version "1.0.3" @@ -3812,10 +3805,10 @@ cosmiconfig@^7.0.0: path-type "^4.0.0" yaml "^1.10.0" -cosmiconfig@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-9.0.0.tgz#34c3fc58287b915f3ae905ab6dc3de258b55ad9d" - integrity sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg== +cosmiconfig@^9.0.0, cosmiconfig@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-9.0.1.tgz#df110631a8547b5d1a98915271986f06e3011379" + integrity sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ== dependencies: env-paths "^2.2.1" import-fresh "^3.3.0" @@ -3861,18 +3854,18 @@ crypto-random-string@^4.0.0: dependencies: type-fest "^1.0.1" -css-functions-list@^3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.2.3.tgz#95652b0c24f0f59b291a9fc386041a19d4f40dbe" - integrity sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA== +css-functions-list@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.3.3.tgz#c4ab5008659de2e3baf3752c8fdef7662f3ffe23" + integrity sha512-8HFEBPKhOpJPEPu70wJJetjKta86Gw9+CCyCnB3sui2qQfOvRyqBy4IKLKKAwdMpWb2lHXWk9Wb4Z6AmaUT1Pg== -css-tree@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-3.1.0.tgz#7aabc035f4e66b5c86f54570d55e05b1346eb0fd" - integrity sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w== +css-tree@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-3.2.1.tgz#86cac7011561272b30e6b1e042ba6ce047aa7518" + integrity sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA== dependencies: - mdn-data "2.12.2" - source-map-js "^1.0.1" + mdn-data "2.27.1" + source-map-js "^1.2.1" css.escape@^1.5.1: version "1.5.1" @@ -4130,10 +4123,10 @@ devtools-license-check@^0.9.0: dependencies: license-checker "^9.0.3" -devtools-reps@^0.27.4: - version "0.27.4" - resolved "https://registry.yarnpkg.com/devtools-reps/-/devtools-reps-0.27.4.tgz#71cd2e595a1fd51164b18e2bbf15d6f83e747d8b" - integrity sha512-YQJy8Quz6H3BNcUYgmjPWYX736owTIU6MKE/k1WuArPtzkyWbP9EEQFgir48GSqc/w5QGuDDmBF4LhM9AS7mcg== +devtools-reps@^0.27.6: + version "0.27.6" + resolved "https://registry.yarnpkg.com/devtools-reps/-/devtools-reps-0.27.6.tgz#26aa682cfd058e171064bc86da6590e80785933f" + integrity sha512-ukQco/6e3nmTOk/qDnW7VuMga6XAlshJ/aBsCfq6ZTmaqJG2YybK7STV6oEiy2vi1U/YGQpg46mhPNx4fKytzw== dependencies: prop-types "^15.7.2" react "^16.8.6" @@ -4222,10 +4215,10 @@ ejs@^3.1.6: dependencies: jake "^10.8.5" -electron-to-chromium@^1.5.263: - version "1.5.267" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz#5d84f2df8cdb6bfe7e873706bb21bd4bfb574dc7" - integrity sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw== +electron-to-chromium@^1.5.328: + version "1.5.335" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.335.tgz#0b957cea44ef86795c227c616d16b4803d119daa" + integrity sha512-q9n5T4BR4Xwa2cwbrwcsDJtHD/enpQ5S1xF1IAtdqf5AAgqDFmR/aakqH3ChFdqd/QXJhS3rnnXFtexU7rax6Q== emittery@^0.13.1: version "0.13.1" @@ -4547,10 +4540,10 @@ eslint-plugin-jest-formatting@^3.1.0: resolved "https://registry.yarnpkg.com/eslint-plugin-jest-formatting/-/eslint-plugin-jest-formatting-3.1.0.tgz#b26dd5a40f432b642dcc880021a771bb1c93dcd2" integrity sha512-XyysraZ1JSgGbLSDxjj5HzKKh0glgWf+7CkqxbTqb7zEhW7X2WHo5SBQ8cGhnszKN+2Lj3/oevBlHNbHezoc/A== -eslint-plugin-jest@^29.15.0: - version "29.15.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-29.15.0.tgz#58a5917a88244f7536ae10c68b5bd58d407896f0" - integrity sha512-ZCGr7vTH2WSo2hrK5oM2RULFmMruQ7W3cX7YfwoTiPfzTGTFBMmrVIz45jZHd++cGKj/kWf02li/RhTGcANJSA== +eslint-plugin-jest@^29.15.1: + version "29.15.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-29.15.1.tgz#f663f9f7903a7181efddea5a92d1d31e66362596" + integrity sha512-6BjyErCQauz3zfJvzLw/kAez2lf4LEpbHLvWBfEcG4EI0ZiRSwjoH2uZulMouU8kRkBH+S0rhqn11IhTvxKgKw== dependencies: "@typescript-eslint/utils" "^8.0.0" @@ -4578,10 +4571,10 @@ eslint-plugin-react@^7.37.5: string.prototype.matchall "^4.0.12" string.prototype.repeat "^1.0.0" -eslint-plugin-testing-library@^7.16.0: - version "7.16.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-testing-library/-/eslint-plugin-testing-library-7.16.0.tgz#ac8dd5e4069e9264d997079b4cb5f45e79d2b8a7" - integrity sha512-lHZI6/Olb2oZqxd1+s1nOLCtL2PXKrc1ERz6oDbUKS0xZAMFH3Fy6wJo75z3pXTop3BV6+loPi2MSjIYt3vpAg== +eslint-plugin-testing-library@^7.16.2: + version "7.16.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-testing-library/-/eslint-plugin-testing-library-7.16.2.tgz#b59738085db6bb3b2fed0294c70c9098b45bbf90" + integrity sha512-8gleGnQXK2ZA3hHwjCwpYTZvM+9VsrJ+/9kDI8CjqAQGAdMQOdn/rJNu7ZySENuiWlGKQWyZJ4ZjEg2zamaRHw== dependencies: "@typescript-eslint/scope-manager" "^8.56.0" "@typescript-eslint/utils" "^8.56.0" @@ -4771,17 +4764,17 @@ exit-x@^0.2.2: resolved "https://registry.yarnpkg.com/exit-x/-/exit-x-0.2.2.tgz#1f9052de3b8d99a696b10dad5bced9bdd5c3aa64" integrity sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ== -expect@30.2.0, expect@^30.0.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/expect/-/expect-30.2.0.tgz#d4013bed267013c14bc1199cec8aa57cee9b5869" - integrity sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw== +expect@30.3.0, expect@^30.0.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-30.3.0.tgz#1b82111517d1ab030f3db0cf1b4061c8aa644f61" + integrity sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q== dependencies: - "@jest/expect-utils" "30.2.0" + "@jest/expect-utils" "30.3.0" "@jest/get-type" "30.1.0" - jest-matcher-utils "30.2.0" - jest-message-util "30.2.0" - jest-mock "30.2.0" - jest-util "30.2.0" + jest-matcher-utils "30.3.0" + jest-message-util "30.3.0" + jest-mock "30.3.0" + jest-util "30.3.0" extend@^3.0.0: version "3.0.2" @@ -5187,10 +5180,10 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@^10.3.10: - version "10.4.5" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" - integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== +glob@^10.5.0: + version "10.5.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c" + integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg== dependencies: foreground-child "^3.1.0" jackspeak "^3.1.2" @@ -5313,10 +5306,10 @@ globby@^11.0.3: merge2 "^1.4.1" slash "^3.0.0" -globby@^16.1.0: - version "16.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-16.1.0.tgz#71ab8199e4fc1c4c21a59bd14ec0f31c71d7d7d4" - integrity sha512-+A4Hq7m7Ze592k9gZRy4gJ27DrXRNnC1vPjxTt1qQxEY8RxagBkBxivkCwg7FxSTG0iLLEMaUx13oOr0R2/qcQ== +globby@^16.1.1: + version "16.2.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-16.2.0.tgz#6ab1351fbac1d9b9e47ed423814c2ad41af308ea" + integrity sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q== dependencies: "@sindresorhus/merge-streams" "^4.0.0" fast-glob "^3.3.3" @@ -6360,96 +6353,95 @@ jake@^10.8.5: filelist "^1.0.1" minimatch "^3.0.4" -jest-changed-files@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-30.2.0.tgz#602266e478ed554e1e1469944faa7efd37cee61c" - integrity sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ== +jest-changed-files@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-30.3.0.tgz#055849df695f9a9fcde0ae44024f815bbc627f3a" + integrity sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA== dependencies: execa "^5.1.1" - jest-util "30.2.0" + jest-util "30.3.0" p-limit "^3.1.0" -jest-circus@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-30.2.0.tgz#98b8198b958748a2f322354311023d1d02e7603f" - integrity sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg== +jest-circus@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-30.3.0.tgz#153614c11ab35867f371bd93496ecb9690b92077" + integrity sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA== dependencies: - "@jest/environment" "30.2.0" - "@jest/expect" "30.2.0" - "@jest/test-result" "30.2.0" - "@jest/types" "30.2.0" + "@jest/environment" "30.3.0" + "@jest/expect" "30.3.0" + "@jest/test-result" "30.3.0" + "@jest/types" "30.3.0" "@types/node" "*" chalk "^4.1.2" co "^4.6.0" dedent "^1.6.0" is-generator-fn "^2.1.0" - jest-each "30.2.0" - jest-matcher-utils "30.2.0" - jest-message-util "30.2.0" - jest-runtime "30.2.0" - jest-snapshot "30.2.0" - jest-util "30.2.0" + jest-each "30.3.0" + jest-matcher-utils "30.3.0" + jest-message-util "30.3.0" + jest-runtime "30.3.0" + jest-snapshot "30.3.0" + jest-util "30.3.0" p-limit "^3.1.0" - pretty-format "30.2.0" + pretty-format "30.3.0" pure-rand "^7.0.0" slash "^3.0.0" stack-utils "^2.0.6" -jest-cli@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-30.2.0.tgz#1780f8e9d66bf84a10b369aea60aeda7697dcc67" - integrity sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA== +jest-cli@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-30.3.0.tgz#5ed75a337f486a1f1c5acbb2de8acddb106ead6c" + integrity sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw== dependencies: - "@jest/core" "30.2.0" - "@jest/test-result" "30.2.0" - "@jest/types" "30.2.0" + "@jest/core" "30.3.0" + "@jest/test-result" "30.3.0" + "@jest/types" "30.3.0" chalk "^4.1.2" exit-x "^0.2.2" import-local "^3.2.0" - jest-config "30.2.0" - jest-util "30.2.0" - jest-validate "30.2.0" + jest-config "30.3.0" + jest-util "30.3.0" + jest-validate "30.3.0" yargs "^17.7.2" -jest-config@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-30.2.0.tgz#29df8c50e2ad801cc59c406b50176c18c362a90b" - integrity sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA== +jest-config@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-30.3.0.tgz#b969e0aaaf5964419e62953bb712c16d15972425" + integrity sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w== dependencies: "@babel/core" "^7.27.4" "@jest/get-type" "30.1.0" "@jest/pattern" "30.0.1" - "@jest/test-sequencer" "30.2.0" - "@jest/types" "30.2.0" - babel-jest "30.2.0" + "@jest/test-sequencer" "30.3.0" + "@jest/types" "30.3.0" + babel-jest "30.3.0" chalk "^4.1.2" ci-info "^4.2.0" deepmerge "^4.3.1" - glob "^10.3.10" + glob "^10.5.0" graceful-fs "^4.2.11" - jest-circus "30.2.0" + jest-circus "30.3.0" jest-docblock "30.2.0" - jest-environment-node "30.2.0" + jest-environment-node "30.3.0" jest-regex-util "30.0.1" - jest-resolve "30.2.0" - jest-runner "30.2.0" - jest-util "30.2.0" - jest-validate "30.2.0" - micromatch "^4.0.8" + jest-resolve "30.3.0" + jest-runner "30.3.0" + jest-util "30.3.0" + jest-validate "30.3.0" parse-json "^5.2.0" - pretty-format "30.2.0" + pretty-format "30.3.0" slash "^3.0.0" strip-json-comments "^3.1.1" -jest-diff@30.2.0, jest-diff@^30.0.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-30.2.0.tgz#e3ec3a6ea5c5747f605c9e874f83d756cba36825" - integrity sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A== +jest-diff@30.3.0, jest-diff@^30.0.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-30.3.0.tgz#e0a4c84ef350ffd790ffd5b0016acabeecf5f759" + integrity sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ== dependencies: - "@jest/diff-sequences" "30.0.1" + "@jest/diff-sequences" "30.3.0" "@jest/get-type" "30.1.0" chalk "^4.1.2" - pretty-format "30.2.0" + pretty-format "30.3.0" jest-docblock@30.2.0: version "30.2.0" @@ -6458,40 +6450,38 @@ jest-docblock@30.2.0: dependencies: detect-newline "^3.1.0" -jest-each@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-30.2.0.tgz#39e623ae71641c2ac3ee69b3ba3d258fce8e768d" - integrity sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ== +jest-each@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-30.3.0.tgz#faa7229bf7a9fa6426dc604057a7d2a173493b1e" + integrity sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA== dependencies: "@jest/get-type" "30.1.0" - "@jest/types" "30.2.0" + "@jest/types" "30.3.0" chalk "^4.1.2" - jest-util "30.2.0" - pretty-format "30.2.0" + jest-util "30.3.0" + pretty-format "30.3.0" -jest-environment-jsdom@^30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-30.2.0.tgz#e95e0921ed22be974f1d8a324766d12b1844cb2c" - integrity sha512-zbBTiqr2Vl78pKp/laGBREYzbZx9ZtqPjOK4++lL4BNDhxRnahg51HtoDrk9/VjIy9IthNEWdKVd7H5bqBhiWQ== +jest-environment-jsdom@^30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-30.3.0.tgz#6bf80519643333ae2faa07b5660d80451d328578" + integrity sha512-RLEOJy6ip1lpw0yqJ8tB3i88FC7VBz7i00Zvl2qF71IdxjS98gC9/0SPWYIBVXHm5hgCYK0PAlSlnHGGy9RoMg== dependencies: - "@jest/environment" "30.2.0" - "@jest/environment-jsdom-abstract" "30.2.0" - "@types/jsdom" "^21.1.7" - "@types/node" "*" + "@jest/environment" "30.3.0" + "@jest/environment-jsdom-abstract" "30.3.0" jsdom "^26.1.0" -jest-environment-node@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-30.2.0.tgz#3def7980ebd2fd86e74efd4d2e681f55ab38da0f" - integrity sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA== +jest-environment-node@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-30.3.0.tgz#aa8a57c5d0c4af0f8b1f7403ba737fec6b3aabbe" + integrity sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ== dependencies: - "@jest/environment" "30.2.0" - "@jest/fake-timers" "30.2.0" - "@jest/types" "30.2.0" + "@jest/environment" "30.3.0" + "@jest/fake-timers" "30.3.0" + "@jest/types" "30.3.0" "@types/node" "*" - jest-mock "30.2.0" - jest-util "30.2.0" - jest-validate "30.2.0" + jest-mock "30.3.0" + jest-util "30.3.0" + jest-validate "30.3.0" jest-extended@^7.0.0: version "7.0.0" @@ -6500,65 +6490,65 @@ jest-extended@^7.0.0: dependencies: jest-diff "^30.0.0" -jest-haste-map@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-30.2.0.tgz#808e3889f288603ac70ff0ac047598345a66022e" - integrity sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw== +jest-haste-map@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-30.3.0.tgz#1ea6843e6e45c077d91270666a4fcba958c24cd5" + integrity sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA== dependencies: - "@jest/types" "30.2.0" + "@jest/types" "30.3.0" "@types/node" "*" anymatch "^3.1.3" fb-watchman "^2.0.2" graceful-fs "^4.2.11" jest-regex-util "30.0.1" - jest-util "30.2.0" - jest-worker "30.2.0" - micromatch "^4.0.8" + jest-util "30.3.0" + jest-worker "30.3.0" + picomatch "^4.0.3" walker "^1.0.8" optionalDependencies: fsevents "^2.3.3" -jest-leak-detector@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz#292fdca7b7c9cf594e1e570ace140b01d8beb736" - integrity sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ== +jest-leak-detector@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-30.3.0.tgz#a695a851e353f517a554a2f5c91c2742fc131c98" + integrity sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ== dependencies: "@jest/get-type" "30.1.0" - pretty-format "30.2.0" + pretty-format "30.3.0" -jest-matcher-utils@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz#69a0d4c271066559ec8b0d8174829adc3f23a783" - integrity sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg== +jest-matcher-utils@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz#d6c739fec1ecd33809f2d2b1348f6ab01d2f2493" + integrity sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA== dependencies: "@jest/get-type" "30.1.0" chalk "^4.1.2" - jest-diff "30.2.0" - pretty-format "30.2.0" + jest-diff "30.3.0" + pretty-format "30.3.0" -jest-message-util@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-30.2.0.tgz#fc97bf90d11f118b31e6131e2b67fc4f39f92152" - integrity sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw== +jest-message-util@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-30.3.0.tgz#4d723544d36890ba862ac3961db52db5b0d1ba39" + integrity sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw== dependencies: "@babel/code-frame" "^7.27.1" - "@jest/types" "30.2.0" + "@jest/types" "30.3.0" "@types/stack-utils" "^2.0.3" chalk "^4.1.2" graceful-fs "^4.2.11" - micromatch "^4.0.8" - pretty-format "30.2.0" + picomatch "^4.0.3" + pretty-format "30.3.0" slash "^3.0.0" stack-utils "^2.0.6" -jest-mock@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-30.2.0.tgz#69f991614eeb4060189459d3584f710845bff45e" - integrity sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw== +jest-mock@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-30.3.0.tgz#e0fa4184a596a6c4fdec53d4f412158418923747" + integrity sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog== dependencies: - "@jest/types" "30.2.0" + "@jest/types" "30.3.0" "@types/node" "*" - jest-util "30.2.0" + jest-util "30.3.0" jest-pnp-resolver@^1.2.3: version "1.2.3" @@ -6570,169 +6560,169 @@ jest-regex-util@30.0.1: resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-30.0.1.tgz#f17c1de3958b67dfe485354f5a10093298f2a49b" integrity sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA== -jest-resolve-dependencies@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz#3370e2c0b49cc560f6a7e8ec3a59dd99525e1a55" - integrity sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w== +jest-resolve-dependencies@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-30.3.0.tgz#4d638c9f0d93a62a6ed25dec874bfd7e756c8ce5" + integrity sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw== dependencies: jest-regex-util "30.0.1" - jest-snapshot "30.2.0" + jest-snapshot "30.3.0" -jest-resolve@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-30.2.0.tgz#2e2009cbd61e8f1f003355d5ec87225412cebcd7" - integrity sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A== +jest-resolve@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-30.3.0.tgz#b7bee9927279805b1b50715d2170a545553b87ff" + integrity sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g== dependencies: chalk "^4.1.2" graceful-fs "^4.2.11" - jest-haste-map "30.2.0" + jest-haste-map "30.3.0" jest-pnp-resolver "^1.2.3" - jest-util "30.2.0" - jest-validate "30.2.0" + jest-util "30.3.0" + jest-validate "30.3.0" slash "^3.0.0" unrs-resolver "^1.7.11" -jest-runner@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-30.2.0.tgz#c62b4c3130afa661789705e13a07bdbcec26a114" - integrity sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ== - dependencies: - "@jest/console" "30.2.0" - "@jest/environment" "30.2.0" - "@jest/test-result" "30.2.0" - "@jest/transform" "30.2.0" - "@jest/types" "30.2.0" +jest-runner@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-30.3.0.tgz#fa970fc4e45d418ad7e7d581b24cac7af5944cb7" + integrity sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw== + dependencies: + "@jest/console" "30.3.0" + "@jest/environment" "30.3.0" + "@jest/test-result" "30.3.0" + "@jest/transform" "30.3.0" + "@jest/types" "30.3.0" "@types/node" "*" chalk "^4.1.2" emittery "^0.13.1" exit-x "^0.2.2" graceful-fs "^4.2.11" jest-docblock "30.2.0" - jest-environment-node "30.2.0" - jest-haste-map "30.2.0" - jest-leak-detector "30.2.0" - jest-message-util "30.2.0" - jest-resolve "30.2.0" - jest-runtime "30.2.0" - jest-util "30.2.0" - jest-watcher "30.2.0" - jest-worker "30.2.0" + jest-environment-node "30.3.0" + jest-haste-map "30.3.0" + jest-leak-detector "30.3.0" + jest-message-util "30.3.0" + jest-resolve "30.3.0" + jest-runtime "30.3.0" + jest-util "30.3.0" + jest-watcher "30.3.0" + jest-worker "30.3.0" p-limit "^3.1.0" source-map-support "0.5.13" -jest-runtime@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-30.2.0.tgz#395ea792cde048db1b0cd1a92dc9cb9f1921bf8a" - integrity sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg== +jest-runtime@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-30.3.0.tgz#1a9bec7a9b68db12dfe4136bbe41ab883ea2c996" + integrity sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng== dependencies: - "@jest/environment" "30.2.0" - "@jest/fake-timers" "30.2.0" - "@jest/globals" "30.2.0" + "@jest/environment" "30.3.0" + "@jest/fake-timers" "30.3.0" + "@jest/globals" "30.3.0" "@jest/source-map" "30.0.1" - "@jest/test-result" "30.2.0" - "@jest/transform" "30.2.0" - "@jest/types" "30.2.0" + "@jest/test-result" "30.3.0" + "@jest/transform" "30.3.0" + "@jest/types" "30.3.0" "@types/node" "*" chalk "^4.1.2" cjs-module-lexer "^2.1.0" collect-v8-coverage "^1.0.2" - glob "^10.3.10" + glob "^10.5.0" graceful-fs "^4.2.11" - jest-haste-map "30.2.0" - jest-message-util "30.2.0" - jest-mock "30.2.0" + jest-haste-map "30.3.0" + jest-message-util "30.3.0" + jest-mock "30.3.0" jest-regex-util "30.0.1" - jest-resolve "30.2.0" - jest-snapshot "30.2.0" - jest-util "30.2.0" + jest-resolve "30.3.0" + jest-snapshot "30.3.0" + jest-util "30.3.0" slash "^3.0.0" strip-bom "^4.0.0" -jest-snapshot@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-30.2.0.tgz#266fbbb4b95fc4665ce6f32f1f38eeb39f4e26d0" - integrity sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA== +jest-snapshot@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-30.3.0.tgz#6e7ea75069dda86e36311a0f73189e830d4f51ad" + integrity sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ== dependencies: "@babel/core" "^7.27.4" "@babel/generator" "^7.27.5" "@babel/plugin-syntax-jsx" "^7.27.1" "@babel/plugin-syntax-typescript" "^7.27.1" "@babel/types" "^7.27.3" - "@jest/expect-utils" "30.2.0" + "@jest/expect-utils" "30.3.0" "@jest/get-type" "30.1.0" - "@jest/snapshot-utils" "30.2.0" - "@jest/transform" "30.2.0" - "@jest/types" "30.2.0" + "@jest/snapshot-utils" "30.3.0" + "@jest/transform" "30.3.0" + "@jest/types" "30.3.0" babel-preset-current-node-syntax "^1.2.0" chalk "^4.1.2" - expect "30.2.0" + expect "30.3.0" graceful-fs "^4.2.11" - jest-diff "30.2.0" - jest-matcher-utils "30.2.0" - jest-message-util "30.2.0" - jest-util "30.2.0" - pretty-format "30.2.0" + jest-diff "30.3.0" + jest-matcher-utils "30.3.0" + jest-message-util "30.3.0" + jest-util "30.3.0" + pretty-format "30.3.0" semver "^7.7.2" synckit "^0.11.8" -jest-util@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-30.2.0.tgz#5142adbcad6f4e53c2776c067a4db3c14f913705" - integrity sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA== +jest-util@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-30.3.0.tgz#95a4fbacf2dac20e768e2f1744b70519f2ba7980" + integrity sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg== dependencies: - "@jest/types" "30.2.0" + "@jest/types" "30.3.0" "@types/node" "*" chalk "^4.1.2" ci-info "^4.2.0" graceful-fs "^4.2.11" - picomatch "^4.0.2" + picomatch "^4.0.3" -jest-validate@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-30.2.0.tgz#273eaaed4c0963b934b5b31e96289edda6e0a2ef" - integrity sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw== +jest-validate@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-30.3.0.tgz#215e11b8fcc5e2ca4b99ea5d730a5b4c969e4355" + integrity sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q== dependencies: "@jest/get-type" "30.1.0" - "@jest/types" "30.2.0" + "@jest/types" "30.3.0" camelcase "^6.3.0" chalk "^4.1.2" leven "^3.1.0" - pretty-format "30.2.0" + pretty-format "30.3.0" -jest-watcher@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-30.2.0.tgz#f9c055de48e18c979e7756a3917e596e2d69b07b" - integrity sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg== +jest-watcher@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-30.3.0.tgz#3afa1af355b9fe80f0261eb8a23981a315858596" + integrity sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w== dependencies: - "@jest/test-result" "30.2.0" - "@jest/types" "30.2.0" + "@jest/test-result" "30.3.0" + "@jest/types" "30.3.0" "@types/node" "*" ansi-escapes "^4.3.2" chalk "^4.1.2" emittery "^0.13.1" - jest-util "30.2.0" + jest-util "30.3.0" string-length "^4.0.2" -jest-worker@30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-30.2.0.tgz#fd5c2a36ff6058ec8f74366ec89538cc99539d26" - integrity sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g== +jest-worker@30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-30.3.0.tgz#ae4dc1f1d93d0cba1415624fcedaec40ea764f14" + integrity sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ== dependencies: "@types/node" "*" "@ungap/structured-clone" "^1.3.0" - jest-util "30.2.0" + jest-util "30.3.0" merge-stream "^2.0.0" supports-color "^8.1.1" -jest@^30.2.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/jest/-/jest-30.2.0.tgz#9f0a71e734af968f26952b5ae4b724af82681630" - integrity sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A== +jest@^30.3.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-30.3.0.tgz#6460b889dd805e9677400505f16f1d9b14c285a3" + integrity sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg== dependencies: - "@jest/core" "30.2.0" - "@jest/types" "30.2.0" + "@jest/core" "30.3.0" + "@jest/types" "30.3.0" import-local "^3.2.0" - jest-cli "30.2.0" + jest-cli "30.3.0" "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" @@ -6933,11 +6923,6 @@ kleur@^4.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.4.tgz#8c202987d7e577766d039a8cd461934c01cda04d" integrity sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA== -known-css-properties@^0.37.0: - version "0.37.0" - resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.37.0.tgz#10ebe49b9dbb6638860ff8a002fb65a053f4aec5" - integrity sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ== - koa-bodyparser@^4.4.1: version "4.4.1" resolved "https://registry.yarnpkg.com/koa-bodyparser/-/koa-bodyparser-4.4.1.tgz#a908d848e142cc57d9eece478e932bf00dce3029" @@ -7681,10 +7666,10 @@ mdast-util-to-string@^3.0.0, mdast-util-to-string@^3.1.0: resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz#56c506d065fbf769515235e577b5a261552d56e9" integrity sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA== -mdn-data@2.12.2: - version "2.12.2" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.12.2.tgz#9ae6c41a9e65adf61318b32bff7b64fbfb13f8cf" - integrity sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA== +mdn-data@2.27.1: + version "2.27.1" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.27.1.tgz#e37b9c50880b75366c4d40ac63d9bbcacdb61f0e" + integrity sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ== media-typer@0.3.0: version "0.3.0" @@ -7736,10 +7721,10 @@ meow@^13.0.0: resolved "https://registry.yarnpkg.com/meow/-/meow-13.2.0.tgz#6b7d63f913f984063b3cc261b6e8800c4cd3474f" integrity sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA== -meow@^14.0.0: - version "14.0.0" - resolved "https://registry.yarnpkg.com/meow/-/meow-14.0.0.tgz#97d44d2f9eeb9836db06108570891f7523103bbc" - integrity sha512-JhC3R1f6dbspVtmF3vKjAWz1EVIvwFrGGPLSdU6rK79xBwHWTuHoLnRX/t1/zHS1Ch1Y2UtIrih7DAHuH9JFJA== +meow@^14.1.0: + version "14.1.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-14.1.0.tgz#3cd2d16ad534829ab12fcb5010fc2fdb89facd31" + integrity sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw== meow@^7.1.0: version "7.1.1" @@ -8374,10 +8359,10 @@ node-int64@^0.4.0: resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= -node-releases@^2.0.27: - version "2.0.27" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.27.tgz#eedca519205cf20f650f61d56b070db111231e4e" - integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA== +node-releases@^2.0.36: + version "2.0.37" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.37.tgz#9bd4f10b77ba39c2b9402d4e8399c482a797f671" + integrity sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg== nopt@^2.2.0: version "2.2.1" @@ -9016,7 +9001,7 @@ postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.4.32, postcss@^8.5.6, postcss@^8.5.8: +postcss@^8.4.32, postcss@^8.5.8: version "8.5.8" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.8.tgz#6230ecc8fb02e7a0f6982e53990937857e13f399" integrity sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg== @@ -9050,10 +9035,10 @@ pretty-bytes@^5.3.0: resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== -pretty-format@30.2.0, pretty-format@^30.0.0: - version "30.2.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-30.2.0.tgz#2d44fe6134529aed18506f6d11509d8a62775ebe" - integrity sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA== +pretty-format@30.3.0, pretty-format@^30.0.0: + version "30.3.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-30.3.0.tgz#e977eed4bcd1b6195faed418af8eac68b9ea1f29" + integrity sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ== dependencies: "@jest/schemas" "30.0.5" ansi-styles "^5.2.0" @@ -9102,10 +9087,10 @@ proto-list@~1.2.1: resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== -protobufjs@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-8.0.0.tgz#d884102c1fe8d0b1e2493789ad37bc7ea47c0893" - integrity sha512-jx6+sE9h/UryaCZhsJWbJtTEy47yXoGNYI4z8ZaRncM0zBKeRqjO2JEcOUYwrYGb1WLhXM1FfMzW3annvFv0rw== +protobufjs@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-8.0.1.tgz#c1781abf9a73812cbd483b32138ac59948223806" + integrity sha512-NWWCCscLjs+cOKF/s/XVNFRW7Yih0fdH+9brffR5NZCy8k42yRdl5KlWKMVXuI1vfCoy4o1z80XR/W/QUb3V3w== dependencies: "@protobufjs/aspromise" "^1.1.2" "@protobufjs/base64" "^1.1.2" @@ -10107,7 +10092,7 @@ smob@^1.0.0: resolved "https://registry.yarnpkg.com/smob/-/smob-1.5.0.tgz#85d79a1403abf128d24d3ebc1cdc5e1a9548d3ab" integrity sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig== -source-map-js@^1.0.1, source-map-js@^1.2.1: +source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== @@ -10308,7 +10293,7 @@ string-width@^7.0.0, string-width@^7.2.0: get-east-asian-width "^1.0.0" strip-ansi "^7.1.0" -string-width@^8.1.1: +string-width@^8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-8.2.0.tgz#bdb6a9bd6d7800db635adae96cdb0443fec56c42" integrity sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw== @@ -10520,50 +10505,47 @@ stylelint-order@^6.0.2: postcss "^8.4.32" postcss-sorting "^8.0.2" -stylelint@^17.3.0: - version "17.3.0" - resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-17.3.0.tgz#18fa199862205516d7011524557fae4ff5b9d60f" - integrity sha512-1POV91lcEMhj6SLVaOeA0KlS9yattS+qq+cyWqP/nYzWco7K5jznpGH1ExngvPlTM9QF1Kjd2bmuzJu9TH2OcA== +stylelint@^17.6.0: + version "17.6.0" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-17.6.0.tgz#029731f853f407699ef49713b1a7b550cc458012" + integrity sha512-tokrsMIVAR9vAQ/q3UVEr7S0dGXCi7zkCezPRnS2kqPUulvUh5Vgfwngrk4EoAoW7wnrThqTdnTFN5Ra7CaxIg== dependencies: "@csstools/css-calc" "^3.1.1" "@csstools/css-parser-algorithms" "^4.0.0" - "@csstools/css-syntax-patches-for-csstree" "^1.0.26" + "@csstools/css-syntax-patches-for-csstree" "^1.1.1" "@csstools/css-tokenizer" "^4.0.0" "@csstools/media-query-list-parser" "^5.0.0" "@csstools/selector-resolve-nested" "^4.0.0" "@csstools/selector-specificity" "^6.0.0" - balanced-match "^3.0.1" colord "^2.9.3" - cosmiconfig "^9.0.0" - css-functions-list "^3.2.3" - css-tree "^3.1.0" + cosmiconfig "^9.0.1" + css-functions-list "^3.3.3" + css-tree "^3.2.1" debug "^4.4.3" fast-glob "^3.3.3" fastest-levenshtein "^1.0.16" file-entry-cache "^11.1.2" global-modules "^2.0.0" - globby "^16.1.0" + globby "^16.1.1" globjoin "^0.1.4" html-tags "^5.1.0" ignore "^7.0.5" import-meta-resolve "^4.2.0" - imurmurhash "^0.1.4" is-plain-object "^5.0.0" - known-css-properties "^0.37.0" mathml-tag-names "^4.0.0" - meow "^14.0.0" + meow "^14.1.0" micromatch "^4.0.8" normalize-path "^3.0.0" picocolors "^1.1.1" - postcss "^8.5.6" + postcss "^8.5.8" postcss-safe-parser "^7.0.1" postcss-selector-parser "^7.1.1" postcss-value-parser "^4.2.0" - string-width "^8.1.1" + string-width "^8.2.0" supports-hyperlinks "^4.4.0" svg-tags "^1.0.0" table "^6.9.0" - write-file-atomic "^7.0.0" + write-file-atomic "^7.0.1" supports-color@^0.2.0: version "0.2.0" @@ -10981,15 +10963,15 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typescript-eslint@^8.58.0: - version "8.58.0" - resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.58.0.tgz#5758b1b68ae7ec05d756b98c63a1f6953a01172b" - integrity sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA== +typescript-eslint@^8.58.1: + version "8.58.1" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.58.1.tgz#e765cbfea5774dcb4b1473e5e77a46254f309b32" + integrity sha512-gf6/oHChByg9HJvhMO1iBexJh12AqqTfnuxscMDOVqfJW3htsdRJI/GfPpHTTcyeB8cSTUY2JcZmVgoyPqcrDg== dependencies: - "@typescript-eslint/eslint-plugin" "8.58.0" - "@typescript-eslint/parser" "8.58.0" - "@typescript-eslint/typescript-estree" "8.58.0" - "@typescript-eslint/utils" "8.58.0" + "@typescript-eslint/eslint-plugin" "8.58.1" + "@typescript-eslint/parser" "8.58.1" + "@typescript-eslint/typescript-estree" "8.58.1" + "@typescript-eslint/utils" "8.58.1" typescript@^6.0.2: version "6.0.2" @@ -11256,7 +11238,7 @@ upath@^1.2.0: resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== -update-browserslist-db@^1.2.0: +update-browserslist-db@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d" integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w== @@ -11350,10 +11332,10 @@ v8-to-istanbul@^9.0.1: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" -valibot@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/valibot/-/valibot-1.2.0.tgz#8fc720d9e4082ba16e30a914064a39619b2f1d6f" - integrity sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg== +valibot@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/valibot/-/valibot-1.3.1.tgz#483c5e229b75c0cb441dfaa9392ffe0a1ec53f2f" + integrity sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg== validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.4: version "3.0.4" @@ -11851,12 +11833,11 @@ write-file-atomic@^5.0.1: imurmurhash "^0.1.4" signal-exit "^4.0.1" -write-file-atomic@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-7.0.0.tgz#f89def4f223e9bf8b06cc6fdb12bda3a917505c7" - integrity sha512-YnlPC6JqnZl6aO4uRc+dx5PHguiR9S6WeoLtpxNT9wIG+BDya7ZNE1q7KOjVgaA73hKhKLpVPgJ5QA9THQ5BRg== +write-file-atomic@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-7.0.1.tgz#0e2a450ab5aa306bcfcd3aed61833b10cc4fb885" + integrity sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg== dependencies: - imurmurhash "^0.1.4" signal-exit "^4.0.1" ws@^8.18.0: From 7f631a0af0a84fb3c1ff03f492713fe3244961b7 Mon Sep 17 00:00:00 2001 From: fatadel Date: Wed, 15 Apr 2026 12:18:45 +0200 Subject: [PATCH 30/64] Add CounterDisplayConfig to counters in the processed profile format (#5912) * Add CounterDisplayConfig to counters in the processed profile format Make counters self-describing in terms of rendering by adding `display` field of `CounterDisplayConfig` type. The value is derived from a counter's `category` and `name` fields. This data is sufficient to understand how a counter should be rendered allowing us to remove hardcoded logic for each counter. This is the first PR for issue #5752. * Migrate counter.color into display.color in the processed profile upgrader The existing optional `color` field on counters (used by external importers) is now consolidated into `display.color`. The upgrader uses the old `color` value as a fallback when deriving the display config, then removes the standalone field. Components now read color from `counter.display.color` directly. --- docs-developer/CHANGELOG-formats.md | 6 ++ src/app-logic/constants.ts | 5 +- .../timeline/TrackBandwidthGraph.tsx | 13 +--- src/components/timeline/TrackMemoryGraph.tsx | 11 +--- src/components/timeline/TrackPowerGraph.tsx | 11 +--- src/profile-logic/process-profile.ts | 57 +++++++++++++++++ .../processed-profile-versioning.ts | 61 +++++++++++++++++++ src/profile-logic/profile-data.ts | 3 +- src/test/components/TrackBandwidth.test.tsx | 37 +++++------ src/test/components/TrackMemory.test.tsx | 9 ++- .../fixtures/profiles/processed-profile.ts | 15 +++++ .../symbolicator-cli.test.ts.snap | 4 +- .../__snapshots__/profile-view.test.ts.snap | 2 +- .../profile-conversion.test.ts.snap | 36 +++++------ .../profile-upgrading.test.ts.snap | 16 +++-- src/types/profile-derived.ts | 4 +- src/types/profile.ts | 23 ++++++- 17 files changed, 233 insertions(+), 80 deletions(-) diff --git a/docs-developer/CHANGELOG-formats.md b/docs-developer/CHANGELOG-formats.md index b1be78b6bb..50da5c96d4 100644 --- a/docs-developer/CHANGELOG-formats.md +++ b/docs-developer/CHANGELOG-formats.md @@ -6,6 +6,12 @@ Note that this is not an exhaustive list. Processed profile format upgraders can ## Processed profile format +### Version 62 + +A new `display` field of type `CounterDisplayConfig` was added to `RawCounter`. +This metadata makes counters self-describing in terms of how they are rendered in the UI. +For existing profiles, the display config is derived from the counter's `category` and `name`. + ### Version 61 The `SourceTable` in `profile.shared.sources` was updated: diff --git a/src/app-logic/constants.ts b/src/app-logic/constants.ts index f70f544144..955a410194 100644 --- a/src/app-logic/constants.ts +++ b/src/app-logic/constants.ts @@ -12,7 +12,7 @@ export const GECKO_PROFILE_VERSION = 34; // The current version of the "processed" profile format. // Please don't forget to update the processed profile format changelog in // `docs-developer/CHANGELOG-formats.md`. -export const PROCESSED_PROFILE_VERSION = 61; +export const PROCESSED_PROFILE_VERSION = 62; // The following are the margin sizes for the left and right of the timeline. Independent // components need to share these values. @@ -35,12 +35,10 @@ export const TRACK_MEMORY_MARKERS_HEIGHT = 15; export const TRACK_MEMORY_HEIGHT = TRACK_MEMORY_GRAPH_HEIGHT + TRACK_MEMORY_MARKERS_HEIGHT; export const TRACK_MEMORY_LINE_WIDTH = 2; -export const TRACK_MEMORY_DEFAULT_COLOR = 'orange'; // The following values are for the bandwidth track. export const TRACK_BANDWIDTH_HEIGHT = 25; export const TRACK_BANDWIDTH_LINE_WIDTH = 2; -export const TRACK_BANDWIDTH_DEFAULT_COLOR = 'blue'; // The following values are for experimental event delay track. export const TRACK_EVENT_DELAY_HEIGHT = 40; @@ -64,7 +62,6 @@ export const TIMELINE_RULER_HEIGHT = 20; // Height of the power track. export const TRACK_POWER_HEIGHT = 25; export const TRACK_POWER_LINE_WIDTH = 2; -export const TRACK_POWER_DEFAULT_COLOR = 'grey'; // Height of the process cpu track. export const TRACK_PROCESS_CPU_HEIGHT = 25; diff --git a/src/components/timeline/TrackBandwidthGraph.tsx b/src/components/timeline/TrackBandwidthGraph.tsx index 6f05c80bd2..0110b3c9a3 100644 --- a/src/components/timeline/TrackBandwidthGraph.tsx +++ b/src/components/timeline/TrackBandwidthGraph.tsx @@ -31,7 +31,6 @@ import { TooltipDetailSeparator, } from 'firefox-profiler/components/tooltip/TooltipDetails'; import { EmptyThreadIndicator } from './EmptyThreadIndicator'; -import { TRACK_BANDWIDTH_DEFAULT_COLOR } from 'firefox-profiler/app-logic/constants'; import { getSampleIndexRangeForSelection } from 'firefox-profiler/profile-logic/profile-data'; import { co2 } from '@tgwf/co2'; @@ -140,12 +139,8 @@ class TrackBandwidthCanvas extends React.PureComponent { ctx.lineWidth = deviceLineWidth; ctx.lineJoin = 'bevel'; - ctx.strokeStyle = getStrokeColor( - counter.color || TRACK_BANDWIDTH_DEFAULT_COLOR - ); - ctx.fillStyle = getFillColor( - counter.color || TRACK_BANDWIDTH_DEFAULT_COLOR - ); + ctx.strokeStyle = getStrokeColor(counter.display.color); + ctx.fillStyle = getFillColor(counter.display.color); ctx.beginPath(); const getX = (i: number) => @@ -618,9 +613,7 @@ class TrackBandwidthGraphImpl extends React.PureComponent { style={{ left, top, - backgroundColor: getDotColor( - counter.color || TRACK_BANDWIDTH_DEFAULT_COLOR - ), + backgroundColor: getDotColor(counter.display.color), }} className="timelineTrackBandwidthGraphDot" /> diff --git a/src/components/timeline/TrackMemoryGraph.tsx b/src/components/timeline/TrackMemoryGraph.tsx index 8e1a0f74a6..e3d9f4772e 100644 --- a/src/components/timeline/TrackMemoryGraph.tsx +++ b/src/components/timeline/TrackMemoryGraph.tsx @@ -25,7 +25,6 @@ import { import { getThreadSelectors } from 'firefox-profiler/selectors/per-thread'; import { Tooltip } from 'firefox-profiler/components/tooltip/Tooltip'; import { EmptyThreadIndicator } from './EmptyThreadIndicator'; -import { TRACK_MEMORY_DEFAULT_COLOR } from 'firefox-profiler/app-logic/constants'; import type { CounterIndex, @@ -130,10 +129,8 @@ class TrackMemoryCanvas extends React.PureComponent { ctx.lineWidth = deviceLineWidth; ctx.lineJoin = 'bevel'; - ctx.strokeStyle = getStrokeColor( - counter.color || TRACK_MEMORY_DEFAULT_COLOR - ); - ctx.fillStyle = getFillColor(counter.color || TRACK_MEMORY_DEFAULT_COLOR); + ctx.strokeStyle = getStrokeColor(counter.display.color); + ctx.fillStyle = getFillColor(counter.display.color); ctx.beginPath(); // The x and y are used after the loop. @@ -460,9 +457,7 @@ class TrackMemoryGraphImpl extends React.PureComponent { style={{ left, top, - backgroundColor: getDotColor( - counter.color || TRACK_MEMORY_DEFAULT_COLOR - ), + backgroundColor: getDotColor(counter.display.color), }} className="timelineTrackMemoryGraphDot" /> diff --git a/src/components/timeline/TrackPowerGraph.tsx b/src/components/timeline/TrackPowerGraph.tsx index c8613636f5..fe654222dd 100644 --- a/src/components/timeline/TrackPowerGraph.tsx +++ b/src/components/timeline/TrackPowerGraph.tsx @@ -21,7 +21,6 @@ import { getThreadSelectors } from 'firefox-profiler/selectors/per-thread'; import { Tooltip } from 'firefox-profiler/components/tooltip/Tooltip'; import { TooltipTrackPower } from 'firefox-profiler/components/tooltip/TrackPower'; import { EmptyThreadIndicator } from './EmptyThreadIndicator'; -import { TRACK_POWER_DEFAULT_COLOR } from 'firefox-profiler/app-logic/constants'; import type { CounterIndex, @@ -122,10 +121,8 @@ class TrackPowerCanvas extends React.PureComponent { ctx.lineWidth = deviceLineWidth; ctx.lineJoin = 'bevel'; - ctx.strokeStyle = getStrokeColor( - counter.color || TRACK_POWER_DEFAULT_COLOR - ); - ctx.fillStyle = getFillColor(counter.color || TRACK_POWER_DEFAULT_COLOR); + ctx.strokeStyle = getStrokeColor(counter.display.color); + ctx.fillStyle = getFillColor(counter.display.color); ctx.beginPath(); const getX = (i: number) => @@ -483,9 +480,7 @@ class TrackPowerGraphImpl extends React.PureComponent { style={{ left, top, - backgroundColor: getDotColor( - counter.color || TRACK_POWER_DEFAULT_COLOR - ), + backgroundColor: getDotColor(counter.display.color), }} className="timelineTrackPowerGraphDot" /> diff --git a/src/profile-logic/process-profile.ts b/src/profile-logic/process-profile.ts index fbf4c99cb5..d95355d18d 100644 --- a/src/profile-logic/process-profile.ts +++ b/src/profile-logic/process-profile.ts @@ -97,6 +97,7 @@ import type { GeckoSourceTable, IndexIntoCategoryList, IndexIntoFrameTable, + CounterDisplayConfig, } from 'firefox-profiler/types'; import { decompress, isGzip } from 'firefox-profiler/utils/gz'; @@ -972,6 +973,61 @@ function _processSamples( return samples; } +/** + * Derive a CounterDisplayConfig from a counter's category and name. + */ +function _deriveCounterDisplay( + category: string, + name: string +): CounterDisplayConfig { + if (category === 'Memory') { + return { + graphType: 'line-accumulated', + unit: 'bytes', + color: 'orange', + markerSchemaLocation: 'timeline-memory', + sortWeight: 20, + label: 'Memory', + }; + } else if (category === 'power') { + return { + graphType: 'line-rate', + unit: 'pWh', + color: 'grey', + markerSchemaLocation: null, + sortWeight: 30, + label: name, + }; + } else if (category === 'Bandwidth') { + return { + graphType: 'line-rate', + unit: 'bytes', + color: 'blue', + markerSchemaLocation: null, + sortWeight: 10, + label: 'Bandwidth', + }; + } else if (category === 'CPU' && name === 'processCPU') { + return { + graphType: 'line-rate', + unit: 'percent', + color: 'grey', + markerSchemaLocation: null, + sortWeight: 40, + label: 'Process CPU', + }; + } + + return { + graphType: 'line-rate', + unit: '', + color: 'grey', + markerSchemaLocation: null, + sortWeight: 50, + label: name, + }; +} + /** * Converts the Gecko list of counters into the processed format. */ @@ -1031,6 +1087,7 @@ function _processCounters( pid: mainThreadPid, mainThreadIndex, samples: adjustTableTimeDeltas(processedCounterSamples, delta), + display: _deriveCounterDisplay(category, name), }); return result; }, diff --git a/src/profile-logic/processed-profile-versioning.ts b/src/profile-logic/processed-profile-versioning.ts index 691380cf78..c8d94509d9 100644 --- a/src/profile-logic/processed-profile-versioning.ts +++ b/src/profile-logic/processed-profile-versioning.ts @@ -3049,6 +3049,67 @@ const _upgraders: { } }, + [62]: (profile: any) => { + // Added CounterDisplayConfig to counters. This metadata controls how a + // counter is rendered (graph type, color, unit, etc.). + // Derive defaults from the counter's category and name. + if (profile.counters) { + for (const counter of profile.counters) { + if (counter.display !== undefined) { + continue; + } + const { category, name } = counter; + if (category === 'Memory') { + counter.display = { + graphType: 'line-accumulated', + unit: 'bytes', + color: counter.color ?? 'orange', + markerSchemaLocation: 'timeline-memory', + sortWeight: 20, + label: 'Memory', + }; + } else if (category === 'power') { + counter.display = { + graphType: 'line-rate', + unit: 'pWh', + color: counter.color ?? 'grey', + markerSchemaLocation: null, + sortWeight: 30, + label: name, + }; + } else if (category === 'Bandwidth') { + counter.display = { + graphType: 'line-rate', + unit: 'bytes', + color: counter.color ?? 'blue', + markerSchemaLocation: null, + sortWeight: 10, + label: 'Bandwidth', + }; + } else if (category === 'CPU' && name === 'processCPU') { + counter.display = { + graphType: 'line-rate', + unit: 'percent', + color: counter.color ?? 'grey', + markerSchemaLocation: null, + sortWeight: 40, + label: 'Process CPU', + }; + } else { + counter.display = { + graphType: 'line-rate', + unit: '', + color: counter.color ?? 'grey', + markerSchemaLocation: null, + sortWeight: 50, + label: name, + }; + } + delete counter.color; + } + } + }, + // If you add a new upgrader here, please document the change in // `docs-developer/CHANGELOG-formats.md`. }; diff --git a/src/profile-logic/profile-data.ts b/src/profile-logic/profile-data.ts index 645d8a9cc8..2e6093270b 100644 --- a/src/profile-logic/profile-data.ts +++ b/src/profile-logic/profile-data.ts @@ -2304,10 +2304,9 @@ export function processCounter(rawCounter: RawCounter): Counter { name: rawCounter.name, category: rawCounter.category, description: rawCounter.description, - color: rawCounter.color, pid: rawCounter.pid, mainThreadIndex: rawCounter.mainThreadIndex, - + display: rawCounter.display, samples, }; diff --git a/src/test/components/TrackBandwidth.test.tsx b/src/test/components/TrackBandwidth.test.tsx index b8109c8693..609fb05856 100644 --- a/src/test/components/TrackBandwidth.test.tsx +++ b/src/test/components/TrackBandwidth.test.tsx @@ -66,23 +66,26 @@ describe('TrackBandwidth', function () { for (let i = 7; i < thread.samples.length - 1; ++i) { sampleTimes[i] = 7 + i / 100; } - profile.counters = [ - getCounterForThreadWithSamples( - thread, - threadIndex, - { - time: sampleTimes.slice(), - // Bandwidth usage numbers. They are bytes. - count: [ - 10000, 40000, 50000, 100000, 2000000, 5000000, 30000, 1000000, - 20000, 1, 12000, 100000, - ], - length: SAMPLE_COUNT, - }, - 'SystemBandwidth', - 'bandwidth' - ), - ]; + const counter = getCounterForThreadWithSamples( + thread, + threadIndex, + { + time: sampleTimes.slice(), + // Bandwidth usage numbers. They are bytes. + count: [ + 10000, 40000, 50000, 100000, 2000000, 5000000, 30000, 1000000, 20000, + 1, 12000, 100000, + ], + length: SAMPLE_COUNT, + }, + 'SystemBandwidth', + 'bandwidth' + ); + counter.display = { + ...counter.display, + color: 'blue', + }; + profile.counters = [counter]; const store = storeWithProfile(profile); const { getState, dispatch } = store; const flushRafCalls = mockRaf(); diff --git a/src/test/components/TrackMemory.test.tsx b/src/test/components/TrackMemory.test.tsx index ab31d78e21..add3f14fea 100644 --- a/src/test/components/TrackMemory.test.tsx +++ b/src/test/components/TrackMemory.test.tsx @@ -63,9 +63,12 @@ describe('TrackMemory', function () { ); const threadIndex = 0; const thread = profile.threads[threadIndex]; - profile.counters = [ - getCounterForThread(thread, threadIndex, counterConfig), - ]; + const counter = getCounterForThread(thread, threadIndex, counterConfig); + counter.display = { + ...counter.display, + color: 'orange', + }; + profile.counters = [counter]; const store = storeWithProfile(profile); const { getState, dispatch } = store; const flushRafCalls = mockRaf(); diff --git a/src/test/fixtures/profiles/processed-profile.ts b/src/test/fixtures/profiles/processed-profile.ts index 5961d2c0bf..428b559836 100644 --- a/src/test/fixtures/profiles/processed-profile.ts +++ b/src/test/fixtures/profiles/processed-profile.ts @@ -33,6 +33,7 @@ import type { CategoryList, JsTracerTable, RawCounter, + CounterDisplayConfig, TabID, MarkerPayload, NetworkPayload, @@ -1479,6 +1480,18 @@ export function getProfileWithJsTracerEvents( return profile; } +/** + * Default display configuration for test counters. + */ +const DEFAULT_TEST_COUNTER_DISPLAY: CounterDisplayConfig = { + graphType: 'line-rate', + unit: '', + color: 'grey', + markerSchemaLocation: null, + sortWeight: 50, + label: 'My Counter', +}; + /** * Creates a Counter fixture for a given thread. */ @@ -1504,6 +1517,7 @@ export function getCounterForThread( count: sampleTimes.map((_, i) => Math.sin(i)), length: thread.samples.length, }, + display: DEFAULT_TEST_COUNTER_DISPLAY, }; return counter; } @@ -1541,6 +1555,7 @@ export function getCounterForThreadWithSamples( pid: thread.pid, mainThreadIndex, samples: newSamples, + display: DEFAULT_TEST_COUNTER_DISPLAY, }; return counter; } diff --git a/src/test/integration/symbolicator-cli/__snapshots__/symbolicator-cli.test.ts.snap b/src/test/integration/symbolicator-cli/__snapshots__/symbolicator-cli.test.ts.snap index 19dd70789f..3af235db1a 100644 --- a/src/test/integration/symbolicator-cli/__snapshots__/symbolicator-cli.test.ts.snap +++ b/src/test/integration/symbolicator-cli/__snapshots__/symbolicator-cli.test.ts.snap @@ -87,7 +87,7 @@ Object { "markerSchema": Array [], "oscpu": "macOS 14.6.1", "pausedRanges": Array [], - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "a.out", "sampleUnits": Object { @@ -1415,7 +1415,7 @@ Object { "markerSchema": Array [], "oscpu": "macOS 14.6.1", "pausedRanges": Array [], - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "a.out", "sampleUnits": Object { diff --git a/src/test/store/__snapshots__/profile-view.test.ts.snap b/src/test/store/__snapshots__/profile-view.test.ts.snap index f04baf9582..9f9343610e 100644 --- a/src/test/store/__snapshots__/profile-view.test.ts.snap +++ b/src/test/store/__snapshots__/profile-view.test.ts.snap @@ -418,7 +418,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Firefox", "sourceURL": "", diff --git a/src/test/unit/__snapshots__/profile-conversion.test.ts.snap b/src/test/unit/__snapshots__/profile-conversion.test.ts.snap index 6a5e04368e..b804fe58c0 100644 --- a/src/test/unit/__snapshots__/profile-conversion.test.ts.snap +++ b/src/test/unit/__snapshots__/profile-conversion.test.ts.snap @@ -591,7 +591,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "ART Trace (Android)", "sampleUnits": undefined, @@ -79192,7 +79192,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "ART Trace (Android)", "sampleUnits": undefined, @@ -315562,7 +315562,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Chrome Trace", "profilingEndTime": 119159778.026, @@ -350479,7 +350479,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Chrome Trace", "profilingEndTime": 119159778.026, @@ -385375,7 +385375,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Chrome Trace", "profilingEndTime": 66155012.423, @@ -387980,7 +387980,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Chrome Trace", "sourceURL": "", @@ -389809,7 +389809,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Chrome Trace", "profilingEndTime": 355035987.653, @@ -393482,7 +393482,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Chrome Trace", "sourceURL": "", @@ -398697,7 +398697,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Chrome Trace", "profilingEndTime": 66155012.423, @@ -399739,7 +399739,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Firefox", "sampleUnits": undefined, @@ -403614,7 +403614,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Firefox", "sampleUnits": undefined, @@ -418512,7 +418512,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Firefox", "sampleUnits": undefined, @@ -424719,7 +424719,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Firefox", "sampleUnits": undefined, @@ -441164,7 +441164,7 @@ Object { "keepProfileThreadOrder": true, "markerSchema": Array [], "platform": "Android", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "com.example.sampleapplication", "sourceCodeIsNotOnSearchfox": true, @@ -498819,7 +498819,7 @@ Object { "keepProfileThreadOrder": true, "markerSchema": Array [], "platform": "Android", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "com.example.sampleapplication", "sourceCodeIsNotOnSearchfox": true, @@ -561079,7 +561079,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "target/debug/examples/work_log (dhat)", "sourceURL": "", @@ -563127,7 +563127,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Flamegraph", "sourceURL": "", @@ -871013,7 +871013,7 @@ Object { "oscpu": "", "physicalCPUs": 0, "platform": "", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Flamegraph", "sourceURL": "", diff --git a/src/test/unit/__snapshots__/profile-upgrading.test.ts.snap b/src/test/unit/__snapshots__/profile-upgrading.test.ts.snap index e139904c6d..e85d4f0300 100644 --- a/src/test/unit/__snapshots__/profile-upgrading.test.ts.snap +++ b/src/test/unit/__snapshots__/profile-upgrading.test.ts.snap @@ -40,7 +40,7 @@ Object { "oscpu": undefined, "physicalCPUs": undefined, "platform": undefined, - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Firefox", "sampleUnits": undefined, @@ -7358,7 +7358,7 @@ Object { "misc": "rv:48.0", "oscpu": "Intel Mac OS X 10.11", "platform": "Macintosh", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Firefox", "stackwalk": 1, @@ -8693,7 +8693,7 @@ Object { "misc": "rv:48.0", "oscpu": "Intel Mac OS X 10.11", "platform": "Macintosh", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Firefox", "stackwalk": 1, @@ -9693,6 +9693,14 @@ Object { Object { "category": "Memory", "description": "Amount of allocated memory", + "display": Object { + "color": "orange", + "graphType": "line-accumulated", + "label": "Memory", + "markerSchemaLocation": "timeline-memory", + "sortWeight": 20, + "unit": "bytes", + }, "mainThreadIndex": 0, "name": "malloc", "pid": "11111", @@ -10155,7 +10163,7 @@ Object { "misc": "rv:48.0", "oscpu": "Intel Mac OS X 10.11", "platform": "Macintosh", - "preprocessedProfileVersion": 61, + "preprocessedProfileVersion": 62, "processType": 0, "product": "Firefox", "stackwalk": 1, diff --git a/src/types/profile-derived.ts b/src/types/profile-derived.ts index f52454fd9e..b634b0e876 100644 --- a/src/types/profile-derived.ts +++ b/src/types/profile-derived.ts @@ -13,7 +13,6 @@ import type { IndexIntoResourceTable, IndexIntoLibs, CounterIndex, - GraphColor, IndexIntoRawMarkerTable, IndexIntoStringTable, TabID, @@ -33,6 +32,7 @@ import type { IndexIntoFrameTable, SourceTable, IndexIntoSourceTable, + CounterDisplayConfig, } from './profile'; import type { IndexedArray } from './utils'; import type { BitSet } from '../utils/bitset'; @@ -187,10 +187,10 @@ export type Counter = { name: string; category: string; description: string; - color?: GraphColor; pid: Pid; mainThreadIndex: ThreadIndex; samples: CounterSamplesTable; + display: CounterDisplayConfig; }; /** diff --git a/src/types/profile.ts b/src/types/profile.ts index 90486b7ecd..a4072ca72e 100644 --- a/src/types/profile.ts +++ b/src/types/profile.ts @@ -529,14 +529,35 @@ export type GraphColor = | 'teal' | 'yellow'; +export type CounterGraphType = 'line-accumulated' | 'line-rate'; + +/** + * Specifies how a counter should be displayed in the UI. + */ +export type CounterDisplayConfig = { + graphType: CounterGraphType; + unit: string; + color: GraphColor; + // The marker schema display location to filter markers for this track, + // e.g., "timeline-memory". If null, no markers are shown. + markerSchemaLocation: string | null; + // Controls the default display position of this counter track relative to + // other tracks. Tracks with lower values appear closer to the top. + sortWeight: number; + // The human-readable label shown in the track sidebar. For known counter + // types this is a friendly name (eg, "Memory"); for generic counters + // it falls back to counter.name. + label: string; +}; + export type RawCounter = { name: string; category: string; description: string; - color?: GraphColor; pid: Pid; mainThreadIndex: ThreadIndex; samples: RawCounterSamplesTable; + display: CounterDisplayConfig; }; /** From d5eb4f2fdc6bb2c0d2fb5ee8ed9799a6ff04f747 Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 14:34:19 +0200 Subject: [PATCH 31/64] Update all Yarn dependencies (2026-04-15) (#5950) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> Co-authored-by: fatadel --- package.json | 8 +-- yarn.lock | 150 +++++++++++++++++++++++++-------------------------- 2 files changed, 79 insertions(+), 79 deletions(-) diff --git a/package.json b/package.json index 08a9fb32c3..e091457a9c 100644 --- a/package.json +++ b/package.json @@ -132,8 +132,8 @@ "@types/react-splitter-layout": "^4.0.0", "@types/redux-logger": "^3.0.6", "@types/tgwf__co2": "^0.14.2", - "@typescript-eslint/eslint-plugin": "^8.58.1", - "@typescript-eslint/parser": "^8.58.1", + "@typescript-eslint/eslint-plugin": "^8.58.2", + "@typescript-eslint/parser": "^8.58.2", "alex": "^11.0.1", "babel-jest": "^30.3.0", "babel-plugin-module-resolver": "^5.0.3", @@ -168,7 +168,7 @@ "npm-run-all2": "^8.0.4", "open": "^11.0.0", "patch-package": "^8.0.1", - "postcss": "^8.5.8", + "postcss": "^8.5.9", "postinstall-postinstall": "^2.1.0", "prettier": "^3.8.1", "rimraf": "^6.1.3", @@ -176,7 +176,7 @@ "stylelint-config-idiomatic-order": "^10.0.0", "stylelint-config-standard": "^40.0.0", "typescript": "^6.0.2", - "typescript-eslint": "^8.58.1", + "typescript-eslint": "^8.58.2", "workbox-cli": "^7.4.0", "yargs": "^18.0.0" }, diff --git a/yarn.lock b/yarn.lock index c1354c1ef9..3ef98d3b9a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2482,100 +2482,100 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@8.58.1", "@typescript-eslint/eslint-plugin@^8.58.1": - version "8.58.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz#cb53038b83d165ca0ef96d67d875efbd56c50fa8" - integrity sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ== +"@typescript-eslint/eslint-plugin@8.58.2", "@typescript-eslint/eslint-plugin@^8.58.2": + version "8.58.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz#a6882a6a328e1259cff259fdb03184245ef06191" + integrity sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw== dependencies: "@eslint-community/regexpp" "^4.12.2" - "@typescript-eslint/scope-manager" "8.58.1" - "@typescript-eslint/type-utils" "8.58.1" - "@typescript-eslint/utils" "8.58.1" - "@typescript-eslint/visitor-keys" "8.58.1" + "@typescript-eslint/scope-manager" "8.58.2" + "@typescript-eslint/type-utils" "8.58.2" + "@typescript-eslint/utils" "8.58.2" + "@typescript-eslint/visitor-keys" "8.58.2" ignore "^7.0.5" natural-compare "^1.4.0" ts-api-utils "^2.5.0" -"@typescript-eslint/parser@8.58.1", "@typescript-eslint/parser@^8.58.1": - version "8.58.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.58.1.tgz#0943eca522ac408bcdd649882c3d95b10ff00f62" - integrity sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw== +"@typescript-eslint/parser@8.58.2", "@typescript-eslint/parser@^8.58.2": + version "8.58.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.58.2.tgz#b267545e4bd515d896fe1f3a5b6f334fa6aa0026" + integrity sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg== dependencies: - "@typescript-eslint/scope-manager" "8.58.1" - "@typescript-eslint/types" "8.58.1" - "@typescript-eslint/typescript-estree" "8.58.1" - "@typescript-eslint/visitor-keys" "8.58.1" + "@typescript-eslint/scope-manager" "8.58.2" + "@typescript-eslint/types" "8.58.2" + "@typescript-eslint/typescript-estree" "8.58.2" + "@typescript-eslint/visitor-keys" "8.58.2" debug "^4.4.3" -"@typescript-eslint/project-service@8.58.1": - version "8.58.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.58.1.tgz#c78781b1ca1ec1e7bc6522efba89318c6d249feb" - integrity sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g== +"@typescript-eslint/project-service@8.58.2": + version "8.58.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.58.2.tgz#8c980249100e21b87baba0ca10880fdf893e0a8e" + integrity sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg== dependencies: - "@typescript-eslint/tsconfig-utils" "^8.58.1" - "@typescript-eslint/types" "^8.58.1" + "@typescript-eslint/tsconfig-utils" "^8.58.2" + "@typescript-eslint/types" "^8.58.2" debug "^4.4.3" -"@typescript-eslint/scope-manager@8.58.1", "@typescript-eslint/scope-manager@^8.56.0": - version "8.58.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.58.1.tgz#35168f561bab4e3fd10dd6b03e8b83c157479211" - integrity sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w== +"@typescript-eslint/scope-manager@8.58.2", "@typescript-eslint/scope-manager@^8.56.0": + version "8.58.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.58.2.tgz#aa73784d78f117940e83f71705af07ba695cd60c" + integrity sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q== dependencies: - "@typescript-eslint/types" "8.58.1" - "@typescript-eslint/visitor-keys" "8.58.1" + "@typescript-eslint/types" "8.58.2" + "@typescript-eslint/visitor-keys" "8.58.2" -"@typescript-eslint/tsconfig-utils@8.58.1", "@typescript-eslint/tsconfig-utils@^8.58.1": - version "8.58.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.1.tgz#eb16792c579300c7bfb3c74b0f5e1dfbb0a2454d" - integrity sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw== +"@typescript-eslint/tsconfig-utils@8.58.2", "@typescript-eslint/tsconfig-utils@^8.58.2": + version "8.58.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.2.tgz#fa13f96432c9348bf87f6f44826def585fad7bca" + integrity sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A== -"@typescript-eslint/type-utils@8.58.1": - version "8.58.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.58.1.tgz#b21085a233087bde94c92ba6f5b4dfb77ca56730" - integrity sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w== +"@typescript-eslint/type-utils@8.58.2": + version "8.58.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.58.2.tgz#024eb1dd597f8a34cb22d8d9ab32da857bc9a817" + integrity sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg== dependencies: - "@typescript-eslint/types" "8.58.1" - "@typescript-eslint/typescript-estree" "8.58.1" - "@typescript-eslint/utils" "8.58.1" + "@typescript-eslint/types" "8.58.2" + "@typescript-eslint/typescript-estree" "8.58.2" + "@typescript-eslint/utils" "8.58.2" debug "^4.4.3" ts-api-utils "^2.5.0" -"@typescript-eslint/types@8.58.1", "@typescript-eslint/types@^8.58.1": - version "8.58.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.58.1.tgz#9dfb4723fcd2b13737d8b03d941354cf73190313" - integrity sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw== +"@typescript-eslint/types@8.58.2", "@typescript-eslint/types@^8.58.2": + version "8.58.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.58.2.tgz#3ab8051de0f19a46ddefb0749d0f7d82974bd57c" + integrity sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ== -"@typescript-eslint/typescript-estree@8.58.1": - version "8.58.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.1.tgz#8230cc9628d2cffef101e298c62807c4b9bf2fe9" - integrity sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg== +"@typescript-eslint/typescript-estree@8.58.2": + version "8.58.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz#b1beb1f959385b341cc76f0aebbf028e23dfdb8b" + integrity sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw== dependencies: - "@typescript-eslint/project-service" "8.58.1" - "@typescript-eslint/tsconfig-utils" "8.58.1" - "@typescript-eslint/types" "8.58.1" - "@typescript-eslint/visitor-keys" "8.58.1" + "@typescript-eslint/project-service" "8.58.2" + "@typescript-eslint/tsconfig-utils" "8.58.2" + "@typescript-eslint/types" "8.58.2" + "@typescript-eslint/visitor-keys" "8.58.2" debug "^4.4.3" minimatch "^10.2.2" semver "^7.7.3" tinyglobby "^0.2.15" ts-api-utils "^2.5.0" -"@typescript-eslint/utils@8.58.1", "@typescript-eslint/utils@^8.0.0", "@typescript-eslint/utils@^8.56.0": - version "8.58.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.58.1.tgz#099a327b04ed921e6ee3988cde9ef34bc4b5435a" - integrity sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ== +"@typescript-eslint/utils@8.58.2", "@typescript-eslint/utils@^8.0.0", "@typescript-eslint/utils@^8.56.0": + version "8.58.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.58.2.tgz#27165554a02d1ff57d98262fa92060498dabc8b3" + integrity sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA== dependencies: "@eslint-community/eslint-utils" "^4.9.1" - "@typescript-eslint/scope-manager" "8.58.1" - "@typescript-eslint/types" "8.58.1" - "@typescript-eslint/typescript-estree" "8.58.1" + "@typescript-eslint/scope-manager" "8.58.2" + "@typescript-eslint/types" "8.58.2" + "@typescript-eslint/typescript-estree" "8.58.2" -"@typescript-eslint/visitor-keys@8.58.1": - version "8.58.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.1.tgz#7c197533177f1ba9b8249f55f7f685e32bb6f204" - integrity sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ== +"@typescript-eslint/visitor-keys@8.58.2": + version "8.58.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz#9ed699eaa9b5720b6b6b6f9c16e6c7d4cd32b276" + integrity sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA== dependencies: - "@typescript-eslint/types" "8.58.1" + "@typescript-eslint/types" "8.58.2" eslint-visitor-keys "^5.0.0" "@ungap/structured-clone@^1.3.0": @@ -9001,10 +9001,10 @@ postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.4.32, postcss@^8.5.8: - version "8.5.8" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.8.tgz#6230ecc8fb02e7a0f6982e53990937857e13f399" - integrity sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg== +postcss@^8.4.32, postcss@^8.5.8, postcss@^8.5.9: + version "8.5.9" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.9.tgz#f6ee9e0b94f0f19c97d2f172bfbd7fc71fe1cca4" + integrity sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw== dependencies: nanoid "^3.3.11" picocolors "^1.1.1" @@ -10963,15 +10963,15 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typescript-eslint@^8.58.1: - version "8.58.1" - resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.58.1.tgz#e765cbfea5774dcb4b1473e5e77a46254f309b32" - integrity sha512-gf6/oHChByg9HJvhMO1iBexJh12AqqTfnuxscMDOVqfJW3htsdRJI/GfPpHTTcyeB8cSTUY2JcZmVgoyPqcrDg== +typescript-eslint@^8.58.2: + version "8.58.2" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.58.2.tgz#55f425fc668c2d5148f45587f2cd04532d715c6a" + integrity sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ== dependencies: - "@typescript-eslint/eslint-plugin" "8.58.1" - "@typescript-eslint/parser" "8.58.1" - "@typescript-eslint/typescript-estree" "8.58.1" - "@typescript-eslint/utils" "8.58.1" + "@typescript-eslint/eslint-plugin" "8.58.2" + "@typescript-eslint/parser" "8.58.2" + "@typescript-eslint/typescript-estree" "8.58.2" + "@typescript-eslint/utils" "8.58.2" typescript@^6.0.2: version "6.0.2" From bb3b854dde013f8298fa5c8bc4e2c5c95b01df4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Mon, 20 Apr 2026 13:35:29 +0200 Subject: [PATCH 32/64] Fix eslint-config-prettier silently overriding custom rules (#5955) Move prettierConfig before our custom rules block so it only disables formatting rules from the base configs, while our explicit rules take precedence. This was causing `curly: 'error'` (and potentially other custom rules) to be silently disabled. --- eslint.config.mjs | 10 ++++++---- scripts/lib/dev-server.mjs | 4 +++- scripts/lib/esbuild-plugins.mjs | 4 +++- src/components/stack-chart/Canvas.tsx | 4 +++- src/test/store/symbolication.test.ts | 4 +++- 5 files changed, 18 insertions(+), 8 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 6706b48b92..8c4f1150dd 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -34,6 +34,11 @@ export default defineConfig( // React config reactPlugin.configs.flat.recommended, + // Prettier config must be placed here to disable formatting rules from the + // base configs above, while allowing our custom rules below to take + // precedence. + prettierConfig, + // Custom configuration for all files { files: ['**/*.js', '**/*.mjs', '**/*.cjs', '**/*.ts', '**/*.tsx'], @@ -311,8 +316,5 @@ export default defineConfig( ...globals.jest, }, }, - }, - - // Prettier config (must be last to override other formatting rules) - prettierConfig + } ); diff --git a/scripts/lib/dev-server.mjs b/scripts/lib/dev-server.mjs index d813bdfc6a..39f86bc4b3 100644 --- a/scripts/lib/dev-server.mjs +++ b/scripts/lib/dev-server.mjs @@ -128,7 +128,9 @@ export async function startDevServer(buildConfig, options = {}) { // Graceful shutdown let isShuttingDown = false; process.on('SIGINT', async () => { - if (isShuttingDown) return; + if (isShuttingDown) { + return; + } isShuttingDown = true; console.log('\nShutting down...'); diff --git a/scripts/lib/esbuild-plugins.mjs b/scripts/lib/esbuild-plugins.mjs index 40d62d0741..c3cd6eac3b 100644 --- a/scripts/lib/esbuild-plugins.mjs +++ b/scripts/lib/esbuild-plugins.mjs @@ -24,7 +24,9 @@ export function circularDependencyPlugin() { setup(build) { build.initialOptions.metafile = true; build.onEnd((result) => { - if (!result.metafile?.inputs) return; + if (!result.metafile?.inputs) { + return; + } const { inputs } = result.metafile; const recursionStack = new Set(); diff --git a/src/components/stack-chart/Canvas.tsx b/src/components/stack-chart/Canvas.tsx index a30950153b..71bd684ab2 100644 --- a/src/components/stack-chart/Canvas.tsx +++ b/src/components/stack-chart/Canvas.tsx @@ -665,7 +665,9 @@ class StackChartCanvasImpl extends React.PureComponent { }; _onDoubleClickStack = (hoveredItem: HoveredStackTiming | null) => { - if (!hoveredItem) return; + if (!hoveredItem) { + return; + } const result = this._getCallNodeIndexOrMarkerIndexFromHoveredItem(hoveredItem); diff --git a/src/test/store/symbolication.test.ts b/src/test/store/symbolication.test.ts index 40c47f71ed..17226e2450 100644 --- a/src/test/store/symbolication.test.ts +++ b/src/test/store/symbolication.test.ts @@ -259,7 +259,9 @@ describe('doSymbolicateProfile', function () { // Helper function to get filename from source index const getFileName = (funcIndex: number): string | null => { const sourceIndex = funcTable.source[funcIndex]; - if (sourceIndex === null) return null; + if (sourceIndex === null) { + return null; + } const urlIndex = sources.filename[sourceIndex]; return stringTable.getString(urlIndex); }; From 484cfd2f9b56ebb6d2941631034363a1d4e68ee0 Mon Sep 17 00:00:00 2001 From: fatadel Date: Mon, 20 Apr 2026 17:14:25 +0200 Subject: [PATCH 33/64] Dim non-matching nodes in the stack chart when searching (#5935) When the search box is active, the stack chart now dims funcs that don't match the search, making hits visually scannable without losing the surrounding context. Matching is OR-combined across comma-separated search strings (a func is highlighted if any term matches its name, filename, or library), while the existing stack-drop behavior keeps its AND semantics. The match computation lives in a memoized selector (computeSearchStringFilterOutput) that returns both the stack-drop TransformOutput and a combined func-match BitSet, shared between getFilteredThread and getSearchFilteredFuncMatchesBitSet. The canvas draw loop does a single checkBit lookup per node instead of rebuilding a Set and re-running string matching on every frame. --- src/components/stack-chart/Canvas.tsx | 33 +++++-- src/components/stack-chart/index.tsx | 6 ++ src/profile-logic/profile-data.ts | 111 ++++++++++++++++-------- src/selectors/per-thread/thread.tsx | 43 ++++++--- src/test/components/StackChart.test.tsx | 51 +++++++++++ src/test/store/profile-view.test.ts | 86 ++++++++++++++++++ src/utils/bitset.ts | 9 ++ src/utils/colors.ts | 19 ++++ 8 files changed, 302 insertions(+), 56 deletions(-) diff --git a/src/components/stack-chart/Canvas.tsx b/src/components/stack-chart/Canvas.tsx index 71bd684ab2..5468e8e3a0 100644 --- a/src/components/stack-chart/Canvas.tsx +++ b/src/components/stack-chart/Canvas.tsx @@ -15,6 +15,7 @@ import type { changeMouseTimePosition } from '../../actions/profile-view'; type ChangeMouseTimePosition = typeof changeMouseTimePosition; import { mapCategoryColorNameToStackChartStyles, + getDimmedStyles, getForegroundColor, getBackgroundColor, } from '../../utils/colors'; @@ -40,6 +41,8 @@ import type { Page, TimelineUnit, } from 'firefox-profiler/types'; +import type { BitSet } from 'firefox-profiler/utils/bitset'; +import { checkBit } from 'firefox-profiler/utils/bitset'; import type { CallNodeInfo } from 'firefox-profiler/profile-logic/call-node-info'; import type { @@ -78,6 +81,7 @@ type OwnProps = { readonly displayStackType: boolean; readonly useStackChartSameWidths: boolean; readonly timelineUnit: TimelineUnit; + readonly searchFilteredFuncMatchesBitSet: BitSet | null; }; type Props = Readonly< @@ -183,6 +187,7 @@ class StackChartCanvasImpl extends React.PureComponent { getMarker, marginLeft, useStackChartSameWidths, + searchFilteredFuncMatchesBitSet, viewport: { containerWidth, containerHeight, @@ -480,9 +485,11 @@ class StackChartCanvasImpl extends React.PureComponent { // Look up information about this stack frame. let text, category, isSelected; + let currentFuncIndex: number | null = null; if ('callNode' in stackTiming && stackTiming.callNode) { const callNodeIndex = stackTiming.callNode[i]; const funcIndex = callNodeTable.func[callNodeIndex]; + currentFuncIndex = funcIndex; const funcNameIndex = thread.funcTable.name[funcIndex]; text = thread.stringTable.getString(funcNameIndex); const categoryIndex = callNodeTable.category[callNodeIndex]; @@ -507,9 +514,19 @@ class StackChartCanvasImpl extends React.PureComponent { depth === hoveredItem.depth && i === hoveredItem.stackTimingIndex; - const colorStyles = mapCategoryColorNameToStackChartStyles( - category.color - ); + // When a search is active, use the dimmed style for non-matching nodes + // so that matching nodes stand out with their category color. + // Hovered or selected nodes always use their real category color. + const isDimmed = + searchFilteredFuncMatchesBitSet !== null && + currentFuncIndex !== null && + !checkBit(searchFilteredFuncMatchesBitSet, currentFuncIndex) && + !isHovered && + !isSelected; + const colorStyles = isDimmed + ? getDimmedStyles() + : mapCategoryColorNameToStackChartStyles(category.color); + // Draw the box. fastFillStyle.set( isHovered || isSelected @@ -542,11 +559,11 @@ class StackChartCanvasImpl extends React.PureComponent { if (textW > textMeasurement.minWidth) { const fittedText = textMeasurement.getFittedText(text, textW); if (fittedText) { - fastFillStyle.set( - isHovered || isSelected - ? colorStyles.getSelectedTextColor() - : getForegroundColor() - ); + if (isHovered || isSelected || isDimmed) { + fastFillStyle.set(colorStyles.getSelectedTextColor()); + } else { + fastFillStyle.set(getForegroundColor()); + } ctx.fillText(fittedText, textX, intY + textDevicePixelsOffsetTop); } } diff --git a/src/components/stack-chart/index.tsx b/src/components/stack-chart/index.tsx index ba98f87a9b..ea20211a15 100644 --- a/src/components/stack-chart/index.tsx +++ b/src/components/stack-chart/index.tsx @@ -58,6 +58,7 @@ import type { Page, TimelineUnit, } from 'firefox-profiler/types'; +import type { BitSet } from 'firefox-profiler/utils/bitset'; import type { CallNodeInfo } from 'firefox-profiler/profile-logic/call-node-info'; import type { ConnectedProps } from '../../utils/connect'; @@ -87,6 +88,7 @@ type StateProps = { readonly hasFilteredCtssSamples: boolean; readonly useStackChartSameWidths: boolean; readonly timelineUnit: TimelineUnit; + readonly searchFilteredFuncMatchesBitSet: BitSet | null; }; type DispatchProps = { @@ -244,6 +246,7 @@ class StackChartImpl extends React.PureComponent { hasFilteredCtssSamples, useStackChartSameWidths, timelineUnit, + searchFilteredFuncMatchesBitSet, } = this.props; const maxViewportHeight = combinedTimingRows.length * STACK_FRAME_HEIGHT; @@ -304,6 +307,7 @@ class StackChartImpl extends React.PureComponent { displayStackType: displayStackType, useStackChartSameWidths, timelineUnit, + searchFilteredFuncMatchesBitSet, }} />
@@ -347,6 +351,8 @@ export const StackChart = explicitConnect<{}, StateProps, DispatchProps>({ selectedThreadSelectors.getHasFilteredCtssSamples(state), useStackChartSameWidths: getStackChartSameWidths(state), timelineUnit: getProfileTimelineUnit(state), + searchFilteredFuncMatchesBitSet: + selectedThreadSelectors.getSearchFilteredFuncMatchesBitSet(state), }; }, mapDispatchToProps: { diff --git a/src/profile-logic/profile-data.ts b/src/profile-logic/profile-data.ts index 2e6093270b..15d2721118 100644 --- a/src/profile-logic/profile-data.ts +++ b/src/profile-logic/profile-data.ts @@ -28,6 +28,7 @@ import { type BitSet, checkBit, combineTwoBitSetsWithAnd, + combineTwoBitSetsWithOr, makeBitSet, setBit, } from 'firefox-profiler/utils/bitset'; @@ -1720,7 +1721,22 @@ export function applyTransformOutputToThread( }); } -export function computeTransformOutputForSearchStringFilter( +/** + * Output of the search string filter: both the filter's TransformOutput (used + * to drop non-matching stacks) and the combined func bitset (used to highlight + * matching nodes in the stack chart). Exposed together so both are computed + * once and memoized together. + * + * Stacks are AND-combined across search strings (a stack is kept only if it + * contains a match for every search string), while funcs are OR-combined (a + * func is highlighted if it matches any of the search strings). + */ +export type SearchStringFilterOutput = { + transformOutput: TransformOutput; + funcMatchesSearchStrings: BitSet | null; +}; + +export function computeSearchStringFilterOutput( stackTable: StackTable, frameTable: FrameTable, funcTable: FuncTable, @@ -1728,51 +1744,67 @@ export function computeTransformOutputForSearchStringFilter( sources: SourceTable, stringTable: StringTable, searchStrings: string[] | null -): TransformOutput { - return timeCode('computeTransformOutputForSearchStringFilter', () => { - if (!searchStrings) { - return { newStackTable: stackTable, effectOnThreadData: {} }; +): SearchStringFilterOutput { + return timeCode('computeSearchStringFilterOutput', () => { + if (!searchStrings || searchStrings.length === 0) { + return { + transformOutput: { newStackTable: stackTable, effectOnThreadData: {} }, + funcMatchesSearchStrings: null, + }; } - const stackMatchesAllSearchStrings = searchStrings - .filter((s) => s) - .reduce( - ( - stackMatchesPreviousSearchStrings: BitSet | undefined, - searchString: string - ) => { - const stackMatchesThisString = _computeStackMatchesSearchString( - stackTable, - frameTable, - funcTable, - resourceTable, - sources, - stringTable, - searchString - ); - if (stackMatchesPreviousSearchStrings !== undefined) { - return combineTwoBitSetsWithAnd( - stackMatchesThisString, - stackMatchesPreviousSearchStrings - ); - } - return stackMatchesThisString; - }, - undefined + const computeMatchesForString = (searchString: string) => { + const funcMatches = computeFuncMatchesSearchString( + funcTable, + resourceTable, + sources, + stringTable, + searchString + ); + const stackMatches = _computeStackMatchesFromFuncMatches( + stackTable, + frameTable, + funcMatches ); + return { funcMatches, stackMatches }; + }; + + let { + funcMatches: combinedFuncMatches, + stackMatches: combinedStackMatches, + } = computeMatchesForString(searchStrings[0]); + for (let i = 1; i < searchStrings.length; i++) { + const { funcMatches, stackMatches } = computeMatchesForString( + searchStrings[i] + ); + combinedFuncMatches = combineTwoBitSetsWithOr( + funcMatches, + combinedFuncMatches + ); + combinedStackMatches = combineTwoBitSetsWithAnd( + stackMatches, + combinedStackMatches + ); + } return { - newStackTable: stackTable, - effectOnThreadData: { - dropIfOldStackIsNot: stackMatchesAllSearchStrings, + transformOutput: { + newStackTable: stackTable, + effectOnThreadData: { + dropIfOldStackIsNot: combinedStackMatches, + }, }, + funcMatchesSearchStrings: combinedFuncMatches, }; }); } -function _computeStackMatchesSearchString( - stackTable: StackTable, - frameTable: FrameTable, +/** + * Compute a BitSet of functions whose name, source filename, or resource name + * matches the given search string. This is used both for filtering stacks and + * for dimming non-matching nodes in the stack chart. + */ +export function computeFuncMatchesSearchString( funcTable: FuncTable, resourceTable: ResourceTable, sources: SourceTable, @@ -1815,7 +1847,14 @@ function _computeStackMatchesSearchString( setBit(funcMatchesSearch, funcIndex); } } + return funcMatchesSearch; +} +function _computeStackMatchesFromFuncMatches( + stackTable: StackTable, + frameTable: FrameTable, + funcMatchesSearch: BitSet +): BitSet { const stackMatchesSearch = makeBitSet(stackTable.length); for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) { const prefix = stackTable.prefix[stackIndex]; diff --git a/src/selectors/per-thread/thread.tsx b/src/selectors/per-thread/thread.tsx index 00158837aa..8f62a266fa 100644 --- a/src/selectors/per-thread/thread.tsx +++ b/src/selectors/per-thread/thread.tsx @@ -46,6 +46,7 @@ import type { } from 'firefox-profiler/types'; import type { TransformLabeL10nIds } from 'firefox-profiler/profile-logic/transforms'; +import type { BitSet } from 'firefox-profiler/utils/bitset'; import type { MarkerSelectorsPerThread } from './markers'; import { mergeThreads } from '../../profile-logic/merge-compare'; @@ -62,8 +63,8 @@ const globallyMemoizedComputeTransformOutputForImplementationFilter = memoize( limit: 2, } ); -const globallyMemoizedComputeTransformOutputForSearchStringFilter = memoize( - ProfileData.computeTransformOutputForSearchStringFilter, +const globallyMemoizedComputeSearchStringFilterOutput = memoize( + ProfileData.computeSearchStringFilterOutput, { limit: 2, } @@ -488,13 +489,17 @@ export function getThreadSelectorsWithMarkersPerThread( } ); - const getFilteredThread: Selector = createSelector( - _getImplementationFilteredThread, - UrlState.getSearchStrings, - (thread: Thread, searchStrings) => { - // Apply the search string filter. - const transformOutput = - globallyMemoizedComputeTransformOutputForSearchStringFilter( + /** + * Single memoized computation of the search string filter, shared by + * `getFilteredThread` (which needs the stack-drop bitset) and + * `getSearchFilteredFuncMatchesBitSet` (which needs the func-match bitset). + */ + const _getSearchStringFilterOutput: Selector = + createSelector( + _getImplementationFilteredThread, + UrlState.getSearchStrings, + (thread: Thread, searchStrings) => + globallyMemoizedComputeSearchStringFilterOutput( thread.stackTable, thread.frameTable, thread.funcTable, @@ -502,11 +507,24 @@ export function getThreadSelectorsWithMarkersPerThread( thread.sources, thread.stringTable, searchStrings - ); - return ProfileData.applyTransformOutputToThread(transformOutput, thread); - } + ) + ); + + const getFilteredThread: Selector = createSelector( + _getImplementationFilteredThread, + _getSearchStringFilterOutput, + (thread: Thread, { transformOutput }) => + ProfileData.applyTransformOutputToThread(transformOutput, thread) ); + /** + * Get a BitSet of func indices that match the current search strings. + * Returns null when there is no active search. This is used by the stack + * chart to dim non-matching nodes without recomputing on every draw call. + */ + const getSearchFilteredFuncMatchesBitSet: Selector = (state) => + _getSearchStringFilterOutput(state).funcMatchesSearchStrings; + const getPreviewFilteredThread: Selector = createSelector( getFilteredThread, ProfileSelectors.getPreviewSelection, @@ -613,6 +631,7 @@ export function getThreadSelectorsWithMarkersPerThread( getTransformStack, getRangeAndTransformFilteredThread, getFilteredThread, + getSearchFilteredFuncMatchesBitSet, getPreviewFilteredThread, getFilteredCtssSamples, getPreviewFilteredCtssSamples, diff --git a/src/test/components/StackChart.test.tsx b/src/test/components/StackChart.test.tsx index e8a2769361..1dab68d697 100644 --- a/src/test/components/StackChart.test.tsx +++ b/src/test/components/StackChart.test.tsx @@ -33,6 +33,7 @@ import { changeImplementationFilter, changeCallTreeSummaryStrategy, updatePreviewSelection, + changeCallTreeSearchString, } from '../../actions/profile-view'; import { changeSelectedTab } from '../../actions/app'; import { selectedThreadSelectors } from '../../selectors/per-thread'; @@ -271,6 +272,56 @@ describe('StackChart', function () { expect(drawnFrames).not.toContain('Z'); }); + it('dims non-matching boxes when searching', function () { + const { dispatch, flushRafCalls } = setupSamples(); + flushDrawLog(); + + // Dispatch a search string that matches some function names. + act(() => { + dispatch(changeCallTreeSearchString('B')); + }); + flushRafCalls(); + + const drawCalls = flushDrawLog(); + + // Non-matching boxes should be drawn with the dimmed style. + const dimmedFillCalls = drawCalls.filter( + ([fn, value]) => fn === 'set fillStyle' && value === '#f9f9fa' + ); + expect(dimmedFillCalls.length).toBeGreaterThan(0); + }); + + it('does not dim boxes that match the search string', function () { + // Use a single-node call stack so there is exactly one box. + const { dispatch, flushRafCalls } = setupSamples(` + A[cat:DOM] + `); + flushDrawLog(); + + // Search for "A" — the only node matches, so nothing should be dimmed. + act(() => { + dispatch(changeCallTreeSearchString('A')); + }); + flushRafCalls(); + + const drawCalls = flushDrawLog(); + const dimmedFillCalls = drawCalls.filter( + ([fn, value]) => fn === 'set fillStyle' && value === '#f9f9fa' + ); + expect(dimmedFillCalls).toHaveLength(0); + }); + + it('does not dim any boxes when there is no search string', function () { + setupSamples(); + const drawCalls = flushDrawLog(); + + // No dimmed fill should be applied without a search. + const dimmedFillCalls = drawCalls.filter( + ([fn, value]) => fn === 'set fillStyle' && value === '#f9f9fa' + ); + expect(dimmedFillCalls).toHaveLength(0); + }); + describe('EmptyReasons', () => { it('shows reasons when a profile has no samples', () => { const profile = getEmptyProfile(); diff --git a/src/test/store/profile-view.test.ts b/src/test/store/profile-view.test.ts index c78779dcf3..45f07b4ae4 100644 --- a/src/test/store/profile-view.test.ts +++ b/src/test/store/profile-view.test.ts @@ -48,6 +48,7 @@ import { type BreakdownByCategory, } from '../../profile-logic/profile-data'; import { getSelfAndTotalForCallNode } from '../../profile-logic/call-tree'; +import { checkBit } from '../../utils/bitset'; import type { TrackReference, @@ -922,6 +923,91 @@ describe('actions/ProfileView', function () { }); }); + /** + * Covers the bitset returned by `getSearchFilteredFuncMatchesBitSet`, which + * the stack chart uses to dim non-matching nodes. The stack filter above + * only exercises whether each stack is kept; these tests pin down the + * per-func match semantics across name, filename, and library fields, and + * the OR semantics when multiple search strings are provided. + */ + describe('getSearchFilteredFuncMatchesBitSet', function () { + // Each func's three searchable fields (name, resource/lib, filename) use + // distinct prefixes ("fn", "rs", "sc") and a unique per-func index, so + // every search string used below appears in exactly one field on exactly + // one func. That lets us assert unambiguously which field triggered a + // match. + function setup() { + const { + profile, + funcNamesPerThread: [funcNames], + } = getProfileFromTextSamples(` + fn1[lib:rs1][file:sc1] fn2[lib:rs2][file:sc2] fn3[lib:rs3][file:sc3] + fn4[lib:rs4][file:sc4] + `); + const { dispatch, getState } = storeWithProfile(profile); + return { dispatch, getState, funcNames }; + } + + function getMatchingFuncNames( + getState: () => any, + funcNames: string[] + ): string[] { + const bitSet = + selectedThreadSelectors.getSearchFilteredFuncMatchesBitSet(getState()); + if (bitSet === null) { + return []; + } + const matched: string[] = []; + for (let i = 0; i < funcNames.length; i++) { + if (checkBit(bitSet, i)) { + matched.push(funcNames[i]); + } + } + return matched.sort(); + } + + it('returns null when there is no active search', function () { + const { getState } = setup(); + expect( + selectedThreadSelectors.getSearchFilteredFuncMatchesBitSet(getState()) + ).toBeNull(); + }); + + it('matches a single func by its name', function () { + const { dispatch, getState, funcNames } = setup(); + dispatch(ProfileView.changeCallTreeSearchString('fn1')); + expect(getMatchingFuncNames(getState, funcNames)).toEqual(['fn1']); + }); + + it('matches a func by its filename', function () { + const { dispatch, getState, funcNames } = setup(); + // sc2 is only set on fn2. + dispatch(ProfileView.changeCallTreeSearchString('sc2')); + expect(getMatchingFuncNames(getState, funcNames)).toEqual(['fn2']); + }); + + it('matches a func by its resource/library name', function () { + const { dispatch, getState, funcNames } = setup(); + // rs3 is only set on fn3. Match is case-insensitive. + dispatch(ProfileView.changeCallTreeSearchString('RS3')); + expect(getMatchingFuncNames(getState, funcNames)).toEqual(['fn3']); + }); + + it('matches every func matching any of several search strings (OR)', function () { + const { dispatch, getState, funcNames } = setup(); + // "fn1" matches only func fn1; "fn3" matches only func fn3. + dispatch(ProfileView.changeCallTreeSearchString('fn1,fn3')); + expect(getMatchingFuncNames(getState, funcNames)).toEqual(['fn1', 'fn3']); + }); + + it('combines matches across different fields with OR', function () { + const { dispatch, getState, funcNames } = setup(); + // "sc1" matches fn1 by filename; "rs4" matches fn4 by lib name. + dispatch(ProfileView.changeCallTreeSearchString('sc1,rs4')); + expect(getMatchingFuncNames(getState, funcNames)).toEqual(['fn1', 'fn4']); + }); + }); + /** * This test is more involved on checking for correctness compared to the other * tests, which are more for asserting their simple getter/setter types of behavior. diff --git a/src/utils/bitset.ts b/src/utils/bitset.ts index 2393189679..5dcd1db063 100644 --- a/src/utils/bitset.ts +++ b/src/utils/bitset.ts @@ -54,6 +54,15 @@ export function combineTwoBitSetsWithAnd(a: BitSet, b: BitSet): BitSet { return result; } +export function combineTwoBitSetsWithOr(a: BitSet, b: BitSet): BitSet { + const slotCount = a.length; + const result = new Int32Array(slotCount); + for (let i = 0; i < slotCount; i++) { + result[i] = a[i] | b[i]; + } + return result; +} + export class BitSetOutOfBoundsError extends Error { override name = 'BitSetOutOfBoundsError'; bitIndex: number; diff --git a/src/utils/colors.ts b/src/utils/colors.ts index 0efd24d0f6..b93ec771d3 100644 --- a/src/utils/colors.ts +++ b/src/utils/colors.ts @@ -10,12 +10,14 @@ import { GREEN_50, GREEN_60, GREEN_70, + GREY_10, GREY_20, GREY_30, GREY_40, GREY_50, GREY_60, GREY_70, + GREY_80, MAGENTA_60, MAGENTA_70, ORANGE_50, @@ -209,6 +211,23 @@ export function mapCategoryColorNameToStackChartStyles( return mapCategoryColorNameToStyles(colorName); } +/** + * A neutral style used to dim non-matching nodes in the stack chart when a + * search filter is active. Closer to the background than any category color + * so that matching nodes stand out clearly. + */ +const DIMMED_STYLE: ColorStyles = { + ...DEFAULT_STYLE, + _selectedFillStyle: [GREY_10, GREY_80], + _unselectedFillStyle: [GREY_10, GREY_80], + _selectedTextColor: [GREY_50, GREY_40], + gravity: 0, +}; + +export function getDimmedStyles(): ColorStyles { + return DIMMED_STYLE; +} + export function getForegroundColor(): string { return lightDark('#000000', GREY_20); } From 2e7e9ab7dd204c3d570d0f415e4ef3d261d1ed20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Tue, 21 Apr 2026 18:53:34 +0200 Subject: [PATCH 34/64] Fix loading .json.gz profiles from inside zip archives (#5959) When clicking a .json.gz entry in a zip file, viewProfileFromZip called file.async('string'), which decoded the gzip bytes as UTF-8 and corrupted the stream before unserializeProfileOfArbitraryFormat could detect the gzip magic and decompress. Switching to 'uint8array' keeps the content binary so the existing isGzip/decompress path runs. Plain .json entries still work via the Uint8Array to TextDecoder fallback. --- src/actions/zipped-profiles.ts | 2 +- src/test/store/zipped-profiles.test.ts | 30 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/actions/zipped-profiles.ts b/src/actions/zipped-profiles.ts index e51d660ae8..acde01eee1 100644 --- a/src/actions/zipped-profiles.ts +++ b/src/actions/zipped-profiles.ts @@ -57,7 +57,7 @@ export function viewProfileFromZip( try { // Attempt to unserialize the profile. const profile = await unserializeProfileOfArbitraryFormat( - await file.async('string'), + await file.async('uint8array'), pathInZipFile ); diff --git a/src/test/store/zipped-profiles.test.ts b/src/test/store/zipped-profiles.test.ts index aceee2a398..19db955c36 100644 --- a/src/test/store/zipped-profiles.test.ts +++ b/src/test/store/zipped-profiles.test.ts @@ -15,6 +15,9 @@ import JSZip from 'jszip'; import * as ZippedProfilesActions from '../../actions/zipped-profiles'; import * as ReceiveProfileActions from '../../actions/receive-profile'; import * as ProfileViewActions from '../../actions/profile-view'; +import { getProfileFromTextSamples } from '../fixtures/profiles/processed-profile'; +import { serializeProfile } from '../../profile-logic/process-profile'; +import { compress } from '../../utils/gz'; import type { PreviewSelection } from 'firefox-profiler/types'; describe('reducer zipFileState', function () { @@ -101,6 +104,33 @@ describe('reducer zipFileState', function () { }); }); + it('can load a gzipped profile from a zip file', async function () { + const store = createStore(); + const { getState, dispatch } = store; + const { profile } = getProfileFromTextSamples('A'); + // Re-wrap in this realm's Uint8Array: the worker-backed `compress` + // returns a typed array whose `instanceof Uint8Array` check fails against + // the main realm's global, so JSZip wouldn't recognize it directly. + const gzippedProfile = new Uint8Array( + await compress(serializeProfile(profile)) + ); + + const zip = new JSZip(); + zip.file('profile.json.gz', gzippedProfile); + const zipBytes = await zip.generateAsync({ type: 'uint8array' }); + const loadedZip = await JSZip.loadAsync(zipBytes); + dispatch(ReceiveProfileActions.receiveZipFile(loadedZip)); + + await dispatch( + ZippedProfilesActions.viewProfileFromPathInZipFile('profile.json.gz') + ); + + expect(ZippedProfilesSelectors.getZipFileState(getState()).phase).toBe( + 'VIEW_PROFILE_IN_ZIP_FILE' + ); + expect(ProfileViewSelectors.getProfile(getState())).toBeTruthy(); + }); + it('will fail when trying to load an invalid profile', async function () { const store = createStore(); const { getState, dispatch } = store; From edba2fd95f9838a7020691fda4e1121bd1a72c43 Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 11:10:42 +0200 Subject: [PATCH 35/64] Update all Yarn dependencies (2026-04-22) (#5960) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> --- package.json | 14 ++--- yarn.lock | 174 +++++++++++++++++++++++++-------------------------- 2 files changed, 94 insertions(+), 94 deletions(-) diff --git a/package.json b/package.json index e091457a9c..1eff7286c0 100644 --- a/package.json +++ b/package.json @@ -132,14 +132,14 @@ "@types/react-splitter-layout": "^4.0.0", "@types/redux-logger": "^3.0.6", "@types/tgwf__co2": "^0.14.2", - "@typescript-eslint/eslint-plugin": "^8.58.2", - "@typescript-eslint/parser": "^8.58.2", + "@typescript-eslint/eslint-plugin": "^8.59.0", + "@typescript-eslint/parser": "^8.59.0", "alex": "^11.0.1", "babel-jest": "^30.3.0", "babel-plugin-module-resolver": "^5.0.3", "browserslist": "^4.28.2", "browserslist-to-esbuild": "^2.1.1", - "caniuse-lite": "^1.0.30001780", + "caniuse-lite": "^1.0.30001788", "cross-env": "^10.1.0", "cross-spawn": "^7.0.6", "devtools-license-check": "^0.9.0", @@ -150,14 +150,14 @@ "eslint-config-prettier": "^10.1.8", "eslint-import-resolver-alias": "^1.1.2", "eslint-plugin-import": "^2.32.0", - "eslint-plugin-jest": "^29.15.1", + "eslint-plugin-jest": "^29.15.2", "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.4.0", + "globals": "^17.5.0", "husky": "^4.3.8", "jest": "^30.3.0", "jest-environment-jsdom": "^30.3.0", @@ -170,13 +170,13 @@ "patch-package": "^8.0.1", "postcss": "^8.5.9", "postinstall-postinstall": "^2.1.0", - "prettier": "^3.8.1", + "prettier": "^3.8.3", "rimraf": "^6.1.3", "stylelint": "^17.6.0", "stylelint-config-idiomatic-order": "^10.0.0", "stylelint-config-standard": "^40.0.0", "typescript": "^6.0.2", - "typescript-eslint": "^8.58.2", + "typescript-eslint": "^8.59.0", "workbox-cli": "^7.4.0", "yargs": "^18.0.0" }, diff --git a/yarn.lock b/yarn.lock index 3ef98d3b9a..136ea89def 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2482,100 +2482,100 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@8.58.2", "@typescript-eslint/eslint-plugin@^8.58.2": - version "8.58.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz#a6882a6a328e1259cff259fdb03184245ef06191" - integrity sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw== +"@typescript-eslint/eslint-plugin@8.59.0", "@typescript-eslint/eslint-plugin@^8.59.0": + version "8.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz#fcbe76b693ce2412410cf4d48aefd617d345f2d9" + integrity sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw== dependencies: "@eslint-community/regexpp" "^4.12.2" - "@typescript-eslint/scope-manager" "8.58.2" - "@typescript-eslint/type-utils" "8.58.2" - "@typescript-eslint/utils" "8.58.2" - "@typescript-eslint/visitor-keys" "8.58.2" + "@typescript-eslint/scope-manager" "8.59.0" + "@typescript-eslint/type-utils" "8.59.0" + "@typescript-eslint/utils" "8.59.0" + "@typescript-eslint/visitor-keys" "8.59.0" ignore "^7.0.5" natural-compare "^1.4.0" ts-api-utils "^2.5.0" -"@typescript-eslint/parser@8.58.2", "@typescript-eslint/parser@^8.58.2": - version "8.58.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.58.2.tgz#b267545e4bd515d896fe1f3a5b6f334fa6aa0026" - integrity sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg== +"@typescript-eslint/parser@8.59.0", "@typescript-eslint/parser@^8.59.0": + version "8.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.59.0.tgz#57a138280b3ceaf07904fbd62c433d5cc1ee1573" + integrity sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg== dependencies: - "@typescript-eslint/scope-manager" "8.58.2" - "@typescript-eslint/types" "8.58.2" - "@typescript-eslint/typescript-estree" "8.58.2" - "@typescript-eslint/visitor-keys" "8.58.2" + "@typescript-eslint/scope-manager" "8.59.0" + "@typescript-eslint/types" "8.59.0" + "@typescript-eslint/typescript-estree" "8.59.0" + "@typescript-eslint/visitor-keys" "8.59.0" debug "^4.4.3" -"@typescript-eslint/project-service@8.58.2": - version "8.58.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.58.2.tgz#8c980249100e21b87baba0ca10880fdf893e0a8e" - integrity sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg== +"@typescript-eslint/project-service@8.59.0": + version "8.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.59.0.tgz#914bf62069d870faa0389ffd725774a200f511bf" + integrity sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw== dependencies: - "@typescript-eslint/tsconfig-utils" "^8.58.2" - "@typescript-eslint/types" "^8.58.2" + "@typescript-eslint/tsconfig-utils" "^8.59.0" + "@typescript-eslint/types" "^8.59.0" debug "^4.4.3" -"@typescript-eslint/scope-manager@8.58.2", "@typescript-eslint/scope-manager@^8.56.0": - version "8.58.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.58.2.tgz#aa73784d78f117940e83f71705af07ba695cd60c" - integrity sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q== +"@typescript-eslint/scope-manager@8.59.0", "@typescript-eslint/scope-manager@^8.56.0": + version "8.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz#f71be268bd31da1c160815c689e4dde7c9bc9e8e" + integrity sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg== dependencies: - "@typescript-eslint/types" "8.58.2" - "@typescript-eslint/visitor-keys" "8.58.2" + "@typescript-eslint/types" "8.59.0" + "@typescript-eslint/visitor-keys" "8.59.0" -"@typescript-eslint/tsconfig-utils@8.58.2", "@typescript-eslint/tsconfig-utils@^8.58.2": - version "8.58.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.2.tgz#fa13f96432c9348bf87f6f44826def585fad7bca" - integrity sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A== +"@typescript-eslint/tsconfig-utils@8.59.0", "@typescript-eslint/tsconfig-utils@^8.59.0": + version "8.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz#1276077f5ad77e384446ea28a2474e8f8be1af41" + integrity sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg== -"@typescript-eslint/type-utils@8.58.2": - version "8.58.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.58.2.tgz#024eb1dd597f8a34cb22d8d9ab32da857bc9a817" - integrity sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg== +"@typescript-eslint/type-utils@8.59.0": + version "8.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz#2834ea3b179cedfc9244dcd4f74105a27751a439" + integrity sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg== dependencies: - "@typescript-eslint/types" "8.58.2" - "@typescript-eslint/typescript-estree" "8.58.2" - "@typescript-eslint/utils" "8.58.2" + "@typescript-eslint/types" "8.59.0" + "@typescript-eslint/typescript-estree" "8.59.0" + "@typescript-eslint/utils" "8.59.0" debug "^4.4.3" ts-api-utils "^2.5.0" -"@typescript-eslint/types@8.58.2", "@typescript-eslint/types@^8.58.2": - version "8.58.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.58.2.tgz#3ab8051de0f19a46ddefb0749d0f7d82974bd57c" - integrity sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ== +"@typescript-eslint/types@8.59.0", "@typescript-eslint/types@^8.59.0": + version "8.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.59.0.tgz#cfcc643c6e879016479775850d86d84c14492738" + integrity sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A== -"@typescript-eslint/typescript-estree@8.58.2": - version "8.58.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz#b1beb1f959385b341cc76f0aebbf028e23dfdb8b" - integrity sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw== +"@typescript-eslint/typescript-estree@8.59.0": + version "8.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz#feba58a70ab6ea7ac53a2f3ae900db28ce3454c2" + integrity sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw== dependencies: - "@typescript-eslint/project-service" "8.58.2" - "@typescript-eslint/tsconfig-utils" "8.58.2" - "@typescript-eslint/types" "8.58.2" - "@typescript-eslint/visitor-keys" "8.58.2" + "@typescript-eslint/project-service" "8.59.0" + "@typescript-eslint/tsconfig-utils" "8.59.0" + "@typescript-eslint/types" "8.59.0" + "@typescript-eslint/visitor-keys" "8.59.0" debug "^4.4.3" minimatch "^10.2.2" semver "^7.7.3" tinyglobby "^0.2.15" ts-api-utils "^2.5.0" -"@typescript-eslint/utils@8.58.2", "@typescript-eslint/utils@^8.0.0", "@typescript-eslint/utils@^8.56.0": - version "8.58.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.58.2.tgz#27165554a02d1ff57d98262fa92060498dabc8b3" - integrity sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA== +"@typescript-eslint/utils@8.59.0", "@typescript-eslint/utils@^8.0.0", "@typescript-eslint/utils@^8.56.0": + version "8.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.59.0.tgz#f50df9bd6967881ef64fba62230111153179ead5" + integrity sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g== dependencies: "@eslint-community/eslint-utils" "^4.9.1" - "@typescript-eslint/scope-manager" "8.58.2" - "@typescript-eslint/types" "8.58.2" - "@typescript-eslint/typescript-estree" "8.58.2" + "@typescript-eslint/scope-manager" "8.59.0" + "@typescript-eslint/types" "8.59.0" + "@typescript-eslint/typescript-estree" "8.59.0" -"@typescript-eslint/visitor-keys@8.58.2": - version "8.58.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz#9ed699eaa9b5720b6b6b6f9c16e6c7d4cd32b276" - integrity sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA== +"@typescript-eslint/visitor-keys@8.59.0": + version "8.59.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz#2e80de30e7e944ed4bd47d751e37dcb04db03795" + integrity sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q== dependencies: - "@typescript-eslint/types" "8.58.2" + "@typescript-eslint/types" "8.59.0" eslint-visitor-keys "^5.0.0" "@ungap/structured-clone@^1.3.0": @@ -3429,10 +3429,10 @@ camelcase@^8.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-8.0.0.tgz#c0d36d418753fb6ad9c5e0437579745c1c14a534" integrity sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA== -caniuse-lite@^1.0.30001780, caniuse-lite@^1.0.30001782: - version "1.0.30001787" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz#fd25c5e42e2d35df5c75eddda00d15d9c0c68f81" - integrity sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg== +caniuse-lite@^1.0.30001782, caniuse-lite@^1.0.30001788: + version "1.0.30001788" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz#31e97d1bfec332b3f2d7eea7781460c97629b3bf" + integrity sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ== ccount@^2.0.0: version "2.0.1" @@ -4540,10 +4540,10 @@ eslint-plugin-jest-formatting@^3.1.0: resolved "https://registry.yarnpkg.com/eslint-plugin-jest-formatting/-/eslint-plugin-jest-formatting-3.1.0.tgz#b26dd5a40f432b642dcc880021a771bb1c93dcd2" integrity sha512-XyysraZ1JSgGbLSDxjj5HzKKh0glgWf+7CkqxbTqb7zEhW7X2WHo5SBQ8cGhnszKN+2Lj3/oevBlHNbHezoc/A== -eslint-plugin-jest@^29.15.1: - version "29.15.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-29.15.1.tgz#f663f9f7903a7181efddea5a92d1d31e66362596" - integrity sha512-6BjyErCQauz3zfJvzLw/kAez2lf4LEpbHLvWBfEcG4EI0ZiRSwjoH2uZulMouU8kRkBH+S0rhqn11IhTvxKgKw== +eslint-plugin-jest@^29.15.2: + version "29.15.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-29.15.2.tgz#e4ecd1c88dfb8a62b4a0857724792c2aab7e9b6d" + integrity sha512-kEN4r9RZl1xcsb4arGq89LrcVdOUFII/JSCwtTPJyv16mDwmPrcuEQwpxqZHeINvcsd7oK5O/rhdGlxFRaZwvQ== dependencies: "@typescript-eslint/utils" "^8.0.0" @@ -5281,10 +5281,10 @@ globals@^14.0.0: resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== -globals@^17.4.0: - version "17.4.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-17.4.0.tgz#33d7d297ed1536b388a0e2f4bcd0ff19c8ff91b5" - integrity sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw== +globals@^17.5.0: + version "17.5.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-17.5.0.tgz#a82c641d898f8dfbe0e81f66fdff7d0de43f88c6" + integrity sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g== globalthis@^1.0.4: version "1.0.4" @@ -9025,10 +9025,10 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier@^3.8.1: - version "3.8.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.1.tgz#edf48977cf991558f4fcbd8a3ba6015ba2a3a173" - integrity sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg== +prettier@^3.8.3: + version "3.8.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.3.tgz#560f2de55bf01b4c0503bc629d5df99b9a1d09b0" + integrity sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw== pretty-bytes@^5.3.0: version "5.6.0" @@ -10963,15 +10963,15 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typescript-eslint@^8.58.2: - version "8.58.2" - resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.58.2.tgz#55f425fc668c2d5148f45587f2cd04532d715c6a" - integrity sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ== +typescript-eslint@^8.59.0: + version "8.59.0" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.59.0.tgz#d1cc7c63559ce7116aeb66d35ec9dbe0063379fd" + integrity sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw== dependencies: - "@typescript-eslint/eslint-plugin" "8.58.2" - "@typescript-eslint/parser" "8.58.2" - "@typescript-eslint/typescript-estree" "8.58.2" - "@typescript-eslint/utils" "8.58.2" + "@typescript-eslint/eslint-plugin" "8.59.0" + "@typescript-eslint/parser" "8.59.0" + "@typescript-eslint/typescript-estree" "8.59.0" + "@typescript-eslint/utils" "8.59.0" typescript@^6.0.2: version "6.0.2" From 5fffe925d00302d435481dc855d53dcbbd99a5ea Mon Sep 17 00:00:00 2001 From: Pontoon Date: Thu, 23 Apr 2026 14:10:34 +0000 Subject: [PATCH 36/64] Pontoon/Firefox Profiler: Update French (fr) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Théo Chevalier (fr) --- locales/fr/app.ftl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/locales/fr/app.ftl b/locales/fr/app.ftl index ddb5b9711e..9e2e5bfc72 100644 --- a/locales/fr/app.ftl +++ b/locales/fr/app.ftl @@ -815,6 +815,11 @@ TrackPower--tooltip-power-watt = { $value } W # $value (String) - the power value at this location TrackPower--tooltip-power-milliwatt = { $value } mW .label = Puissance +# This is used in the tooltip when the instant power value uses the microwatt unit. +# Variables: +# $value (String) - the power value at this location +TrackPower--tooltip-power-microwatt = { $value } μW + .label = Puissance # This is used in the tooltip when the power value uses the kilowatt unit. # Variables: # $value (String) - the power value at this location From 595ff0a5d621ba5980d02f5fa8851a7c7bb66a3a Mon Sep 17 00:00:00 2001 From: Ryan Hunt Date: Fri, 24 Apr 2026 08:29:44 -0500 Subject: [PATCH 37/64] Add a fullscreen button to the bottom box (#5605) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Nazım Can Altınova --- locales/en-US/app.ftl | 8 ++ res/img/svg/fullscreen-exit.svg | 6 ++ res/img/svg/fullscreen.svg | 6 ++ src/actions/profile-view.ts | 8 ++ src/app-logic/url-handling.ts | 13 +++- src/components/app/BottomBox.css | 19 +++++ src/components/app/BottomBox.tsx | 40 +++++++++- src/components/app/FullscreenToggleButton.tsx | 77 +++++++++++++++++++ src/reducers/url-state.ts | 11 +++ src/selectors/url-state.ts | 4 + src/types/actions.ts | 3 + src/types/state.ts | 1 + 12 files changed, 191 insertions(+), 5 deletions(-) create mode 100644 res/img/svg/fullscreen-exit.svg create mode 100644 res/img/svg/fullscreen.svg create mode 100644 src/components/app/FullscreenToggleButton.tsx diff --git a/locales/en-US/app.ftl b/locales/en-US/app.ftl index 9475a20d4c..17f323d1af 100644 --- a/locales/en-US/app.ftl +++ b/locales/en-US/app.ftl @@ -1260,6 +1260,14 @@ BottomBox--assembly-code-not-available-title = Assembly code not available BottomBox--assembly-code-not-available-text = See issue #4520 for supported scenarios and planned improvements. +# The toggle button for making the bottom box fullscreen. +BottomBox--hide-fullscreen = + .title = Exit fullscreen + +# The toggle button for making the bottom box fullscreen. +BottomBox--show-fullscreen = + .title = Fullscreen + SourceView--close-button = .title = Close the source view diff --git a/res/img/svg/fullscreen-exit.svg b/res/img/svg/fullscreen-exit.svg new file mode 100644 index 0000000000..2843b49fae --- /dev/null +++ b/res/img/svg/fullscreen-exit.svg @@ -0,0 +1,6 @@ + + + + diff --git a/res/img/svg/fullscreen.svg b/res/img/svg/fullscreen.svg new file mode 100644 index 0000000000..ce460de24e --- /dev/null +++ b/res/img/svg/fullscreen.svg @@ -0,0 +1,6 @@ + + + + diff --git a/src/actions/profile-view.ts b/src/actions/profile-view.ts index a7bf7ddc39..c61ef144af 100644 --- a/src/actions/profile-view.ts +++ b/src/actions/profile-view.ts @@ -1987,6 +1987,14 @@ export function closeBottomBox(): ThunkAction { }; } +export function toggleBottomBoxFullscreen(): ThunkAction { + return (dispatch) => { + dispatch({ + type: 'TOGGLE_BOTTOM_BOX_FULLSCREEN', + }); + }; +} + export function handleCallNodeTransformShortcut( event: React.KeyboardEvent, threadsKey: ThreadsKey, diff --git a/src/app-logic/url-handling.ts b/src/app-logic/url-handling.ts index eecd06a2be..aa6b1dad77 100644 --- a/src/app-logic/url-handling.ts +++ b/src/app-logic/url-handling.ts @@ -216,6 +216,7 @@ type Query = BaseQuery & { transforms?: string; sourceViewIndex?: number; assemblyView?: string; + bottomFullscreen?: boolean; // StackChart specific showUserTimings?: null | undefined; @@ -352,8 +353,12 @@ export function getQueryStringFromUrlState(urlState: UrlState): string { 'timing' ? undefined : urlState.profileSpecific.lastSelectedCallTreeSummaryStrategy; - const { sourceView, assemblyView, isBottomBoxOpenPerPanel } = - urlState.profileSpecific; + const { + sourceView, + assemblyView, + isBottomBoxOpenPerPanel, + isBottomBoxFullscreen, + } = urlState.profileSpecific; if (isBottomBoxOpenPerPanel[selectedTab]) { if (sourceView.sourceIndex !== null) { @@ -365,6 +370,9 @@ export function getQueryStringFromUrlState(urlState: UrlState): string { nativeSymbols[currentNativeSymbol] ); } + if (isBottomBoxFullscreen) { + query.bottomFullscreen = true; + } } break; } @@ -593,6 +601,7 @@ export function stateFromLocation( sourceView, assemblyView, isBottomBoxOpenPerPanel, + isBottomBoxFullscreen: query.bottomFullscreen || false, timelineType: validateTimelineType(query.timelineType), showJsTracerSummary: query.summary === undefined ? false : true, globalTrackOrder: convertGlobalTrackOrderFromString( diff --git a/src/components/app/BottomBox.css b/src/components/app/BottomBox.css index dc80d0345b..16676702d2 100644 --- a/src/components/app/BottomBox.css +++ b/src/components/app/BottomBox.css @@ -2,6 +2,15 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +.bottom-box-fullscreen { + position: fixed; + z-index: 100; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + .bottom-box-pane { --internal-sourceview-background-color: var(--grey-20); --internal-close-icon: url(../../../res/img/svg/close-dark.svg); @@ -73,6 +82,8 @@ .bottom-close-button, .bottom-assembly-button, +.bottom-fullscreen-hide-button, +.bottom-fullscreen-show-button, .bottom-prev-button, .bottom-next-button { width: 24px; @@ -101,6 +112,14 @@ background-image: var(--internal-assembly-icon); } +.bottom-fullscreen-show-button { + background-image: url(firefox-profiler-res/img/svg/fullscreen.svg); +} + +.bottom-fullscreen-hide-button { + background-image: url(firefox-profiler-res/img/svg/fullscreen-exit.svg); +} + .codeLoadingOverlay, .sourceCodeErrorOverlay, .assemblyCodeErrorOverlay { diff --git a/src/components/app/BottomBox.tsx b/src/components/app/BottomBox.tsx index 96558f3b9c..8e103ce6d3 100644 --- a/src/components/app/BottomBox.tsx +++ b/src/components/app/BottomBox.tsx @@ -8,6 +8,7 @@ import classNames from 'classnames'; import { SourceView } from '../shared/SourceView'; import { AssemblyView } from '../shared/AssemblyView'; +import { FullscreenToggleButton } from './FullscreenToggleButton'; import { AssemblyViewToggleButton } from './AssemblyViewToggleButton'; import { AssemblyViewNativeSymbolNavigator } from './AssemblyViewNativeSymbolNavigator'; import { IonGraphView } from '../shared/IonGraphView'; @@ -22,9 +23,13 @@ import { getAssemblyViewScrollGeneration, getAssemblyViewScrollToInstructionAddress, getAssemblyViewHighlightedInstruction, + getIsBottomBoxFullscreen, } from 'firefox-profiler/selectors/url-state'; +import { + closeBottomBox, + toggleBottomBoxFullscreen, +} from 'firefox-profiler/actions/profile-view'; import { selectedThreadSelectors } from 'firefox-profiler/selectors/per-thread'; -import { closeBottomBox } from 'firefox-profiler/actions/profile-view'; import { parseFileNameFromSymbolication } from 'firefox-profiler/utils/special-paths'; import { getSourceViewCode, @@ -53,6 +58,7 @@ import { Localized } from '@fluent/react'; import './BottomBox.css'; type StateProps = { + readonly isFullscreen: boolean; readonly sourceViewFile: string | null; readonly sourceViewCode: SourceCodeStatus | void; readonly sourceViewScrollGeneration: number; @@ -71,6 +77,7 @@ type StateProps = { type DispatchProps = { readonly closeBottomBox: typeof closeBottomBox; + readonly toggleBottomBoxFullscreen: typeof toggleBottomBoxFullscreen; }; type Props = ConnectedProps<{}, StateProps, DispatchProps>; @@ -153,12 +160,31 @@ class BottomBoxImpl extends React.PureComponent { _sourceView = React.createRef(); _assemblyView = React.createRef(); + override componentDidMount() { + document.addEventListener('keydown', this._onKeyDown); + } + + override componentWillUnmount() { + document.removeEventListener('keydown', this._onKeyDown); + } + + _onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape' && this.props.isFullscreen) { + this.props.toggleBottomBoxFullscreen(); + } + }; + _onClickCloseButton = () => { this.props.closeBottomBox(); + // Close the fullscreen if we're closing the bottom box + if (this.props.isFullscreen) { + this.props.toggleBottomBoxFullscreen(); + } }; override render() { const { + isFullscreen, sourceViewFile, sourceViewCode, globalLineTimings, @@ -200,6 +226,7 @@ class BottomBoxImpl extends React.PureComponent {
{assemblyViewIsOpen ? : null} +