From d07c28b4a577a9b435eee442ebaf9ebfe3550ad7 Mon Sep 17 00:00:00 2001 From: Tooru Fujisawa Date: Tue, 23 Dec 2025 23:20:12 +0900 Subject: [PATCH 1/2] Support negative timestamp --- .../__snapshots__/MenuButtons.test.tsx.snap | 6 +++--- src/utils/format-numbers.ts | 14 +++++++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/test/components/__snapshots__/MenuButtons.test.tsx.snap b/src/test/components/__snapshots__/MenuButtons.test.tsx.snap index 944baae3a4..1a03b0ef76 100644 --- a/src/test/components/__snapshots__/MenuButtons.test.tsx.snap +++ b/src/test/components/__snapshots__/MenuButtons.test.tsx.snap @@ -2187,7 +2187,7 @@ exports[`app/MenuButtons matches the snapshot for the menu buttons and class="menuButtonsDownloadSize" > ( - 1.58 kB + 1.59 kB ) @@ -2305,7 +2305,7 @@ exports[`app/MenuButtons matches the snapshot for the opened panel for class="menuButtonsDownloadSize" > ( - 1.56 kB + 1.57 kB ) @@ -2418,7 +2418,7 @@ exports[`app/MenuButtons matches the snapshot for the opened panel for class="menuButtonsDownloadSize" > ( - 1.58 kB + 1.59 kB ) diff --git a/src/utils/format-numbers.ts b/src/utils/format-numbers.ts index 77ecb185a7..3698200890 100644 --- a/src/utils/format-numbers.ts +++ b/src/utils/format-numbers.ts @@ -425,7 +425,19 @@ export function formatTimestamp( maxFractionalDigits: number = 3, // precision is the minimum required precision. precision: Milliseconds = Infinity -) { +): string { + if (time < 0) { + return ( + '-' + + formatTimestamp( + Math.abs(time), + significantDigits, + maxFractionalDigits, + precision + ) + ); + } + if (precision !== Infinity) { // Round the values to display nicer numbers when the extra precision // isn't useful. (eg. show 3h52min10s instead of 3h52min14s) From 9faf5e8609acd08dd7fe3d1fab6855dc435cddab Mon Sep 17 00:00:00 2001 From: Tooru Fujisawa Date: Wed, 24 Dec 2025 22:53:33 +0900 Subject: [PATCH 2/2] Make it possible to override the zeroAt value --- locales/en-US/app.ftl | 8 ++++ src/actions/profile-view.ts | 7 +++ src/components/app/MenuButtons/index.css | 9 ++++ src/components/app/MenuButtons/index.tsx | 44 ++++++++++++++++++- src/components/shared/MarkerContextMenu.css | 4 ++ src/components/shared/MarkerContextMenu.tsx | 17 +++++++ src/reducers/profile-view.ts | 10 +++++ src/selectors/profile.ts | 20 ++++++++- src/test/components/MarkerTable.test.tsx | 17 +++++++ .../__snapshots__/MarkerChart.test.tsx.snap | 14 ++++++ src/types/actions.ts | 4 ++ src/types/state.ts | 1 + 12 files changed, 152 insertions(+), 3 deletions(-) diff --git a/locales/en-US/app.ftl b/locales/en-US/app.ftl index 9eb89ea769..8d51ab50bf 100644 --- a/locales/en-US/app.ftl +++ b/locales/en-US/app.ftl @@ -438,6 +438,8 @@ MarkerContextMenu--end-selection-at-marker-start = End selection at marker’s start MarkerContextMenu--end-selection-at-marker-end = End selection at marker’s end +MarkerContextMenu--override-zero-at-marker-start = + Override zero at marker’s start MarkerContextMenu--copy-description = Copy description MarkerContextMenu--copy-call-stack = Copy call stack MarkerContextMenu--copy-url = Copy URL @@ -681,6 +683,12 @@ MenuButtons--publish--download = Download MenuButtons--publish--compressing = Compressing… MenuButtons--publish--error-while-compressing = Error while compressing, try unchecking some checkboxes to reduce the profile size. +# This string is the button's label, where the button is shown when the "zero" +# point of the timeline is overridden. +# Variables: +# $zeroAt (String) - The timestamp of the overridden "zero" +MenuButtons--zero-at = Zero at { $zeroAt } + ## NetworkSettings ## This is used in the network chart. diff --git a/src/actions/profile-view.ts b/src/actions/profile-view.ts index 47c50d4017..a87ade1316 100644 --- a/src/actions/profile-view.ts +++ b/src/actions/profile-view.ts @@ -1905,6 +1905,13 @@ export function changeMouseTimePosition( }; } +export function overrideZeroAt(zeroAt: Milliseconds | null): Action { + return { + type: 'OVERRIDE_ZERO_AT', + zeroAt, + }; +} + export function changeTableViewOptions( tab: TabSlug, tableViewOptions: TableViewOptions diff --git a/src/components/app/MenuButtons/index.css b/src/components/app/MenuButtons/index.css index 88f34acab1..dbc8259ab7 100644 --- a/src/components/app/MenuButtons/index.css +++ b/src/components/app/MenuButtons/index.css @@ -111,6 +111,15 @@ background-image: url(../../../../res/img/svg/maximize-dark-12.svg); } +.menuButtonsResetZeroAtButton::before { + background-image: url(../../../../res/img/svg/undo-dark-12.svg); +} + +.menuButtonsZeroAtTimestamp { + font-weight: bold; + margin-inline-start: 0.3em; +} + .profileInfoUploadedActions { padding: 8px 0 8px 40px; /* The 40px padding leaves the room for the cloud image */ border-bottom: 1px solid rgb(0 0 0 / 0.05); diff --git a/src/components/app/MenuButtons/index.tsx b/src/components/app/MenuButtons/index.tsx index fc2d965154..7fead3fe94 100644 --- a/src/components/app/MenuButtons/index.tsx +++ b/src/components/app/MenuButtons/index.tsx @@ -11,7 +11,10 @@ import classNames from 'classnames'; import { Localized } from '@fluent/react'; import explicitConnect from 'firefox-profiler/utils/connect'; -import { getProfileRootRange } from 'firefox-profiler/selectors/profile'; +import { + getProfileRootRange, + getOverriddenZeroAtTimestamp, +} from 'firefox-profiler/selectors/profile'; import { getDataSource, getProfileUrl, @@ -36,6 +39,7 @@ import { dismissNewlyPublished, profileRemotelyDeleted, } from 'firefox-profiler/actions/app'; +import { overrideZeroAt } from 'firefox-profiler/actions/profile-view'; import { getAbortFunction, @@ -72,12 +76,14 @@ type StateProps = { readonly hasPrePublishedState: boolean; readonly abortFunction: () => void; readonly currentProfileUploadedInformation: UploadedProfileInformation | null; + readonly getOverriddenZeroAtTimestamp: string | null; }; type DispatchProps = { readonly dismissNewlyPublished: typeof dismissNewlyPublished; readonly revertToPrePublishedState: typeof revertToPrePublishedState; readonly profileRemotelyDeleted: typeof profileRemotelyDeleted; + readonly overrideZeroAt: typeof overrideZeroAt; }; type Props = ConnectedProps; @@ -130,6 +136,11 @@ class MenuButtonsImpl extends React.PureComponent { }); }; + _showZeroAtMenu = () => { + const { overrideZeroAt } = this.props; + overrideZeroAt(null); + }; + _renderUploadedProfileActions( currentProfileUploadedInformation: UploadedProfileInformation ) { @@ -309,6 +320,34 @@ class MenuButtonsImpl extends React.PureComponent { ) : null; } + _renderZeroAt() { + const { getOverriddenZeroAtTimestamp } = this.props; + if (getOverriddenZeroAtTimestamp === null) { + return null; + } + + return ( + + ); + } + _renderRevertProfile() { const { hasPrePublishedState, revertToPrePublishedState } = this.props; if (!hasPrePublishedState) { @@ -330,6 +369,7 @@ class MenuButtonsImpl extends React.PureComponent { override render() { return ( <> + {this._renderZeroAt()} {this._renderRevertProfile()} {this._renderMetaInfoButton()} {this._renderPublishPanel()} @@ -360,11 +400,13 @@ export const MenuButtons = explicitConnect( abortFunction: getAbortFunction(state), currentProfileUploadedInformation: getCurrentProfileUploadedInformation(state), + getOverriddenZeroAtTimestamp: getOverriddenZeroAtTimestamp(state), }), mapDispatchToProps: { dismissNewlyPublished, revertToPrePublishedState, profileRemotelyDeleted, + overrideZeroAt, }, component: MenuButtonsImpl, } diff --git a/src/components/shared/MarkerContextMenu.css b/src/components/shared/MarkerContextMenu.css index 851877cfac..7487c4702f 100644 --- a/src/components/shared/MarkerContextMenu.css +++ b/src/components/shared/MarkerContextMenu.css @@ -31,6 +31,10 @@ background-image: url(../../../res/img/svg/end-selection-at-marker-end.svg); } +.markerContextMenuIconOverrideZeroAtMarkerStart { + background-image: url(../../../res/img/svg/start-selection-at-marker-start.svg); +} + .markerContextMenuIconCopyDescription { background-image: url(../../../res/img/svg/copy-dark.svg); } diff --git a/src/components/shared/MarkerContextMenu.tsx b/src/components/shared/MarkerContextMenu.tsx index c1243223d8..d08ec5144f 100644 --- a/src/components/shared/MarkerContextMenu.tsx +++ b/src/components/shared/MarkerContextMenu.tsx @@ -13,6 +13,7 @@ import { setContextMenuVisibility, updatePreviewSelection, selectTrackFromTid, + overrideZeroAt, } from 'firefox-profiler/actions/profile-view'; import { getPreviewSelection, @@ -65,6 +66,7 @@ type DispatchProps = { readonly updatePreviewSelection: typeof updatePreviewSelection; readonly setContextMenuVisibility: typeof setContextMenuVisibility; readonly selectTrackFromTid: typeof selectTrackFromTid; + readonly overrideZeroAt: typeof overrideZeroAt; }; type Props = ConnectedProps; @@ -141,6 +143,11 @@ class MarkerContextMenuImpl extends PureComponent { }); }; + overrideZeroAtMarkerStart = () => { + const { marker, overrideZeroAt } = this.props; + overrideZeroAt(marker.start); + }; + _isZeroDurationMarker(marker: Marker | null): boolean { return !marker || marker.end === null; } @@ -482,6 +489,15 @@ class MarkerContextMenuImpl extends PureComponent { )} +
+ + + + + Override zero at marker’s start + + +
@@ -539,6 +555,7 @@ const MarkerContextMenu = explicitConnect({ updatePreviewSelection, setContextMenuVisibility, selectTrackFromTid, + overrideZeroAt, }, component: MarkerContextMenuImpl, }); diff --git a/src/reducers/profile-view.ts b/src/reducers/profile-view.ts index 26465510fd..8c9aa7f5dc 100644 --- a/src/reducers/profile-view.ts +++ b/src/reducers/profile-view.ts @@ -824,6 +824,15 @@ const mouseTimePosition: Reducer = ( } }; +const overrideZeroAt: Reducer = (state = null, action) => { + switch (action.type) { + case 'OVERRIDE_ZERO_AT': + return action.zeroAt; + default: + return state; + } +}; + /** * Provide a mechanism to wrap the reducer in a special function that can reset * the state to the default values. This is useful when viewing multiple profiles @@ -864,6 +873,7 @@ const profileViewReducer: Reducer = wrapReducerInResetter( hoveredMarker, mouseTimePosition, perTab: tableViewOptionsPerTab, + overrideZeroAt, }), profile, globalTracks, diff --git a/src/selectors/profile.ts b/src/selectors/profile.ts index d761943a3f..59ca5d47ff 100644 --- a/src/selectors/profile.ts +++ b/src/selectors/profile.ts @@ -6,6 +6,7 @@ import * as Tracks from '../profile-logic/tracks'; import * as CPU from '../profile-logic/cpu'; import * as UrlState from './url-state'; import { ensureExists } from '../utils/types'; +import { formatTimestamp } from '../utils/format-numbers'; import { accumulateCounterSamples, extractProfileFilterPageData, @@ -96,8 +97,23 @@ export const getScrollToSelectionGeneration: Selector = (state) => getProfileViewOptions(state).scrollToSelectionGeneration; export const getFocusCallTreeGeneration: Selector = (state) => getProfileViewOptions(state).focusCallTreeGeneration; -export const getZeroAt: Selector = (state) => - getProfileRootRange(state).start; +export const getZeroAt: Selector = (state) => { + const viewOptions = getProfileViewOptions(state); + if (viewOptions.overrideZeroAt !== null) { + return viewOptions.overrideZeroAt; + } + return getProfileRootRange(state).start; +}; +export const getOverriddenZeroAtTimestamp: Selector = ( + state +) => { + const viewOptions = getProfileViewOptions(state); + if (viewOptions.overrideZeroAt === null) { + return null; + } + const offset = viewOptions.overrideZeroAt - getProfileRootRange(state).start; + return formatTimestamp(offset); +}; export const getProfileTimelineUnit: Selector = (state) => { const { sampleUnits } = getProfile(state).meta; return sampleUnits ? sampleUnits.time : 'ms'; diff --git a/src/test/components/MarkerTable.test.tsx b/src/test/components/MarkerTable.test.tsx index 2d381ff82a..19a17bf5f2 100644 --- a/src/test/components/MarkerTable.test.tsx +++ b/src/test/components/MarkerTable.test.tsx @@ -239,6 +239,23 @@ describe('MarkerTable', function () { ); }); + it('can override the zero at timing using the context menu', () => { + const { getRowElement } = setup(); + + const startNode = ensureExists( + getRowElement(/setTimeout/).querySelector('.start') + ); + expect(startNode).toHaveTextContent('0.153s'); + + fireFullContextMenu(getRowElement(/setTimeout/) as HTMLElement); + fireFullClick(screen.getByText('Override zero at marker’s start')); + + const startNode2 = ensureExists( + getRowElement(/setTimeout/).querySelector('.start') + ); + expect(startNode2).toHaveTextContent('0s'); + }); + describe('EmptyReasons', () => { it('shows reasons when a profile has no non-network markers', () => { const { profile } = getProfileFromTextSamples('A'); // Just a simple profile without any marker. diff --git a/src/test/components/__snapshots__/MarkerChart.test.tsx.snap b/src/test/components/__snapshots__/MarkerChart.test.tsx.snap index 68263b885b..2ef2242237 100644 --- a/src/test/components/__snapshots__/MarkerChart.test.tsx.snap +++ b/src/test/components/__snapshots__/MarkerChart.test.tsx.snap @@ -133,6 +133,20 @@ exports[`MarkerChart context menus displays when right clicking on a marker 1`]
+ +