Skip to content

[macOS] Window resize blocks the main thread for 1s per step when the Flutter scene has nothing to rasterize #190075

Description

@edde746

Steps to reproduce

  1. flutter create bug and replace lib/main.dart with the sample below.
  2. flutter run -d macos. Reproduces in debug, profile and release.
  3. Drag any window edge or corner to resize.
  4. Flip kPaintSomething to true and repeat step 3.

Expected results

Resizing is smooth in both cases. A frame in which nothing paints should not make window resizing slower than a frame that paints something.

Actual results

With kPaintSomething = false the window follows the cursor at roughly one step per second and the console fills with Resize timed out. With kPaintSomething = true — a single opaque box, nothing else changed — resizing is smooth.

scene mean per resize step Resize timed out
kPaintSomething = false (nothing paints) 1083.67 ms 20 / 20
kPaintSomething = true (one opaque box) 16.37 ms 0

A release build behaves the same as debug: 1093.41 ms per step, 20/20 timed out. 1083 ms is the ResizeSynchronizer timeout, so every step times out.

The timings come from local instrumentation, not from the Dart sample — the probe is included below so the measurement is reproducible. The user-visible symptom (one resize step per second, Resize timed out spam) needs only the Dart sample and a mouse.

Suspected cause

Not maintainer-confirmed; this is my reading of the engine source at the revision under test (83675ed276).

GetRequiredFrameSize() derives the committed frame size from the presented surfaces, starting at CGSizeZero:

static CGSize GetRequiredFrameSize(NSArray<FlutterSurfacePresentInfo*>* surfaces) {
  CGSize size = CGSizeZero;
  for (FlutterSurfacePresentInfo* info in surfaces) { ... }
  return size;
}

When the scene paints nothing, EmbedderExternalView::HasEngineRenderedContents() is false — DlOpSpy::setColor treats a fully transparent colour as "will not draw" — so FlutterCompositor::Present builds an empty surfaces array and the committed size is (0, 0). ResizeSynchronizer.beginResize is waiting for contentSize == size, which can never happen, so -[FlutterView setFrameSize:] spins the platform thread for the full timeoutDuration = 1.0.

For contrast, the Windows embedder completes the same handshake from the resize target rather than from the presented layers (FlutterWindowsView::OnEmptyFrameGenerated, regression test TestEmptyFrameResizes), so an empty frame there does not stall.

A real app reaches this state by rendering into a native CALayer inserted below a transparent FlutterView — with the overlay UI hidden, nothing in the Flutter scene paints and window resizing becomes unusable. I have not checked whether a scene containing only a platform view (and no engine-rendered content) hits the same path.

Code sample

Code sample
import 'package:flutter/material.dart';

/// Flip to `true` and the resize stall disappears.
const bool kPaintSomething = false;

void main() {
  runApp(
    MaterialApp(
      // The debug banner paints, which would defeat the repro.
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        // Nothing in the scene paints, so the frame has no backing store.
        // A real app reaches this state by drawing its content into a native
        // CALayer inserted below a transparent FlutterView.
        backgroundColor: Colors.transparent,
        body: kPaintSomething
            ? const ColoredBox(color: Color(0xFF2196F3), child: SizedBox.expand())
            : const SizedBox.expand(),
      ),
    ),
  );
}
Measurement probe (optional — appended to macos/Runner/MainFlutterWindow.swift)

Call ResizeProbe.install(window: self) at the end of awakeFromNib(), then kill -USR1 <pid>. It drives 20 setFrame steps through the same -[FlutterView setFrameSize:] path a live drag uses and reports how long each one blocked the main thread.

enum ResizeProbe {
  private static var source: DispatchSourceSignal?
  static func install(window: NSWindow) {
    signal(SIGUSR1, SIG_IGN)
    let src = DispatchSource.makeSignalSource(signal: SIGUSR1, queue: .main)
    src.setEventHandler { [weak window] in
      guard let window else { return }
      let base = window.frame
      var samples: [Double] = []
      for i in 0..<20 {
        var f = window.frame
        let d = CGFloat((i % 2 == 0) ? 6 : -6)
        f.size.width += d; f.size.height += d
        let t0 = CFAbsoluteTimeGetCurrent()
        window.setFrame(f, display: true)
        samples.append((CFAbsoluteTimeGetCurrent() - t0) * 1000.0)
      }
      window.setFrame(base, display: true)
      let mean = samples.reduce(0, +) / Double(samples.count)
      NSLog("[ResizeProbe] n=%d mean=%.2fms", samples.count, mean)
    }
    src.resume(); source = src
  }
}

Logs

Logs
Running with merged UI and platform thread. Experimental.
Resize timed out
Resize timed out
Resize timed out
Resize timed out
... (one per resize step; 20 of 20 in the measured run)
[ResizeProbe] n=20 mean=1083.67ms

Flutter Doctor output

Doctor output
[✓] Flutter (Channel stable, 3.44.5, on macOS 26.5.2 25F84 darwin-arm64, locale en-US) [215ms]
    • Flutter version 3.44.5 on channel stable at ~/Documents/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision f94f4fc76b (3 weeks ago), 2026-07-06 11:19:24 -0700
    • Engine revision 83675ed276
    • Dart version 3.12.2
    • DevTools version 2.57.0
    • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, enable-swift-package-manager, omit-legacy-version-file, enable-lldb-debugging, enable-uiscene-migration

[!] Android toolchain - develop for Android devices (Android SDK version 37.0.0) [975ms]
    • Android SDK at ~/Library/Android/sdk
    • Emulator version 36.6.11.0 (build_id 15507667) (CL:N/A)
    • Platform android-37.0, build-tools 37.0.0
    • ANDROID_HOME = ~/Library/Android/sdk
    • ANDROID_SDK_ROOT = ~/Library/Android/sdk
    • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
      This is the JDK bundled with the latest Android Studio installation on this machine.
      To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
    • Java version OpenJDK Runtime Environment (build 21.0.10+-117844308-b1163.108)
    ! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses

[✓] Xcode - develop for iOS and macOS (Xcode 26.6) [528ms]
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 17F113
    • CocoaPods version 1.17.0

[✗] Chrome - develop for the web (Cannot find Chrome executable at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome) [3ms]
    ! Cannot find Chrome. Try setting CHROME_EXECUTABLE to a Chrome executable.

[✓] Connected device (4 available) [6.0s]
    • Pixel 7 (mobile)       • <redacted-serial>         • android-arm64 • Android 16 (API 36)
    • Box R 4K Plus (mobile) • <redacted-lan-device>     • android-arm   • Android 14 (API 34)
    • iPhone 17 (mobile)     • <redacted-simulator-uuid> • ios           • com.apple.CoreSimulator.SimRuntime.iOS-26-5 (simulator)
    • macOS (desktop)        • macos                     • darwin-arm64  • macOS 26.5.2 25F84 darwin-arm64

[✓] Network resources [639ms]
    • All expected network resources are available.

! Doctor found issues in 2 categories.

Searched for duplicates

GetRequiredFrameSize (0 results), CGSizeZero resize (0), ResizeSynchronizer (10, none about zero-layer frames), "Resize timed out" (1: #188493, closed as a duplicate of #155977 — a permanent freeze after app switching, not a per-step resize stall). #104638 covers app code blocking the main queue, which is the opposite direction. GetRequiredFrameSize is unchanged on main as of be9b204 (2026-07-27).

Activity

  1. saurabh-mirajkar commented on Jul 28, 2026

    @saurabh-mirajkar
    Contributor

    @edde746 Thanks for the detailed report. I can reproduce this with the sample provided on the latest stable Flutter release on macOS in debug, profile, and release modes when kPaintSomething = false. Labelling this as reproducible and routing it for further investigation.

  2. added
    engineflutter/engine related. See also e: labels.
    platform-macosBuilding on or for macOS specifically
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    team-macosOwned by the macOS platform team
    and removed
    in triagePresently being triaged by the triage team
    on Jul 28, 2026
  3. nyinyi740022900-stack commented on Jul 28, 2026

    @nyinyi740022900-stack

    Possibly related, but distinct enough to be worth noting: I'm seeing a permanent version of this (not just a resize stall) on a real production app, from the very first launch — no resize interaction involved at all.

    Setup: flutter create --platforms=macos,windows . on an existing Flutter app (offline-first POS, Material 3, no custom CALayers, no transparent Scaffold backgrounds — a fully opaque UI throughout).

    Symptom: flutter run -d macos (and flutter build macos && open build/.../Runner.app) launches a real NSWindow — it shows up in the app's Window menu, the process stays alive with near-zero CPU, no crash, no exception in the console — but the window never paints any content, in debug, profile, or release. Confirmed independently by a human looking at the physical screen and by automated screenshots (so it's not a screen-capture/compositor artifact). The console does print Running with merged UI and platform thread. Experimental. at launch.

    Ruled out:

    • Not a display/GPU issue — a plain AppKit app (TextEdit) renders perfectly on the same machine, and system_profiler SPDisplaysDataType reports full Metal 4 support (Apple M4).
    • Not specific to the app's own MainFlutterWindow.swift customizations — reproduces with the vanilla flutter create-generated window code too, and with an explicit setFrame(..., display: true) call removed entirely from awakeFromNib (tried both, same result — removing it entirely even seems to drop the window's Window menu entry, suggesting the content view controller attachment path is itself affected).
    • Not version-specific — reproduces identically on the current stable (3.44.5/3.44.8) and on 3.41.0 (~6 months older, installed via FVM), both printing the same merged UI and platform thread. Experimental. line.

    Environment: macOS 26.5.1 (25F80), Apple M4, engine hash 83675ed276 (matches the revision referenced in this issue).

    Given the resize-stall root cause described here — an empty/zero-content first frame permanently wedging ResizeSynchronizer's beginResize/contentSize == size handshake — it seems plausible a similar "frame committed as empty" state could be reached from a different path than the one described (transparent Scaffold + custom CALayer) and then never recover, even without any user resize interaction, if the very first frame race loses for any reason. I haven't been able to pin down what makes the very first frame register as "nothing to rasterize" in my case (the UI is fully opaque throughout), so I can't rule out this being a separate bug that only looks similar from the outside — flagging it here in case it's useful corroborating data, or in case a maintainer recognizes the pattern.

    Happy to provide a minimal reproduction repo if useful.

  4. added
    P2Important issues not at the top of the work list
    triaged-macosTriaged by the macOS platform team
    on Jul 31, 2026
  5. LouiseHsu commented on Jul 31, 2026

    @LouiseHsu
    Contributor

    Thank you for the detailed investigation - we are happy to take contributions! :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listengineflutter/engine related. See also e: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onplatform-macosBuilding on or for macOS specificallyteam-macosOwned by the macOS platform teamtriaged-macosTriaged by the macOS platform team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions