Skip to content

perf: load and save drawings off the UI thread - #3

Merged
SectionTN merged 3 commits into
mainfrom
perf/off-thread-document-commands
Oct 23, 2026
Merged

SectionTN merged 3 commits into
mainfrom
perf/off-thread-document-commands

Conversation

@SectionTN

@SectionTN SectionTN commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

This takes #1 by @azizbecha onto current main, with the fixes from its review.

getStrokes and setStrokes now run off the UI thread on both platforms. On a 300-stroke drawing with 45,000 points, they blocked the main thread for 65 ms and 89 ms on the iOS simulator. On the Android emulator it was 160 ms and 434 ms.

Commands still apply in the order JS sends them. A clear, undo, redo, getStrokes or toImage sent after a setStrokes waits for the load to finish, so setStrokes(doc); clear() clears the loaded drawing, and two loads apply in the order JS sent them. When nothing is loading, commands run right away as before. Each platform has a small main-thread queue for this, CommandQueue on Android and INKCommandQueue on iOS. Sending every command through the worker thread would have made them wait behind exports, and on Android an export can wait there for the background image.

Android answers now post through the main looper. View.post holds a runnable until the view attaches to a window again, so a call made while the canvas was in an inactive tab would time out.

On iOS, an export that waits for the background image keeps the drawing from the moment of the call, so toImage(); clear() no longer exports the cleared canvas.

The iOS brush map has no cap. An entry costs about 116 bytes, while undo history already holds about 4.8 KB for each 150-point stroke. A capped map also gave the strokes it evicted the current brush on save.

CommandQueueTest covers the Android queue on the JVM. InkpadViewCommandsTest is an instrumented test on a real InkpadView: clear, undo and getStrokes behind a pending load, two loads, a failed load, and a detached view. iosCommandQueue.test.ts builds the iOS queue with clang and runs it. The e2e suite in CI covers the iOS view itself.

Supersedes #1.

azizbecha and others added 3 commits October 23, 2026 23:07
getStrokes serialized every point to JSON on the UI thread on both platforms, and
setStrokes built a stroke per point there too. Both now run on a worker thread and
answer back on the UI thread, the way the image export already did. The Android
export executor is shared, so the document commands queue behind it instead of
building a second thread.

The iOS brush map grew for the whole session and was never pruned, because clear
and undo leave entries that undo itself still needs. Keeping the most recent
1000 bounds the memory while leaving undo intact.
setStrokes now builds its strokes on a worker thread, so a clear, undo, redo, getStrokes or
exportImage sent right after it could run before the load landed. Commands now go through a
main-thread queue: they still run at once, and only wait while a setStrokes is loading.

Exports go back to their own thread, because they can wait there for the background image and
document commands should not wait behind that. Answers post through the main looper, since
View.post holds them until a detached view is attached again.

Co-authored-by: Aziz Becha <aziz07becha@gmail.com>
setStrokes decoded on a concurrent global queue, so two loads could land out of order, and a
clear, undo, redo, getStrokes or exportImage sent after a load could run before it. Document
work now runs on a private serial queue, and commands go through a main-thread queue that only
waits while a load is pending. An export that waits for the background image keeps the drawing
it was asked for.

The brush map drops its 1000-entry cap. An entry is about 116 bytes while undo history holds
about 4.8 KB per 150-point stroke, and the cap gave evicted strokes the current brush on save.

Co-authored-by: Aziz Becha <aziz07becha@gmail.com>
Copilot AI balanced review requested due to automatic review settings October 23, 2026 22:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@coderabbitai

coderabbitai Bot commented Oct 23, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 1c375f9d-f637-4f49-9a2a-d2a5bfddcbf3
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@SectionTN
SectionTN merged commit 6a0a3a5 into main Oct 23, 2026
10 of 11 checks passed
@SectionTN
SectionTN deleted the perf/off-thread-document-commands branch October 23, 2026 23:21
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