Repository: flutter
Issue: AppKit APIs that block the main queue cause UI to hang on macOS
Labels: a: animation,c: performance,platform-mac,a: desktop,d: devtools,perf: speed,has reproducible steps,P2,found in release: 3.0,found in release: 3.1,team-macos,triaged-macos

## Details

### Full description

It appears there are some operations within AppKit on macOS that can noticeably
delay or entirely block the main queue.  This manifests in Flutter as jank or
the UI completely hanging.  The two relevant actions I have found are
interacting with the menu bar (jank) and showing a native popup menu (hang).

Running in profile mode shows that the hang occurs in
[`FlutterCompositorPresentLayers`][present_layers]. Debugging the engine
eventually leads to [`FlutterResizeSynchronizer.requestCommit`][requestCommit].
This function attempts to dispatch some work on the main queue and wait for it
to complete. If the main queue is blocked, this causes the UI to hang.

To test I changed the `requestCommit` to run its work on a background queue with
the highest QoS of `USER_INTERACTIVE` (see below). This resolved the issue and
allowed the UI to run smoothly while popup menus were opened and the menu bar
was interacted with. ~However, it did seem to have a performance impact as UI
frametimes went from ~0.2ms to ~0.4ms in the profiler while showing a single
loader animation~. Edit: Upon further investigation it's not clear whether this has
a significant performance impact. My initial results are invalid because I did not
realize the engine compiles only for x64 by default, so the 0.2ms slowdown
could be attributed to running through Rosetta. I haven't found a consistent way
to compare the performance before and after this change. Someone familiar
with profiling the engine performance would likely have better luck.

I also attempted to fix the issue on the plugin side by triggering the popup menu
in a background queue. This is apparently disallowed as exceptions were thrown from
inside of AppKit every time I tried. It is my understanding that this is not an
uncommon restriction for Apple to place on certain APIs in their platforms.

#### Modified `requestCommit` code

```diff
--- a/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.mm
+++ b/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.mm
@@ -121,7 +121,7 @@
     // No resize, schedule commit on platform thread and wait until either done
     // or interrupted by incoming BeginResize
     [_delegate resizeSynchronizerFlush:self];
-    dispatch_async(dispatch_get_main_queue(), [self, cookie = _cookie] {
+    dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), [self, cookie = _cookie] {
       std::unique_lock<std::mutex> lock(_mutex);
       if (cookie == _cookie) {
         if (_delegate) {
```

### Steps to reproduce

1. Execute `flutter run -d macos` on the code sample on a macOS machine
2. Click the `Summon menu` button
3. Click elsewhere to dismiss the contextual menu that appeared
4. With the app still focused, click `View` (or any other item) in the menu bar
5. Click outside the open menu to close it

**Expected results:** <!-- what did you want to see? -->

- The app animations should continue running while the popup menu is open
- The app animations should run smoothly throughout the interaction with the menu bar

**Actual results:** <!-- what did you see? -->

- The app animations stop entirely while the popup menu is open
- The app animations jank when the menu bar menu is closed

<details>
<summary>Code sample</summary>

As some of the reproduction requires a plugin package to trigger the macOS
context menu, I have created a git repo with the full reproduction here:
[jmatth/flutter_macos_hang_repro][repro_repo]. The contents of `lib/main.dart`
from that repo are below. It should work as long as the `contextual_menu` plugin
is installed.

```dart
import 'package:contextual_menu/contextual_menu.dart' as cm;
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Main thread block repro',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({
    super.key,
    required this.title,
  });

  final String title;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            // const Placeholder(),
            const CircularProgressIndicator(),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                cm.popUpContextualMenu(cm.Menu(
                  items: [
                    cm.MenuItem(
                      label: 'Item 1',
                    ),
                  ],
                ));
              },
              child: const Text(
                'Summon menu',
              ),
            ),
          ],
        ),
      ),
    );
  }
}

```

</details>

### Video recording:

As I already traced to root cause and it is not related to constrained system
resources, I used the screen record function on macOS instead of recording the
monitor with my phone. If you feel it is still necessary to record with an
external device please let me know.

Also sorry for the `.json.txt` files for the timeline traces, github didn't like `.json`
files for some reason.

#### Hang and jank

https://user-images.githubusercontent.com/1316184/170315748-73b363aa-c7cc-493d-b2b3-869092908457.mov

Timeline trace: 
[jank-timeline.json.txt](https://github.com/flutter/flutter/files/8773047/jank-timeline.json.txt)


#### Fixed with custom engine build

https://user-images.githubusercontent.com/1316184/170316394-6f7332de-3f70-4967-b432-ab3b3ff75165.mov

Timeline trace:
[custom_engine.json.txt](https://github.com/flutter/flutter/files/8773053/custom_engine.json.txt)

<!--
     Please tell us which target platform(s) the problem occurs (Android / iOS / Web / macOS / Linux / Windows)
     Which target OS version, for Web, browser, is the test system running?
     Does the problem occur on emulator/simulator as well as on physical devices?
-->

**Target Platform: macOS**
**Target OS version/browser: 12.4**
**Devices: Physical 14 inch M1 Pro Macbook Pro**

## Logs

<details>
<summary>Logs</summary>

<!--
     Run `flutter analyze` and attach any output of that command below.
     If there are any analysis errors, try resolving them before filing this issue.
-->

```
Warning: You are using these overridden dependencies:
! contextual_menu 0.1.1 from git https://github.com/jmatth/contextual_menu.git at 88ff51
! menu_base 0.1.0 from git https://github.com/jmatth/menu_base.git at 2b24c8
Running "flutter pub get" in flutter_macos_hang_repro...           417ms
Analyzing flutter_macos_hang_repro...                                   
No issues found! (ran in 3.0s)
```

<!-- Finally, paste the output of running `flutter doctor -v` here, with your device plugged in. -->

```
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel master, 3.1.0-0.0.pre.902, on macOS 12.4 21F79 darwin-arm, locale en-US)
[✓] Android toolchain - develop for Android devices (Android SDK version 32.1.0-rc1)
[✓] Xcode - develop for iOS and macOS (Xcode 13.3.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2021.2)
[✓] Android Studio (version 2021.1)
[✓] VS Code (version 1.67.2)
[✓] Connected device (2 available)
[✓] HTTP Host Availability

• No issues found!
```

</details>

[repro_repo]: https://github.com/jmatth/flutter_macos_hang_repro
[present_layers]: https://github.com/flutter/engine/blob/91642700b6d62d0221a5fbaa1110ee0040d40526/shell/platform/embedder/embedder.cc#L905-L912
[requestCommit]: https://github.com/flutter/engine/blob/91642700b6d62d0221a5fbaa1110ee0040d40526/shell/platform/darwin/macos/framework/Source/FlutterResizeSynchronizer.mm#L121-L134