Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DevTools: Scheduling profiler: Add vertical scroll bar #22005

Merged
merged 15 commits into from
Aug 11, 2021

Conversation

bvaughn
Copy link
Contributor

@bvaughn bvaughn commented Aug 2, 2021

Adds a vertical scrollbar to right side when sum of view heights is larger than the canvas.

Remaining items to be done:

  • Shift+wheel should scroll the outer window down if hovering over a non-scrollable sub-region
  • Resize track and thumb should appear without extra mouse move delay

This is a pretty big change. May help to look at individual commits?

outer-scroll-bar.mp4

@facebook-github-bot facebook-github-bot added React Core Team Opened by a member of the React Core Team CLA Signed labels Aug 2, 2021
@mrkev
Copy link
Contributor

mrkev commented Aug 2, 2021

Wow it's (finally) happening! Let's gooooo!

tenor-2

@bvaughn bvaughn changed the title Scheduling profiler UX changes DevTools: Scheduling profiler UX changes Aug 3, 2021
@bvaughn bvaughn changed the title DevTools: Scheduling profiler UX changes DevTools: Scheduling profiler: Add vertical scroll bar Aug 9, 2021
@bvaughn bvaughn force-pushed the devtools-scheduling-profiler branch 2 times, most recently from 46c228f to 7a7ec56 Compare August 9, 2021 21:18
@bvaughn
Copy link
Contributor Author

bvaughn commented Aug 9, 2021

@taneliang I have an idea to add support for interactions to bubble (like DOM events) and for views to stop an interaction from bubbling further if they handle it, then VerticalScrollOverflowView can compose VerticalScrollView to enable drag and wheel scrolling. I will push a commit shortly :)

composed-scroll-view.mp4

@taneliang
Copy link
Contributor

@bvaughn That makes sense! I had an initial bubbling implementation but deleted it, probably just because we could do everything we needed without it. Here’s the old code anyway in case there are ideas we can steal! MLH-Fellowship/scheduling-profiler-prototype@46b28d0

subviews.forEach(subview => {
// Pass the interaction to subviews first,
// so they have the opportunity to claim it before it bubbles.
for (let i = 0; i < subviews.length; i++) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Something I noticed the old code did: I looped through the list of subviews in reverse because later subviews can draw over earlier ones, so we’d want later subviews to handle events first.

https://github.com/MLH-Fellowship/scheduling-profiler-prototype/blob/46997525744263b1614c2b617167ddcb33588ce7/src/layout/StaticLayoutView.js#L83-L85

Copy link
Contributor Author

@bvaughn bvaughn Aug 10, 2021

Choose a reason for hiding this comment

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

Yeah! I could have sworn my bubbling code did the same but I guess that was an earlier iteration that I didn't end up committing. I'll change it to do this though :)

@bvaughn
Copy link
Contributor Author

bvaughn commented Aug 10, 2021

  • Resize track and thumb should appear without extra mouse move delay

Regarding this issue, I see what the cause is but I'm not sure yet of how to fix it.

For example, the code in ResizableView that expands the collapsed view to full height when you click on the collapsed label– it asks the subview (e.g. the FlamechartView) for its desired size:

const subviewDesiredSize = this._subview.desiredSize();

Then we count on the withVerticalScrollbarLayout to show the scrollbar based on the new view height, but at the time it's called, the desired size of the content (VerticalScrollView) is still small:

const desiredContentSize = contentLayoutInfo.view.desiredSize();

It's not until the next interaction that the content size has increased– even if I explicitly also call setFrame on the content (VerticalScrollView) in the "click" handler.

_updateSubviewFrames() doesn't run until after withVerticalScrollbarLayout has run, at which point the new height info is available.

If I manually trigger surface.displayIfNeeded() (via a hack) even if I do it synchronously, at the end of the "click" handler, the layout is re-run and the scrollbar appears.

@bvaughn
Copy link
Contributor Author

bvaughn commented Aug 10, 2021

Okay, I have an idea for an approach that only sucks a little. In CanvasPage:

    const surface = surfaceRef.current;
    surface.handleInteraction(interaction);

    // Flush any display work that got queued up as part of the previous interaction.
    // Typically there should be no work, but certain interactions may need a second pass.
    // For example, the ResizableView may collapse/expand its contents,
    // which requires a second layout pass for an ancestor VerticalScrollOverflowView.
    surface.displayIfNeeded();

Brian Vaughn and others added 11 commits August 10, 2021 13:50
…positioned at y=0

This layouter computed its remainingHeight value assuming its superview was
always positioned at y=0. This caused issues because views are scrolled by
changing their frame's origin.y value, as well as all their subviews'.
1. Composes VerticalScrollView for overall scrolling.
2. View accepts optional background color prop. This allowed us to delete lastViewTakesUpRemainingSpaceLayout, which was causing resizing troubles for the composed scroll view component.

Initial sizing update delay still remains.
Views are painted first to last, so they should process interactions last to first, so views in front (on top) can claim the interaction first.
This prevents the drag interaction from also scrolling the page
…file. I was getting confused about which class I was looking at while grepping within the file.
…t to scroll vertically

This prevents the view from stopping a bubbling interaction that could be handled by an outer view, for example to scroll the outer container.
This is an edge case, but it was causing e.g. the ResizableView's collapse/toggle to not properly update the display/size of the outermost scrollbar.
@bvaughn bvaughn force-pushed the devtools-scheduling-profiler branch from 5dc8f02 to e00b5cb Compare August 10, 2021 20:50
@bvaughn
Copy link
Contributor Author

bvaughn commented Aug 10, 2021

Unfortunately there's another bit of a race case with this item. The outer VerticalScrollView can remember scroll state between tab changes, but it's immediately blown away by one of the layouter functions.

Adding an async delay before restoring it "works" but is super clunky.

I think the interactions between the layout helpers may warrant another pass. Seems like it's awkward to code around them. Maybe I should make them stateful? I don't know. I'm feeling burnt out on this for now. I may merge this as-is and leave those last two items for follow up.

@bvaughn bvaughn marked this pull request as ready for review August 10, 2021 21:32
@taneliang
Copy link
Contributor

but at the time it's called, the desired size of the content (VerticalScrollView) is still small

Oh interesting! Did you tried calling this._updateSubviewFrames(); at the start of ResizableView's desiredSize method? I suspect that'll make its subview desire its new height 😂

Calling surface.displayIfNeeded(); in the event handler may not be a great idea – I recall seeing perf issues because we redrew the canvas multiple times in a single animation frame, which could happen if multiple events (e.g. mouse moves) are dispatched within the single animation frame.

Scroll state should be moved up into context

Hmm, this is an unorthodox solution, but would it be a good idea to just not unmount the scheduling profiler when switching tabs? That'll allow us to avoid all of these state restoration issues.

I had a similar issue at work where we had a complex component that was extremely expensive to mount, so just keeping it mounted but frozen (with a React.memo hack similar to https://codesandbox.io/s/subtree-freeze-geu6f?file=/src/App.js:925-990) + tucked away somewhere offscreen let us avoid expensive mounts when we tabbed between these components.

I think we could do something similar here to preserve component + view state, especially if we'll be facing many state restoration issues/complexity. Just an idea!

@bvaughn
Copy link
Contributor Author

bvaughn commented Aug 11, 2021

Did you tried calling this._updateSubviewFrames(); at the start of ResizableView's desiredSize method? I suspect that'll make its subview desire its new height 😂

Calling _updateSubviewFrames doesn't change the subview's desired size. I assume this is what you were suggesting?

diff --git a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js
index 0caf20a4e3..8c84eac1b1 100644
--- a/packages/react-devtools-scheduling-profiler/src/CanvasPage.js
+++ b/packages/react-devtools-scheduling-profiler/src/CanvasPage.js
@@ -402,12 +402,6 @@ function AutoSizedCanvas({
     const surface = surfaceRef.current;
     surface.handleInteraction(interaction);
 
-    // Flush any display work that got queued up as part of the previous interaction.
-    // Typically there should be no work, but certain interactions may need a second pass.
-    // For example, the ResizableView may collapse/expand its contents,
-    // which requires a second layout pass for an ancestor VerticalScrollOverflowView.
-    surface.displayIfNeeded();
-
     canvas.style.cursor = surface.getCurrentCursor() || 'default';
 
     // Defer drawing to canvas until React's commit phase, to avoid drawing
diff --git a/packages/react-devtools-scheduling-profiler/src/view-base/resizable/ResizableView.js b/packages/react-devtools-scheduling-profiler/src/view-base/resizable/ResizableView.js
index b56422f715..fb7a92979d 100644
--- a/packages/react-devtools-scheduling-profiler/src/view-base/resizable/ResizableView.js
+++ b/packages/react-devtools-scheduling-profiler/src/view-base/resizable/ResizableView.js
@@ -79,6 +79,8 @@ export class ResizableView extends View {
   }
 
   desiredSize() {
+    this._updateSubviewFrames();
+
     const subviewDesiredSize = this._subview.desiredSize();
 
     if (this._shouldRenderResizeBar()) {

The easiest way to repro the (broken) behavior locally (with minimal noise from other views) is to remove all subviews from the root except for the flamechartViewWrapper, then just load a profile and click on the collapsed "flamechart" accordion tab.

The overall flow is:

  1. ResizableView desiredSize() runs and has an updated content height (e.g. 322)
  2. withVerticalScrollbarLayout() runs but with the old content height still (e.g. 16)
  3. Eventually another interaction runs
  4. withVerticalScrollbarLayout() runs again, now with the new content height (e.g. 330)

which could happen if multiple events (e.g. mouse moves) are dispatched within the single animation frame.

Oooh, as a side note, we should probably avoid dispatching multiple mouse-move events within an animation frame?

Calling surface.displayIfNeeded(); in the event handler may not be a great idea – I recall seeing perf issues because we redrew the canvas multiple times in a single animation frame, which could happen if multiple events (e.g. mouse moves) are dispatched within the single animation frame.

Good call. I'd love to be able to remove the hack if we can find a better way to get the nested layout issue resolved.

Copy link
Contributor

@jstejada jstejada left a comment

Choose a reason for hiding this comment

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

i think there's lots i need to wrap my head around to give a useful review here, but overall lgtm. added some questions, some seemingly unintended changes are still showing up for me, but accepting to unblock

@bvaughn
Copy link
Contributor Author

bvaughn commented Aug 11, 2021

Since this has been approved by Luna and Juan, I'm going to go ahead and merge it so we can share with the v18 Working Group and start gathering user feedback.

@taneliang Let's continue the discussion thread above with a follow up PR?

@bvaughn bvaughn merged commit 280e3f9 into facebook:main Aug 11, 2021
@bvaughn bvaughn deleted the devtools-scheduling-profiler branch August 11, 2021 18:19
@taneliang
Copy link
Contributor

Calling _updateSubviewFrames doesn't change the subview's desired size. I assume this is what you were suggesting?

Unfortunately yes, that was what I was suggesting. Interesting that the layouter in step 2 is still using the old size even though ResizableView is returning the new size in step 1? This seems surprisingly difficult.

It's possible that the desiredSize implementation also needs a closer look – it has always felt a little messy to me, with different views having various ways of figuring out their sizes, and I vaguely recall some weird cases where the desired size only works when certain views are used with each other.

share with the v18 Working Group and start gathering user feedback.

Excited for this! Let's continue this thread in a future PR :)

zhengjitf pushed a commit to zhengjitf/react that referenced this pull request Apr 15, 2022
Co-authored-by: E-Liang Tan <eliang@eliangtan.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed Component: Developer Tools React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants