Persist the selected marker in the url#5847
Persist the selected marker in the url#5847fatadel wants to merge 7 commits intofirefox-devtools:mainfrom
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5847 +/- ##
==========================================
- Coverage 85.56% 85.47% -0.09%
==========================================
Files 319 319
Lines 31411 31496 +85
Branches 8670 8601 -69
==========================================
+ Hits 26876 26921 +45
- Misses 4104 4144 +40
Partials 431 431 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
canova
left a comment
There was a problem hiding this comment.
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.
src/actions/receive-profile.ts
Outdated
| // 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)); | ||
| } | ||
| } | ||
| } | ||
| }; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
| 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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
requestAnimationFrameis necessary here. Inside_syncSelectedItemFromProp, we callthis._canvas.getBoundingClientRect():const canvasRect = this._canvas.getBoundingClientRect(); const pageX = canvasRect.left + window.scrollX + tooltipPosition.offsetX; const pageY = canvasRect.top + window.scrollY + tooltipPosition.offsetY;
componentDidMountfires 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.
requestAnimationFramedelays the call until just before the next paint, by which point the browser has done a layout pass andgetBoundingClientRect()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
componentDidMountand notcomponentDidUpdate: by the timecomponentDidUpdateruns, the component has already been rendered at least once and layout has occurred, sogetBoundingClientRect()is already reliable.
It seemed as valid feedback to me. But if you don't agree - let's discuss.
There was a problem hiding this comment.
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 yieldTS2345: 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:
- In componentDidMount: Call _syncSelectedItemFromProp directly, but only if
containerWidth !== 0. If it's still 0, skip it, componentDidUpdate will pick it up. - 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!
| } | ||
|
|
||
| // Parse the selected marker for the current thread | ||
| const selectedMarkers: { [key: string]: MarkerIndex | null } = {}; |
There was a problem hiding this comment.
Nit: We can use the SelectedMarkersPerThread type here directly.
67e8e0b to
14671c0
Compare
14671c0 to
fff55d5
Compare
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.