diff --git a/profiler-cli/schemas.txt b/profiler-cli/schemas.txt index 302f8f7d61..9aff7cae56 100644 --- a/profiler-cli/schemas.txt +++ b/profiler-cli/schemas.txt @@ -168,6 +168,7 @@ profiler-cli thread markers --json type: "thread-markers", threadHandle, friendlyThreadName, totalMarkerCount, filteredMarkerCount, + fullRangeMarkerCount?, byType: [{ markerName, count, isInterval, durationStats?: { min, max, avg, median, p95, p99 }, diff --git a/profiler-cli/src/formatters.ts b/profiler-cli/src/formatters.ts index f34eaa7021..a3f0c08fc3 100644 --- a/profiler-cli/src/formatters.ts +++ b/profiler-cli/src/formatters.ts @@ -1124,8 +1124,14 @@ export function formatThreadMarkersResult( ? ` (filtered from ${result.totalMarkerCount})` : ''; + // When zoomed, show the in-view count against this thread's full-range total. + const zoomSuffix = + result.fullRangeMarkerCount !== undefined + ? ` in view (of ${result.fullRangeMarkerCount} in the full range)` + : ''; + lines.push( - `Markers in thread ${result.threadHandle} (${result.friendlyThreadName}) — ${result.filteredMarkerCount} markers${filterSuffix}` + `Markers in thread ${result.threadHandle} (${result.friendlyThreadName}) — ${result.filteredMarkerCount} markers${filterSuffix}${zoomSuffix}` ); lines.push('Legend: ✓ = has stack trace, ✗ = no stack trace\n'); diff --git a/profiler-cli/src/test/unit/marker-formatting.test.ts b/profiler-cli/src/test/unit/marker-formatting.test.ts index 283acc8723..d45353e509 100644 --- a/profiler-cli/src/test/unit/marker-formatting.test.ts +++ b/profiler-cli/src/test/unit/marker-formatting.test.ts @@ -148,3 +148,24 @@ describe('formatThreadMarkersResult flat list mode', function () { expect(output).not.toContain('By Category'); }); }); + +describe('formatThreadMarkersResult zoom baseline', function () { + it('notes the full-range total when zoomed', function () { + const result = makeResult({ + filteredMarkerCount: 3, + totalMarkerCount: 3, + fullRangeMarkerCount: 42, + }); + + const output = formatThreadMarkersResult(result); + expect(output).toContain('3 markers in view (of 42 in the full range)'); + }); + + it('omits the full-range note when not zoomed', function () { + const result = makeResult({ filteredMarkerCount: 3, totalMarkerCount: 3 }); + + const output = formatThreadMarkersResult(result); + expect(output).not.toContain('in the full range'); + expect(output).toContain('3 markers'); + }); +}); diff --git a/src/profile-query/formatters/marker-info.ts b/src/profile-query/formatters/marker-info.ts index 530c1fb164..ce1c2614fa 100644 --- a/src/profile-query/formatters/marker-info.ts +++ b/src/profile-query/formatters/marker-info.ts @@ -2,7 +2,10 @@ * 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 { getSelectedThreadIndexes } from 'firefox-profiler/selectors/url-state'; +import { + getSelectedThreadIndexes, + getAllCommittedRanges, +} from 'firefox-profiler/selectors/url-state'; import { getProfile, getCategories, @@ -702,12 +705,20 @@ export function collectThreadMarkers( const markerSchemaByName = getMarkerSchemaByName(state); const stringTable = getStringTable(state); - // Get marker indexes - use search-filtered if search is active, otherwise all markers + // Get marker indexes scoped to the committed (zoom) range. When a search is + // active we use the search-filtered set, which is itself built on top of the + // committed-range-filtered indexes, so both paths respect the current zoom. const originalCount = - threadSelectors.getFullMarkerListIndexes(state).length; + threadSelectors.getCommittedRangeFilteredMarkerIndexes(state).length; let filteredIndexes = searchString ? threadSelectors.getSearchFilteredMarkerIndexes(state) - : threadSelectors.getFullMarkerListIndexes(state); + : threadSelectors.getCommittedRangeFilteredMarkerIndexes(state); + + // When zoomed, show this thread's marker count over the full time range. + const isZoomed = getAllCommittedRanges(state).length > 0; + const fullRangeMarkerCount = isZoomed + ? threadSelectors.getFullMarkerListIndexes(state).length + : undefined; // Apply all marker filters filteredIndexes = applyMarkerFilters( @@ -864,6 +875,7 @@ export function collectThreadMarkers( friendlyThreadName, totalMarkerCount: originalCount, filteredMarkerCount: filteredIndexes.length, + fullRangeMarkerCount, filters, byType, byCategory, diff --git a/src/profile-query/types.ts b/src/profile-query/types.ts index 536c6d908f..3fba1cdf44 100644 --- a/src/profile-query/types.ts +++ b/src/profile-query/types.ts @@ -570,6 +570,7 @@ export type ThreadMarkersResult = { friendlyThreadName: string; totalMarkerCount: number; filteredMarkerCount: number; + fullRangeMarkerCount?: number; filters?: { searchString?: string; minDuration?: number; diff --git a/src/test/unit/profile-query/profile-querier.test.ts b/src/test/unit/profile-query/profile-querier.test.ts index 062a1fbf43..81cb904e42 100644 --- a/src/test/unit/profile-query/profile-querier.test.ts +++ b/src/test/unit/profile-query/profile-querier.test.ts @@ -694,4 +694,63 @@ describe('ProfileQuerier', function () { expect(zoomed.networkActivity!.inFlightMs).toBeLessThan(fullInFlight); }); }); + + describe('threadMarkers', function () { + function querierWithMarkers() { + const profile = getProfileWithMarkers([ + ['Alpha', 10, null, { type: 'tracing', category: 'Test' }], + ['Beta', 20, null, { type: 'tracing', category: 'Test' }], + ['Gamma', 30, null, { type: 'tracing', category: 'Test' }], + ['Delta', 40, null, { type: 'tracing', category: 'Test' }], + ]); + const store = storeWithProfile(profile); + const rootRange = getProfileRootRange(store.getState()); + return { querier: new ProfileQuerier(store, rootRange), rootRange }; + } + + it('restricts the default marker list to the committed (zoom) range', async function () { + const { querier, rootRange } = querierWithMarkers(); + + const full = await querier.threadMarkers('t-0'); + expect(full.totalMarkerCount).toBe(4); + expect(full.filteredMarkerCount).toBe(4); + // Not zoomed: no full-range baseline is reported. + expect(full.fullRangeMarkerCount).toBeUndefined(); + + // Zoom to a window that only contains the marker at 20ms. + const startName = querier._timestampManager.nameForTimestamp( + rootRange.start + 2 + ); + const endName = querier._timestampManager.nameForTimestamp( + rootRange.start + 18 + ); + await querier.pushViewRange(`${startName},${endName}`); + + const zoomed = await querier.threadMarkers('t-0'); + expect(zoomed.totalMarkerCount).toBe(1); + expect(zoomed.filteredMarkerCount).toBe(1); + // Zoomed: the whole-profile baseline is surfaced alongside the in-view count. + expect(zoomed.fullRangeMarkerCount).toBe(4); + const zoomedNames = zoomed.byType.map((t) => t.markerName); + expect(zoomedNames).toContain('Beta'); + expect(zoomedNames).not.toContain('Alpha'); + expect(zoomedNames).not.toContain('Delta'); + }); + + it('restricts the --list output to the committed (zoom) range', async function () { + const { querier, rootRange } = querierWithMarkers(); + + const startName = querier._timestampManager.nameForTimestamp( + rootRange.start + 2 + ); + const endName = querier._timestampManager.nameForTimestamp( + rootRange.start + 18 + ); + await querier.pushViewRange(`${startName},${endName}`); + + const zoomed = await querier.threadMarkers('t-0', { list: true }); + const listedNames = zoomed.flatMarkers!.map((m) => m.name); + expect(listedNames).toEqual(['Beta']); + }); + }); });