Open jmatth opened 2 years ago
Hi @jmatth. Thanks for filing the issue. I can reproduce this issue on latest stable
and master
channels.
Due to contextual_menu is a 3rd party plugin rather than Flutter's plugin, I've replaced context menu with a CupertinoContextMenu for demonstration.
Refer this to meta-issue: https://github.com/flutter/flutter/issues/74833
Great, thanks.
In my testing and the video you added CupertinoContextMenu
doesn't cause the same issue because it's rendered in Flutter rather than calling any AppKit APIs. To completely block rendering something needs to call NSMenu.popUp
, and as far as I can tell no built-in Flutter plugins do that.
It's not that I'm attached to contextual_menu
in particular, the issue is that this makes it impossible to write plugins for macOS that use a certain set of AppKit APIs without causing the app to freeze. If a separate issue needs to be filed to address this as a bug in the plugins system rather than / in addition to a performance bug please let me know and I'll do so. I suspect that any fix for the jank caused by interacting with the menu bar will also fix the popUp issue though.
I did a bit more testing and realized my initial estimate of ~0.2ms slowdown is probably invalid. I didn't realize that the engine compiles to x64 by default so testing with the default engine was running natively, while the version with my changes was running through Rosetta, which might account for the slowdown.
I tried recompiling to arm64 and testing again but frametimes aren't consistent enough to conclusively say whether there's a significant performance impact. I'm going to look into running the benchmarks I see in the engine repo with and without my changes. Assuming those show no major change it seems like running the resize callback on the USER_INTERACTIVE
queue could be a viable fix.
cc @cbracken
The TL;DR is that we advise against blocking the platform thread (the macOS main thread) in the Engine architecture wiki. That thread is responsible for keyboard events, pointer events, etc. so blocking that thread may cause those events to be lost. You can see some previous discussion of this on this issue: https://github.com/flutter/flutter/issues/22024#issuecomment-465289341
That said, we almost certainly shouldn't be blocking existing Dart-driven animation; that's a bug. Thanks for reporting this.
Possibly related, although to my knowledge I'm not using AppKit: I'm having very long FlutterCompositorPresentLayers
on Metal (that is, both iOS and macOS ARM/Intel), even on high-end devices. This occurs when a layer in flutter_map
is rebuilt using a StreamBuilder
. These frames can be several times longer than my frame budget, and SceneDisplayLag
is significant.
I can cause similar rebuilds by interacting with the map, in which case I'm well within the frame budget. The StreamBuilder
is rebuilt when data is received over the network and sent over the stream. If the StreamBuilder
rebuild occurs as the user is interacting with the map I'm thinking it might cause jank. All these slow frames are also cluttering the dev tools, as I'm receiving data pretty frequently.
Even on low-end Windows machines the performance is more predictable and within frame budget. This is recurring so I'm assuming it's not related to shader compilation jank.
Tried compiling with Impeller on iOS just for fun (Flutter 3.3.0), but both performance and features were too lacking for the app to function properly.
EDIT: No jank, just skipped frames, but that is also not ideal since the layer doesn't update when the user interacts with the map.
Mark! I have the same issue.
We're calling [NSMenu popUpMenuPositioningItem:]
in NativeShell all the time without any issues. I think the problem in your case is that you're blocking the main dispatch queue.
Unlike CFRunLoop, dispatch queues are not reentrant. When a queue callback is blocked, no other dispatch queue callback is delivered. Flutter uses dispatch queue to schedule platform channel calls as well as screen updates.
When you call [NSMenu popup] from platform channel callback, you will block the main dispatch queue so none of these are delivered.
You should be able to reschedule the call to the main run loop directly, (i.e. using [NSRunLoop performBlock:]
and call [NSMenu popup]
from this block. That way you'll free the dispatch queue. [NSMenu popup]
will run the even loop, ensuring the main displatch queue is drained properly.
Thanks @knopp! You were absolutely right that moving the call to the main run loop fixed the issue.
For anyone else in this thread with the same issue, here's what worked for me in Swift:
// Wrapper class to pass closures to Objective-C APIs that take selectors taken from https://stackoverflow.com/a/36983811/1988017
final class Action: NSObject {
private let _action: () -> ()
init(action: @escaping () -> ()) {
_action = action
super.init()
}
@objc func action() {
_action()
}
}
//...
let action = Action(action: {
self.menu?.popUp(...)
})
RunLoop.current.perform(
#selector(action.action),
target: action,
argument: nil,
order: 0,
modes: [RunLoop.Mode.common]
)
Once Flutter completes its ongoing migration to only support macOS 10.13+, it should be possible to simplify this code to use a more idiomatic version of perform
that was introduced in macOS 10.12:
RunLoop.current.perform {
menu.popUp(...)
}
This only addresses plugins calling NSMenu.popUp
and similar APIs, and not the jank observed when closing menus in the menubar on macOS. I need to take some time to poke around that part of the engine and see if the same or a similar fix can be implemented there.
The triaged-desktop
label is irrelevant if there is no team-desktop
label or fyi-desktop
label.
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
. Debugging the engine eventually leads toFlutterResizeSynchronizer.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 ofUSER_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
codeSteps to reproduce
flutter run -d macos
on the code sample on a macOS machineSummon menu
buttonView
(or any other item) in the menu barExpected results:
Actual results:
Code sample
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: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
Fixed with custom engine build
https://user-images.githubusercontent.com/1316184/170316394-6f7332de-3f70-4967-b432-ab3b3ff75165.mov
Timeline trace: custom_engine.json.txt
Target Platform: macOS Target OS version/browser: 12.4 Devices: Physical 14 inch M1 Pro Macbook Pro
Logs
Logs
``` 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) ``` ``` 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! ```