Visitar URL original
perf(wasm): Create image decode workers on demand (backport #25132) by mergify[bot] · Pull Request #25897 · unoplatform/uno · GitHub
Skip to content

perf(wasm): Create image decode workers on demand (backport #25132) - #25897

Merged
agneszitte merged 1 commit into
servicing/6.8from
mergify/bp/servicing/6.8/pr-25132
Nov 1, 2026
Merged

agneszitte merged 1 commit into
servicing/6.8from
mergify/bp/servicing/6.8/pr-25132

Conversation

@mergify

@mergify mergify Bot commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

GitHub Issue: closes #25125

PR Type:

🐞 Bugfix (performance)

What changed? 🚀

Uno.UI.Runtime.ImageLoader created one decode worker per logical processor when the script loaded, whether or not the app ever decoded an image. Each worker is a separate isolate with its own heap and an OffscreenCanvas 2D context.

Workers are now created on demand. A decode uses an idle worker if there is one. Otherwise it creates a worker while fewer than navigator.hardwareConcurrency exist, and otherwise it waits in the existing queue. The limit is unchanged, so burst decoding keeps the same parallelism.

Measurements 📊

Master's SamplesApp on WASM in headless Edge, on a 12-thread machine. navigator.hardwareConcurrency was set through the DevTools protocol (Emulation.setHardwareConcurrencyOverride) before load, which sets the pool size without changing code. Memory is Windows private bytes of the tab's renderer process; the GPU process doesn't change with worker count. Two runs per row.

Idle cost, before this PR: about 4 MB per worker.

hardwareConcurrency Before After
1 494–497 MB –
12 538–541 MB 493–495 MB
32 622–629 MB 497–500 MB

After the change, one worker exists at idle because the SamplesApp decodes an image at startup. An app that never decodes one creates none.

After a burst of 48 decodes of a 2048×2048 PNG, 15 s later:

  • Each worker that decoded holds up to about 11 MB more. On master with 32 workers that's +347 MB.
  • Its own GC releases this within about a minute: 971 MB at 15 s, 641 MB at 60 s.
  • This PR doesn't change that peak once the pool has grown; idle release would be a follow-up.

Decode time for the same burst is unchanged:

  • 12 workers: 794–844 ms on master, 802–817 ms with this PR.
  • 4 workers: about 1.1 s on both.
  • Throughput stops improving at about 6 workers on this machine.

Validation

  • Runtime:
    • WASM Given_BitmapSource, in headless Edge: 16/16 pass, 1 skipped (an existing [Ignore]). This includes When_Browser_Workers_Are_Reused, which sends one more concurrent decode than hardwareConcurrency and so exercises growing the pool to its limit and reusing a worker.
    • The measurements above, run on the published app.
  • Not added: a test for "no worker before the first decode". The pool is per process and earlier tests decode images, so the runtime-test app can't observe an untouched pool.
  • Not measured: GPU memory of the workers' canvases on a real GPU. The headless browser here draws canvases in software.

PR Checklist ✅

🤖 Generated with Claude Code


This is an automatic backport of pull request #25132 done by [Mergify](https://mergify.com).

ImageLoader created one worker per logical processor at startup, each an
isolate with its own heap and canvas: about 4 MB apiece, so 130 MB on a
32-thread machine before any image was decoded. Workers are now created
when a decode finds none idle, up to the same limit, so burst decoding
is unchanged.

Fixes #25125

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit e768182)
@mergify mergify Bot mentioned this pull request Nov 1, 2026
1 of 5 tasks
@unodevops

Copy link
Copy Markdown
Contributor

🤖 Your WebAssembly Sample App stage site is ready! Visit it here:

@agneszitte
agneszitte merged commit a0eb458 into servicing/6.8 Nov 1, 2026
22 of 42 checks passed
@agneszitte
agneszitte deleted the mergify/bp/servicing/6.8/pr-25132 branch November 1, 2026 23:08
mikernet added a commit to Singulink/uno that referenced this pull request Nov 14, 2026
Takes Uno's servicing/6.8 from 6951c2c up to 5254ace (42 pull
requests). Conflicts, by area:

IME and TextBox (unoplatform#25984, backport of unoplatform#25804; unoplatform#25952, backport of unoplatform#25619;
unoplatform#25976, backport of unoplatform#25818)
- Takes upstream's composition design (unoplatform#25804): the browser's input is
  the source of truth for the text, compositions included. This drops
  the composition parts of the unoplatform#24502 port (90746b8): its
  TypeScript composition handling, the StartIndex/TextChangePending
  event arguments, the IsReadOnly forwarding to the hidden input, and
  its Given_TextBox.BrowserSoftKeyboard tests.
- Keeps the non-IME browser fixes on top of it, in the form their
  master pull requests have on unoplatform#25804: Enter raised once per press and
  tracked by key code (unoplatform#25174, with Given_TextBox.BrowserEnterKey from
  master in place of the Enter tests that lived in the dropped file),
  the iOS refocus on tap (unoplatform#25185), pastes without text and isIOS from
  BrowserInputHelper.
- Moves the unoplatform#26005 MaxLength fixes onto unoplatform#25804's native input path, as
  master has them: a refused browser edit keeps the TextBox's selection,
  TextBoxView.UpdateTextFromNative reports whether the edit went in, and
  the IME keeps its range on the applied text (master be85699, which
  d226897 had ported onto the unoplatform#24502 IME). Adds that commit's browser
  and platform-applied IME tests.
- TextBox.pointers.skia.cs: upstream's file (unoplatform#25619 iOS touch editing,
  unoplatform#25818, unoplatform#25804), with the unoplatform#25126 port (the hold acts when it starts,
  a touch right-tap keeps the caret) in the form master's unoplatform#25126 branch
  has it on unoplatform#25619, including focusing before the iOS hold timer acts
  and its iOS not-focusable test.

Pointers (unoplatform#25976, backport of unoplatform#25818)
- Tapped and RightTapped are raised once the PointerReleased bubbled
  (unoplatform#25818), with the unoplatform#25732 port's window coordinates (2-argument
  routed event args, RawPosition for ContextRequested), as master's
  unoplatform#25732 branch has them.
- The release wrapper of unoplatform#25818 keeps the unoplatform#25126 port's
  StopContextMenuTimer at the start of the release.

DependencyObject (unoplatform#26142, backport of unoplatform#26123)
- Takes upstream, then the code of unoplatform#25839 (97a2f17), which replaces
  this branch's unoplatform#25102 ports (800247f, be41da6, fd40bf9,
  7546379). DependencyObjectStore.Binder.cs, .alc.cs,
  Application.Alc.cs, ResourceDictionary.alc.cs and their tests now
  match unoplatform#25839; this drops 800247f's own
  SingleParentNonFE_Parent_Removes_Value_Then_Next_Parent_Starts_Clean
  test, which unoplatform#25839 doesn't have.

Imaging (unoplatform#25597, unoplatform#25915, unoplatform#25911, unoplatform#25897)
- Keeps this branch's side: it has the later forms of these fixes
  (unoplatform#25124, unoplatform#25559, unoplatform#25633 and unoplatform#26370 ports), where upstream has the
  6.8-only unoplatform#25597 design and earlier snapshots. Upstream's shared
  download, IgnoreImageCache and failed-load fixes are covered by
  SharedImageDecode. Adds upstream's When_Source_Outlives_Brushes test.

ToolTip (unoplatform#26098, backport of unoplatform#25137 and unoplatform#26096)
- Keeps this branch's unoplatform#25137 port and adds unoplatform#26096 in master's form: the
  service releases only a ToolTip it opened, and leaves one the app
  opened during the show delay to the app. Keeps both sides' tests.

Flyout (unoplatform#26062, backport of unoplatform#26060)
- This branch had the same commits already; keeps the "Keep
  non-resizable flyouts in window" port and its tests.

Also removes a helper pair in Given_AccessibleTextBox that both sides
added.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants