keymanapp / keyman

Keyman cross platform input methods system running on Android, iOS, Linux, macOS, Windows and mobile and desktop web
https://keyman.com/
Other
367 stars 102 forks source link

change(web): optimization for keyboard-layout preprocessing ⏩ #11263

Open jahorton opened 2 weeks ago

jahorton commented 2 weeks ago

Addresses parts of #11166.

As previously noted, the touch-layout preprocessing stage for touch keyboards takes... remarkably longer than seems necessary. This PR serves to address parts of that. The main three changes:

  1. Make object-properties less expensive by having a common prototype, rather than artificially copying 'em over.
  2. Only use delete when actually necessary.
  3. Pre-constructing key-event objects per key adds up a bit; what if we deferred building them until needed?

Now... why those three? And what led me to decide on those three?

To stress-test things, let's examine one load of gff_amharic on an SM-T350 / Galaxy Tab A:

gff_amharic loading+display profile

Corresponding Chrome profile, exported as json

Basic observable stats:

That is quite a lot of time to start up gff_amharic... and that's after a lot of recent optimization work! Can we push things further? The first question to ask in that regard: where is all that time being spent?

call stack inspection:  over 700ms spent in 'key' and 'subkey' level `polyfill` stack-frames

call stack inspection:  over 240 ms spent in 'key' and 'subkey' level `sanitize` stack-frames

Over 700 ms spent in key-level polyfill() call-frames + over 240ms spent in key-level sanitize stack-frames = about 70.6% of the pre-processing time being spent in those stack-frames alone. Obviously, that's where to look for big wins.

As a runner-up point, note that constructBaseKeyEvent - our key-event preprocessing step - costs a total additional 136 ms.

Within sanitize:

sanitize in-line execution-time view

Key observations:

Within polyfill:

polyfill in-line execution-time view

Key observations:


So, how to optimize things? Back to those big three changes:

  1. Make object-properties less expensive by having a common prototype, rather than artificially copying 'em over.
  2. Only use delete when actually necessary.
  3. Pre-constructing key-event objects per key adds up a bit; what if we deferred building them until needed?

For the first item there, the easiest way to handle this... is to use proper class-construction, rather than attempting to reuse the objects. Per MDN, setting the prototype afterward is expensive... and even then, it didn't appear to actually enable our desired properties on the key-objects anyway. By having them defined only once, on the common prototype... we can get a dramatic speedup.

gff_amharic loading+display profile (after)

The important observable stat: the pre-processing stage now only takes 523ms, rather than 1330ms. That's 2.54x as fast!

Other notable details:

30.2ms total spent using Object.assign()

As this PR has follow-ups doing further optimization...

@keymanapp-test-bot skip

keymanapp-test-bot[bot] commented 2 weeks ago

User Test Results

Test specification and instructions

User tests are not required

Test Artifacts