-
-
Notifications
You must be signed in to change notification settings - Fork 111
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
feat(web): VisualKeyboard layout-reflow optimization 🪠 #11177
Conversation
User Test ResultsTest specification and instructions User tests are not required |
…o feat/web/vkbd-layout-optimization
Changes in this pull request will be available for download in Keyman version 17.0.307-beta |
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 am not comfortable with this change. We may have to live with it for 17.0 because we are running out of time, but the code flow is extremely obtuse due to the extensive use of closures, and I think it will be very hard to maintain.
I think we should be able to simplify some of these patterns dramatically with a bit of careful design, what do you think?
this.square.style.marginLeft = vkbd.layoutWidth.scaledBy(key.proportionalPad).styleString; | ||
this.btn.style.width = vkbd.usesFixedWidthScaling ? this.square.style.width : '100%'; | ||
public refreshLayout(layoutParams: KeyLayoutParams) { | ||
const keyTextClosure = super.refreshLayout(layoutParams); // key labels in particular. |
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 'keyTextClosure'? It's really unclear what that does! Perhaps we should call it superFinalization
to clarify that it's more work that super.refreshlayout has to do once this function is finished?
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 'keyTextClosure'?
Because it's a closure to finalize/apply layout changes directly tied to key text as seen in OSKKey.
Perhaps we should call it
superFinalization
[...]
If I swapped to superFinalization
or something else unrelated to the code-contents of the closure... well, I suppose I can always just add a comment about what the closure's actual effects are.
return () => { | ||
this.label.style.fontSize = fontSize.styleString; | ||
}; |
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 kinda ugly and it's unclear why this is postponed and how it gets called
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 the other side of that keyTextClosure
you were asking about - its actual definition.
Admittedly, yeah, the keyTextClosure
aspect is probably the least-clear thing in this PR. It got tricky to forward well due to the inheritance structures involved.
public refreshLayout(layoutParams: KeyLayoutParams) { | ||
// Avoid doing any font-size related calculations if there's no text to display. | ||
if(this.spec.sp == ButtonClasses.spacer || this.spec.sp == ButtonClasses.blank) { | ||
return () => {}; |
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.
It'd be better to return null
and then in the caller, use closure?.()
to make the call: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining#optional_chaining_with_function_calls
rowClosures.forEach((closure) => closure()); | ||
rowKeyClosures.forEach((closure) => closure()); |
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'm really not keen on all this passing around of closures. It's really hard to follow what is happening when. It shouldn't be necessary with a good structure to the code.
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.
Huh.
To me, I felt that this made it extremely clear that we've deferred and essentially batched all operations that could affect DOM layout (and thus, could trigger layout reflow) to this one place. (Granted, maybe I should have added documentation of this detail to the lower-level closure-building calls...) All of the closures rely on precomputed values, some of which do refer to existing layout (when prerequisite values are uncached). By precomputing all values first, without any edits... and then triggering those edits later... that's how we prevent layout reflow here.
Now that all precomputations are complete, we're safe to apply all of them sequentially without any reference to layout or style information. Use of closures automatically caches the precomputations with manually-defined code, which is a major reason I used the pattern. (Rather than manually tracking the values, we can just leverage existing language features to do that heavy lifting.)
Granted, maybe some extra comments could make this clearer - whether here or on the lower-level refresh
methods in regard to why the returned closures exist. What would make this clearer for you? Alternatively... what alternate "good structure" would you suggest?
const layerHeight = layoutParams.keyboardHeight; | ||
this.showLanguage(layoutParams.spacebarText); | ||
|
||
public refreshLayout(layoutParams: LayerLayoutParams) { |
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.
So this implementation of refreshLayout
doesn't return a closure but other ones do. It would be good if this was clear without having to search for return
statements.
Suspect IssuesThis pull request was deployed and Sentry observed the following issues:
Did you find this useful? React with a 👍 or 👎 |
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 think we should be able to simplify some of these patterns dramatically with a bit of careful design, what do you think?
I can only really see it working if we promote those closures to class methods... and manually construct the value-tracking objects that we get for free by using closures. I did look at that pattern initially, but it started getting a bit too gnarly for my tastes for the cases where key-style values weren't yet cached.
Then again, maybe having a "try-cache" method (sorry re: the horrible pun) before the precomputation pass could work? (As in, "fetch key font-style info if not yet cached and cacheable".)
Waaaaaaait. That (the "try-cache") might actually work better. We'd need to test, but I think it'd be possible to run that specific call earlier in the VisualKeyboard.refreshLayout
method - before it runs its getComputedStyle
calls. If so, I believe we'd be able to eliminate a major layout-reflow source I previously thought we couldn't.
(Reasoning: the font-size computation either picks up a fixed font-size - in which case, changing the base font-scaling doesn't matter - or it picks up a relative one... and still successfully computes the local, key-specific scaling factor just fine.)
this.square.style.marginLeft = vkbd.layoutWidth.scaledBy(key.proportionalPad).styleString; | ||
this.btn.style.width = vkbd.usesFixedWidthScaling ? this.square.style.width : '100%'; | ||
public refreshLayout(layoutParams: KeyLayoutParams) { | ||
const keyTextClosure = super.refreshLayout(layoutParams); // key labels in particular. |
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 'keyTextClosure'?
Because it's a closure to finalize/apply layout changes directly tied to key text as seen in OSKKey.
Perhaps we should call it
superFinalization
[...]
If I swapped to superFinalization
or something else unrelated to the code-contents of the closure... well, I suppose I can always just add a comment about what the closure's actual effects are.
return () => { | ||
this.label.style.fontSize = fontSize.styleString; | ||
}; |
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 the other side of that keyTextClosure
you were asking about - its actual definition.
Admittedly, yeah, the keyTextClosure
aspect is probably the least-clear thing in this PR. It got tricky to forward well due to the inheritance structures involved.
rowClosures.forEach((closure) => closure()); | ||
rowKeyClosures.forEach((closure) => closure()); |
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.
Huh.
To me, I felt that this made it extremely clear that we've deferred and essentially batched all operations that could affect DOM layout (and thus, could trigger layout reflow) to this one place. (Granted, maybe I should have added documentation of this detail to the lower-level closure-building calls...) All of the closures rely on precomputed values, some of which do refer to existing layout (when prerequisite values are uncached). By precomputing all values first, without any edits... and then triggering those edits later... that's how we prevent layout reflow here.
Now that all precomputations are complete, we're safe to apply all of them sequentially without any reference to layout or style information. Use of closures automatically caches the precomputations with manually-defined code, which is a major reason I used the pattern. (Rather than manually tracking the values, we can just leverage existing language features to do that heavy lifting.)
Granted, maybe some extra comments could make this clearer - whether here or on the lower-level refresh
methods in regard to why the returned closures exist. What would make this clearer for you? Alternatively... what alternate "good structure" would you suggest?
Some extra comments would definitely help. The problem with the term keyman/web/src/engine/osk/src/keyboard-layout/oskLayer.ts Lines 195 to 196 in 573ffd8
|
Now for some of the "meat" of the OSK optimization - this PR heavily reduces
VisualKeyboard
layout reflow, batching similar operations (read vs write) as much as possible at all times. Outside of a few knock-on effects for key and gesture previews that needed resolving, all changes are restricted to the scope ofVisualKeyboard.refreshLayout()
, the methods it calls, and the values it requests.This PR has been spun off of an older state of #11140.
@keymanapp-test-bot skip
All testing reasonable to do here is also reasonable to do on #11140.