Repository navigation
[flutter_tools] add preHotRestart callbacks - #86134
jonahwilliams wants to merge 11 commits into
Conversation
| /// | ||
| /// This can be used to release native resources acquired through platform | ||
| /// channels or `dart:ffi`. | ||
| void registerHotRestartCallback(PreHotRestartCallback callback, {String? debugLabel}) { |
There was a problem hiding this comment.
I could give a much larger example of how to use this based on some dart:ffi native code (since I have an implementation that uses this sitting right here...)
There was a problem hiding this comment.
the more documentation the better!
There was a problem hiding this comment.
done-ish (could add the native code example too)
| export 'dart:ui' show AppLifecycleState, Locale; | ||
|
|
||
| /// A callback that can be registered with [WidgetsBinding.registerHotRestartCallback]. | ||
| typedef PreHotRestartCallback = FutureOr<void> Function(); |
There was a problem hiding this comment.
I don't usually like FutureOr, but in this case since we want to support async functions, the futureOr lets you pass tear-offs of void returning methods without wrapping in another function.
var foos = <int>[];
...
WidgetsBinding.instance!.registerHotRestartCallback(foos.clear);
// vs
WidgetsBinding.instance!.registerHotRestartCallback(() async => foos.clear());There was a problem hiding this comment.
Future<void>? is probably better. Or just Future<void> since really it's trivial to add async to a function.
There was a problem hiding this comment.
Object? will also accept either a future returning function or a void function/tear-off, i'll switch to that
| /// | ||
| /// This can be used to release native resources acquired through platform | ||
| /// channels or `dart:ffi`. | ||
| void registerHotRestartCallback(PreHotRestartCallback callback, {String? debugLabel}) { |
There was a problem hiding this comment.
you can simplify some of the code here by making debugLabel have a default value ("unknown" or something)
|
|
||
| await Future.wait(<Future<void>>[ | ||
| for (MapEntry<PreHotRestartCallback, String?> entry in _hotRestartCallbacks.entries) | ||
| invokeAndWait(entry.key, entry.value).timeout(const Duration(seconds: 5), onTimeout: () { |
There was a problem hiding this comment.
If we have timeouts here, we really should do them on the tool side... but I would just remove the timeouts entirely.
Optionally, we could make a second hot restart request while one is already ongoing just force a hot restart without triggering these callbacks.
There was a problem hiding this comment.
I can remove the timeouts here, possibly use the VM service notification API to let the tool know which callbacks are pending. then the timeout warning could include what exactly is stuck.
Rather than a second hot restart, we could prompt to continue ignoring the callbacks as long as a terminal is attached. If one of these is caught in an infinite loop though, there isn't anything we can do (though that is true of other parts of the framework too: #85987)
There was a problem hiding this comment.
Prompting requires more work to make this work in IDEs. Forcing the hot restart is always what you're going to want, as a developer. When would you ever say "no, I'd rather not do the thing I asked for actually, just continue waiting on this thing that's never going to finish"? :-)
| // printHotReloadWorked(); | ||
|
|
||
| print('((((TICK $count))))'); | ||
| print('((((TICK \$count))))'); |
There was a problem hiding this comment.
oh i see, wow. how did that not get caught before.
There was a problem hiding this comment.
what is count even resolving to here?!
There was a problem hiding this comment.
oh, this was previously a raw string so that was safe, but I want to conditionally embed some code so I switched it out to a regular string and escaped the count interpolation to keep the same behavior.
| try { | ||
| await callback(); | ||
| } catch (error) { | ||
| debugPrint('Failed to invoke preHotRestartCallback ${label ?? 'unlabeled'}: $error'); |
There was a problem hiding this comment.
we have logic for this (FlutterError and so forth). If for some reason we can't use it here it would be good to leave a comment saying why.
| await Future.wait(<Future<void>>[ | ||
| for (MapEntry<PreHotRestartCallback, String?> entry in _hotRestartCallbacks.entries) | ||
| invokeAndWait(entry.key, entry.value).timeout(const Duration(seconds: 5), onTimeout: () { | ||
| debugPrint('preHotRestartCallback ${entry.value ?? 'unlabeled'} is taking longer than expected...'); |
There was a problem hiding this comment.
i'd probably put the name in quotes
|
(re the customer_testing-linux failure, #86143 to add more debugging) |
|
@chaselatta @arbreng @jxson This PR provides a hook that could also be useful for getting Hot Restart to work on Fuchsia, for example for tearing down handles that would otherwise leak and/or whose initialization is not idempotent. |
|
Still working on changing how timeouts work |
|
This is ready for another review |
|
Friendly ping @Hixie |
| /// /// be re-acquired. | ||
| /// late final Pointer _context = _createContext(); | ||
| /// } | ||
| /// ``` |
There was a problem hiding this comment.
do you know if this code is getting analyzed?
There was a problem hiding this comment.
In its current form, this is most likely not analyzed. It should be wrapped in a {@tool snippet ...} or {@tool sample ...} section, so the code here doesn't become stale.
| export 'dart:ui' show AppLifecycleState, Locale; | ||
|
|
||
| /// A callback that can be registered with [WidgetsBinding.registerHotRestartCallback]. | ||
| typedef PreHotRestartCallback = Object? Function(); |
There was a problem hiding this comment.
This should document what the return value should be. It's very unusual for an API to just allow any return value.
(I don't understand why this can't just be void or Future<void> though?)
| ) | ||
| ); | ||
| } finally { | ||
| developer.postEvent('preHotRestartCallback', <String, Object>{'label': label, 'finished': true}); |
There was a problem hiding this comment.
why postEvent/postEvent rather than Timeline.startSync/finishSync?
There was a problem hiding this comment.
(I don't see this event being used anywhere?)
| /// tearing down a native resource acquired through `dart:ffi`. In this example, if | ||
| /// the `context` pointer is not passed through to the `_destroyContext` function before | ||
| /// a hot restart, the application will crash after a hot restart. | ||
| /// |
There was a problem hiding this comment.
these docs should say what happens if you register the same callback twice
| FlutterRunTestDriver flutter; | ||
|
|
||
| setUp(() async { | ||
| tempDir = createResolvedTempDirectorySync('hot_reload_test.'); |
There was a problem hiding this comment.
style guide says temp directories must start with flutter_
|
cc @goderbauer since this is core framework stuff (feel free to land if you've addressed my comments and @goderbauer gives you an LGTM at some future point). |
| /// /// be re-acquired. | ||
| /// late final Pointer _context = _createContext(); | ||
| /// } | ||
| /// ``` |
There was a problem hiding this comment.
In its current form, this is most likely not analyzed. It should be wrapped in a {@tool snippet ...} or {@tool sample ...} section, so the code here doesn't become stale.
| if (!kDebugMode) { | ||
| return; | ||
| } | ||
| _hotRestartCallbacks[callback] = debugLabel; |
There was a problem hiding this comment.
Maybe document that registering the same callback multiple times will only invoke it once (which differs from callback behavior we have in many other places of the framework).
There was a problem hiding this comment.
Maybe also document that the callback is persistent?
| /// class NativeResourceService { | ||
| /// NativeResourceService() { | ||
| /// if (kDebugMode) { | ||
| /// WidgetsBinding.instance!.registerHotRestartCallback(() => _destroyContext(_context), debugLabel: 'NativeResourceService'); |
There was a problem hiding this comment.
How do I unregister this callback if I no longer need it? (e.g. because I've manually disposed this service as it's no longer needed for the functionality of my app)
|
@jonahwilliams are you still working on this one? |
|
I'm not, closing |
Fixes #75528
Adds a new API to register a callback that will be invoked + awaited before a hot restart is initiated:
With this I was able to wire up the folloing to allow me to hot restart my little roguelike without crashing: