-
Notifications
You must be signed in to change notification settings - Fork 30.2k
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
GridLayout perf improvements #79614
GridLayout perf improvements #79614
Conversation
@sbatten hi! This looks like nice work and I will gladly review this and test it with you. Do you have a scenario where the previous solution was showing this slow perf? How can we easily measure that this new approach is an improvement? |
@@ -226,7 +280,7 @@ class BranchNode implements ISplitView, IDisposable { | |||
} | |||
} | |||
|
|||
addChild(node: Node, size: number | Sizing, index: number): void { | |||
addChild(node: Node, size: number | Sizing, index: number, skipLayout?: boolean): void { | |||
if (index < 0 || index > this.children.length) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
skipLayout
is not being used. Why are we adding it?
@@ -31,51 +31,131 @@ | |||
<script> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great that you added tests! I see they pass, I will not review them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@isidorn Everything in test/splitview
are just visual manual tests, like a playground, you can safely ignore that. Proper unit tests are in place to make sure all this works.
const snappedBefore = typeof snapBeforeIndex === 'number' && !this.viewItems[snapBeforeIndex].visible; | ||
const snappedAfter = typeof snapAfterIndex === 'number' && !this.viewItems[snapAfterIndex].visible; | ||
|
||
if (snappedBefore && collapsesUp[index]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not understand the collapsesUp
and collapsesDown
change.
This does not seem related to the serialization in the grid or am I missing something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this change fixes a bug that I thought was only in my branch but it actually reproduces in insiders as well, I can pull this out, but you can repro the issue by hiding the sidebar, toggling its position, then trying to drag it out (unsnap it). It will not unsnap. If needed, I can move this to a separate bug fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I'd just put this one in master
directly.
@@ -52,6 +53,10 @@ export interface IView { | |||
setVisible?(visible: boolean): void; | |||
} | |||
|
|||
export interface ISerializableView extends IView { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is introducing the concept of serialization
to the SplitView.
I think the SplitView
should have no notion of this concept, but instead the clients of the SplitView
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -272,6 +286,17 @@ export class SplitView extends Disposable { | |||
this.viewContainer = dom.append(this.el, dom.$('.split-view-container')); | |||
|
|||
this.style(options.styles || defaultStyles); | |||
|
|||
// We have an existing set of view, add them now | |||
if (options.descriptor) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is the splitView
doing this? Why doesn't the client simply construct the new SplitView and add all the deserialized views and says skip the layout.
If we went with this approach I think we can dump the ISplitViewDescriptor
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SplitView was kind of an easy case for adding skipLayout, but I don't actually like having that as part of its public api, especially since it doesn't match well with GridView's branch node api. To make this look more correct optically and avoid people calling with skiplayout, I've made that a private call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't want to add view by view and skipping the layout until the end. This will still cause all the math to happen in the background. Plus it will expose a terrible skipLayout
API. As per comment above, we want a serializable Splitview which can deserialize in a single layout call.
@@ -953,6 +1026,47 @@ export class GridView implements IDisposable { | |||
return this._getViews(node, this.orientation, { top: 0, left: 0, width: this.width, height: this.height }); | |||
} | |||
|
|||
static deserialize<T extends ISerializableView>(json: ISerializedGridView, deserializer: IViewDeserializer<T>, options: IGridViewOptions = {}): GridView { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like this does not belong here.
So this is a GridView which so far had no notion of serialization and we are adding it.
I thought that is the point of the SerializedGird.
Same as my previous comment, serialization should not be done on this level but on one level higher it feels.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With this PR both the Splitview and Gridview will support serialization. The idea is to also get rid of the SerializableGrid, since the Grid will be able to deserialize.
super(); | ||
this.gridview = new GridView(options); | ||
|
||
if (view instanceof GridView) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please clarify this new case when the view is instanceof GridView. Since this seems to not be covered before
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Grid historically created a grid view and then added views, we know this has pitfalls due to layouts for each added view. This allows us to provide a preconstructed one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ya this is weird, I'd rather have a public deserialize
method for this. But that's just API preference.
@sbatten I did an initial review and added comments - which are more like questions. So it would be great if you could adress them or explain to me more about the reasoning behind why we choose to do it in this way - if it is easier we can also talk via Teams (I should be online for another 90 minutes). Can you also explain more about why the new serialisation tehnique is better than the previous one - will make it easier for me to digest the code since I have no prior deep understanding. |
@isidorn I've updated the PR description with more context and added some responses to your comments as well. Lmk if more information is desired. |
@isidorn Deserializing a splitview/grid today requires additively modifying the widget from an empty state into the fully reconstructed state, adding view by view. This is expensive because in runs through all the layout code in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sbatten I'm not a big fan of deserialize2
. Since this is a PR, just go ahead and replace the current deserialize
code with the new approach. This will help you validate the implementation in two ways: by benefiting from the already existing test suite and by seeing it work in the editor grid. You're going to have to do that work eventually, better do it today than in debt week.
const snappedBefore = typeof snapBeforeIndex === 'number' && !this.viewItems[snapBeforeIndex].visible; | ||
const snappedAfter = typeof snapAfterIndex === 'number' && !this.viewItems[snapAfterIndex].visible; | ||
|
||
if (snappedBefore && collapsesUp[index]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I'd just put this one in master
directly.
this.state = State.Idle; | ||
|
||
if (typeof size !== 'number' && size.type === 'distribute') { | ||
this.distributeViewSizes(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
distributeViewSizes
is expensive, we very likely also want to skip this if skipLayout
.
views: { | ||
visible?: boolean; | ||
size: number; | ||
view: IView; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sbatten I might be wrong but I believe this won't work in the big picture. Remember that a gridview is a tree of splitviews. In order to fully deserialize a whole gridview in one step, you're very likely going to have to use the deserializer pattern in which a fromJSON
method is called, even down in the splitview.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the grid view deserializes views using a depth first search so this shouldn't be an issue
@joaomoreno thanks for the review |
@isidorn I've addressed @joaomoreno's feedback and now have a single deserialize call. With this change and some modification, the new deserialize API passes all unit tests and sanity manual tests. I am more confident in it now and think it should be ready for testing. @joaomoreno Interesting point about the latest commit. One of the unit tests was failing because it was testing that the grid would resize all views proportionally on the first layout call. This logic is actually counter to how the workbench should work. We don't usually want to scale out the sidebar or panel when we get some extra space. And today if the window is smaller than the panel, it will just take most of the screen in legacy layout. With that observation, I have reserved this proportion-preserving behavior only for grids with proportonalLayout enabled. That means I don't need to do anything special in the initial layout, because it is functionally no different from any other layout. I simply needed to initialize the proportions in SplitView to make this work. |
@sbatten thanks for adressing the feedback. I plan to test the branch out today. Will update this comment with my findings. |
@sbatten I have tested this and it works nicely. I am especially happy about the Panel maximize now being able to take the full view. As for the performance, when running out of source from this branch with your new serialization improvements (after 4 runs on average) I get Since the layout seems to works and we should start self hosting on this I am approving this PR. Thanks for the nice work |
fyi @jrieken for potential minor startup perf hit. Based on my local measuring workbenchLoad is 2% slower with the new grid layout. |
But this should still be a perf gain when comparing to today's insiders |
Summary
deserialize2
to avoid mucking with anything the editor grid is doing)index.html
page to allow playing with it.layout.ts
I've played around with this and it feels good and shows a great perf improvement, but as we all know testing ones own code can suffer from tunnel vision.
I know @joaomoreno is out so I'm looking for some help getting this tested and safely in.
Perf Improvments
The primary goal of this work is to improve workbench startup time. As measured on my machine, the current grid layout performs ~40ms slower than the legacy layout on initial startup time. I am using the startup performance command to look at markers. I have the below patch for more fine grained measurement. On my machine, these changes recover >75% of that lost time.
Why was it slow?
The way we deserialize today is by adding views individually at the grid level (not grid view, or splitview level). What that means is that every addition of a view triggers a layout call for that view and additionally for the rest of the views. Joao did some work to make this faster by trying to delay layout calls until the end of the deserialization, but the issue remained because not enough information was initialized during the deserialization process so the initial layout call triggered many recalculations. Instead of dealing with layout controllers, the proper fix as Joao and I discussed was to bring this logic down to the individual view types. Now the initial layout calls layout on each part exactly once during startup.