Skip to content

Comments

Persist the selected marker in the url#5847

Open
fatadel wants to merge 11 commits intofirefox-devtools:mainfrom
fatadel:persist-selected-marker-5241
Open

Persist the selected marker in the url#5847
fatadel wants to merge 11 commits intofirefox-devtools:mainfrom
fatadel:persist-selected-marker-5241

Conversation

@fatadel
Copy link
Contributor

@fatadel fatadel commented Feb 17, 2026

Closes #5241.

This PR adds a possibility to persist the selected marker in the URL, so that, when sharing profiles, the view of the two users matches as much as possible.

Before

https://profiler.firefox.com/public/707b9xarf5kkjg49pn1rrgs8fg20mrz554wdc1g/marker-chart/?globalTrackOrder=fwi0we&thread=0&v=13

After

https://deploy-preview-5847--perf-html.netlify.app/public/707b9xarf5kkjg49pn1rrgs8fg20mrz554wdc1g/marker-chart/?globalTrackOrder=fwi0we&marker=5173&thread=0&v=14

As you may notice, now the URL has marker= query param that is responsible for persisting the selected marker. The URL version is bumped to version 14.

@codecov
Copy link

codecov bot commented Feb 17, 2026

Codecov Report

❌ Patch coverage is 57.65766% with 47 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.46%. Comparing base (37b31b0) to head (2990331).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
src/components/marker-chart/Canvas.tsx 36.17% 30 Missing ⚠️
src/components/shared/chart/Canvas.tsx 54.05% 17 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5847      +/-   ##
==========================================
- Coverage   85.56%   85.46%   -0.11%     
==========================================
  Files         319      319              
  Lines       31420    31525     +105     
  Branches     8661     8711      +50     
==========================================
+ Hits        26885    26943      +58     
- Misses       4104     4151      +47     
  Partials      431      431              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@fatadel fatadel changed the title Persist selected markers in the url Persist the selected marker in the url Feb 17, 2026
@fatadel fatadel marked this pull request as ready for review February 17, 2026 13:52
@fatadel fatadel requested review from canova and mstange February 17, 2026 13:52
Copy link
Member

@canova canova left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR!

I would prefer to merge the 2 dispatches inside finalizeFullProfileView, see my comment below.

I noticed that when you scroll down the marker chart and select the marker, it doesn't scroll enough to keep the marker in the viewport. For example:
https://deploy-preview-5847--perf-html.netlify.app/public/707b9xarf5kkjg49pn1rrgs8fg20mrz554wdc1g/marker-chart/?globalTrackOrder=fwi0we&marker=46160&thread=0&v=14
Can you look into that?

I couldn't finish my full review yet (for example getTooltipPosition etc.), but sending my initial findings beforehand.

Comment on lines 376 to 386
// Initialize selected markers from URL state if present
if (hasUrlInfo) {
const selectedMarkers = getSelectedMarkers(getState());
// Dispatch marker selection for each thread that has a marker in URL
for (const [threadsKey, markerIndex] of Object.entries(selectedMarkers)) {
if (markerIndex !== null) {
dispatch(changeSelectedMarker(threadsKey, markerIndex));
}
}
}
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is there a reason why this is not merged with VIEW_FULL_PROFILE? Currently, we only have single dispatch inside finalizeFullProfileView and we put all the state update there to update it once. We should ideally keep it that way. We can update the relevant reducers to update the state on VIEW_FULL_PROFILE. How does the transform url param handles this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, what you say is 100% valid, I kinda missed that logic. I've made a commit now for this specific piece of feedback - could you pls have a quick look if I got you correctly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(resolved irl)

override componentDidMount() {
// Initialize selectedItem from props on mount if provided
// Use requestAnimationFrame to ensure the canvas is fully laid out
if (this.props.selectedItem !== undefined) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: If you assign selectedItem to a const variable, then you won't have to assert below with !.

For example:

const { hoveredItem } = this.props;
if (selectedItem !== undefined) {
...
this._syncSelectedItemFromProp(selectedItem);

It's more accurate type-wise because typescript now knows that it can't be changed in between componentDidMount and requestAnimationFrame callback.

But apart from that, I don't think window.requestAnimationFrame is great here. Can you tell me why it was needed? Maybe we can find a different way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the nit:

It is usually true, yes, but not here, because there are different scopes accessing this.props.selectedItem. One of them attaches the callback, another access is inside the callback itself. So, removing the exclamation mark will yield TS2345: Argument of type 'Item | null | undefined' is not assignable to parameter of type 'Item | null'. Type 'undefined' is not assignable to type 'Item | null'.


Now regarding the usage of window.requestAnimationFrame: to be honest, I did not have it in the first place too, but I've asked claude to review my code before submitting. The feedback from claude was the following:

The requestAnimationFrame is necessary here. Inside _syncSelectedItemFromProp, we call this._canvas.getBoundingClientRect():

  const canvasRect = this._canvas.getBoundingClientRect();                                                                                                                
  const pageX = canvasRect.left + window.scrollX + tooltipPosition.offsetX;                                                                                               
  const pageY = canvasRect.top + window.scrollY + tooltipPosition.offsetY;                                                                                                

componentDidMount fires right after the component's DOM nodes are inserted into the document, but before the browser has performed layout. At that moment, the canvas exists in the DOM but may not yet have its final size and position calculated — getBoundingClientRect() could return zeros or stale values.

requestAnimationFrame delays the call until just before the next paint, by which point the browser has done a layout pass and getBoundingClientRect() returns accurate values.

Without it, on initial load the tooltip would likely appear at position (0, 0) or some incorrect location because the canvas's bounding rect hasn't been computed yet.

Why this only affects componentDidMount and not componentDidUpdate: by the time componentDidUpdate runs, the component has already been rendered at least once and layout has occurred, so getBoundingClientRect() is already reliable.

It seemed as valid feedback to me. But if you don't agree - let's discuss.

Copy link
Member

@canova canova Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is usually true, yes, but not here, because there are different scopes accessing this.props.selectedItem. One of them attaches the callback, another access is inside the callback itself. So, removing the exclamation mark will yield TS2345: Argument of type 'Item | null | undefined' is not assignable to parameter of type 'Item | null'. Type 'undefined' is not assignable to type 'Item | null'.

That's why I suggested to assign the selectedItem to another constant variable. (ah now I see that in my example I used hoveredItem instead of selectedItem. So the full code would be:

const { selectedItem } = this.props;
if (selectedItem !== undefined) {
  window.requestAnimationFrame(() => {
    this._syncSelectedItemFromProp(selectedItem);
  });
}

And thinking about it, this callback has a bug because of it. First we check this.props.selectedItem to see if it's not undefined. Then we use it inside this callback, what happens if the value of it is changed in between?


For the rAF, there is really no guarantee that the Viewport update will happen in the next rAF callback. So it looks a bit racy to me.

So I would probably:

  1. In componentDidMount: Call _syncSelectedItemFromProp directly, but only if containerWidth !== 0. If it's still 0, skip it, componentDidUpdate will pick it up.
  2. In componentDidUpdate: Add a guard for when prevProps.containerWidth === 0 && this.props.containerWidth !== 0 && this.props.selectedItem !== undefined.

This will remove the rAF and it will make sure that the sync happens after the layout. But I haven't tested it fully. Let me know if you think otherwise!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, I've rewritten this piece of code.

}

// Parse the selected marker for the current thread
const selectedMarkers: { [key: string]: MarkerIndex | null } = {};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: We can use the SelectedMarkersPerThread type here directly.

@fatadel fatadel force-pushed the persist-selected-marker-5241 branch 2 times, most recently from 14671c0 to fff55d5 Compare February 19, 2026 15:55
@fatadel fatadel marked this pull request as draft February 20, 2026 14:19
@fatadel
Copy link
Contributor Author

fatadel commented Feb 23, 2026

@canova Thank you very much for the valuable feedback! I think I went thru all the points you've raised and now this PR should be ready for another review :)

@fatadel fatadel marked this pull request as ready for review February 23, 2026 15:24
@fatadel fatadel force-pushed the persist-selected-marker-5241 branch from 323de83 to 2990331 Compare February 23, 2026 15:26
@fatadel fatadel requested a review from canova February 23, 2026 15:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Persist the selected marker in the url

2 participants