Skip to content

fix(macOS): offload compression off the AppKit main thread - #398

Merged
AlexV525 merged 1 commit into
mainfrom
fix/macos-offload-compression-to-background
Jul 17, 2026
Merged

fix(macOS): offload compression off the AppKit main thread#398
AlexV525 merged 1 commit into
mainfrom
fix/macos-offload-compression-to-background

Conversation

@AlexV525

Copy link
Copy Markdown
Member

Summary

FlutterMethodChannel dispatches handle(_:result:) on the platform thread — on macOS that's the AppKit main / UI thread. The plugin ran decode, CGContext resize+rotate, CGImageDestination encode and disk writes inline on that thread, so compressing anything nontrivial stalled the Flutter engine and froze the UI (dropped frames, gesture/input hangs, animation stutter).

Android and iOS already offload:

  • Android — every compress method is threadPool.execute { … } on a fixed 8-thread ExecutorService, with results posted back via Handler(Looper.getMainLooper()) (ResultHandler.kt).
  • iOSImageCompressPlugin.handleMethodCall: wraps the switch in dispatch_async(dispatch_get_global_queue(0, 0), …) (ImageCompressPlugin.m).

macOS now matches: compression runs on DispatchQueue.global(qos: .userInitiated), FlutterResult is marshaled back to the main thread via a small mainResult shim.

What changed

FlutterImageCompressMacosPlugin.swifthandle(_:result:) only:

  1. Threading — the three compress branches (compressAndGetFile, compressWithFile, compressWithList) plus default (FlutterMethodNotImplemented) run inside DispatchQueue.global(qos: .userInitiated).async { … }.
  2. mainResult shim — wraps FlutterResult so every downstream callback (handleResult's error paths at lines 108/124/132, Compressor.compressToPath / compressToBytes at lines 402/423) always resolves on the main thread regardless of where it's invoked from.
  3. showLog reply gap — the previous switch case was Logger.showLog(show: args as! Bool) with no result(…) call, leaving the awaiting Dart future pending. Now returns 1 to match Android (result.success(1)) and iOS (result(@1)). Kept synchronous on the calling thread — it's a fast setter, no reason to hop.
  4. [weak self] nil branch — if the plugin is detached before the async block starts, previously the guard let self else { return } swallowed the call silently. Now replies with FlutterError(code: "plugin_detached", …) so the Dart future always resolves.

Also updates packages/flutter_image_compress_macos/macos/.gitignore to ignore SwiftPM's .build/ output that IDE indexing / swift build leaves under macos/flutter_image_compress_macos/.

Test plan

  • flutter build macos --debug from the example — clean.
  • flutter test integration_test/compress_test.dart -d macos — all 13 macOS-relevant tests pass (7 skipped are Android/iOS-only), including HEIC round-trip, EXIF preservation across JPEG/PNG/HEIC, rotate 90/180/270, transparent PNG, WebP interop.
  • Reviewer manual UI-freeze repro (optional): before this PR, compressing a ~20MB image on macOS visibly hitches Flutter animations for the duration of the compress; after this PR, animations run smoothly and the compress just resolves out of band.

Notes for melos changelog

Single fix(macOS): … commit — will land under flutter_image_compress_macos's Bug Fixes section.

FlutterMethodChannel dispatches handle(_:result:) on the platform
thread (main/UI on macOS). Decode, CGContext resize+rotate,
CGImageDestination encode and disk writes all ran inline on that
thread, so compressing anything nontrivial stalled the Flutter
engine and froze the UI. Move the switch body onto a userInitiated
global queue and marshal every FlutterResult back to main — the
Android threadPool + main-Looper Handler and iOS global-queue
handlers already worked this way; macOS now matches.

Also close two reply-path gaps so every branch of handle(...)
resolves `result`: showLog previously never called result(...)
(Android returns 1, iOS returns @1), leaving the Dart future
pending; and the [weak self]/nil branch on the background queue
now replies with an error rather than silently swallowing the call.

Ignore SwiftPM's .build/ output under macos/flutter_image_compress_macos/
so it doesn't leak into git status after `swift build` or IDE
indexing.
@AlexV525
AlexV525 requested a review from CaiJingLong as a code owner July 17, 2026 03:58
@AlexV525
AlexV525 enabled auto-merge (squash) July 17, 2026 04:12
@AlexV525
AlexV525 disabled auto-merge July 17, 2026 04:16
@AlexV525
AlexV525 merged commit 3d45802 into main Jul 17, 2026
9 checks passed
@AlexV525
AlexV525 deleted the fix/macos-offload-compression-to-background branch July 17, 2026 04:17
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.

1 participant