Skip to content

[flutter_tools] add preHotRestart callbacks - #86134

Closed
jonahwilliams wants to merge 11 commits into
flutter:masterfrom
jonahwilliams:hot_restart_hook
Closed

jonahwilliams wants to merge 11 commits into
flutter:masterfrom
jonahwilliams:hot_restart_hook

Conversation

@jonahwilliams

@jonahwilliams jonahwilliams commented Jul 8, 2021 •

Copy link
Copy Markdown
Contributor

Fixes #75528

Adds a new API to register a callback that will be invoked + awaited before a hot restart is initiated:

class Foo {
  Foo() {
    var data = acquireNativeResource();
   if (kIsDebugMode) {
     WidgetsBinding.instance!.registerHotRestartCallback(data.release, debugLabel: 'Foo Native Resource Release');
   }
  }
}

With this I was able to wire up the folloing to allow me to hot restart my little roguelike without crashing:

class SoundSystemImpl implements SoundSystem {
  SoundSystemImpl() {
    _init();
  }

  final _wavs = <String, Pointer<Wav>>{};
  Point _listener = Point.zero;

  late final Pointer<Soloud> _audioContext;

  void _init() {
    if (!kSoundEnabled) {
      return;
    }
    _audioContext = _soloudCreate();
    _soloudInitEx(
        _audioContext,
        SOLOUD_CLIP_ROUNDOFF | SOLOUD_ENABLE_VISUALIZATION,
        SOLOUD_AUTO,
        SOLOUD_AUTO,
        SOLOUD_AUTO,
        SOLOUD_AUTO);
    _soloudSetGlobalVolume(_audioContext, 2);
    WidgetsBinding.instance!.registerHotRestartCallback(_destroy, debugLabel: 'Soloud'); // NEW
  }

  void _destroy() {
    _soloudDeInit(_audioContext);
    _soloudDestroy(_audioContext);
  }

@flutter-dashboard flutter-dashboard Bot added framework flutter/packages/flutter repository. See also f: labels. tool Affects the "flutter" command-line tool. See also t: labels. labels Jul 8, 2021
@google-cla google-cla Bot added the cla: yes label Jul 8, 2021
@jonahwilliams
jonahwilliams marked this pull request as ready for review July 9, 2021 00:47
@jonahwilliams
jonahwilliams requested review from Hixie and zanderso July 9, 2021 00:47
///
/// This can be used to release native resources acquired through platform
/// channels or `dart:ffi`.
void registerHotRestartCallback(PreHotRestartCallback callback, {String? debugLabel}) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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...)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the more documentation the better!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

@jonahwilliams jonahwilliams Jul 9, 2021 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Future<void>? is probably better. Or just Future<void> since really it's trivial to add async to a function.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object? will also accept either a future returning function or a void function/tear-off, i'll switch to that

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

///
/// This can be used to release native resources acquired through platform
/// channels or `dart:ffi`.
void registerHotRestartCallback(PreHotRestartCallback callback, {String? debugLabel}) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can simplify some of the code here by making debugLabel have a default value ("unknown" or something)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


await Future.wait(<Future<void>>[
for (MapEntry<PreHotRestartCallback, String?> entry in _hotRestartCallbacks.entries)
invokeAndWait(entry.key, entry.value).timeout(const Duration(seconds: 5), onTimeout: () {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))))');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh i see, wow. how did that not get caught before.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is count even resolving to here?!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aaah

try {
await callback();
} catch (error) {
debugPrint('Failed to invoke preHotRestartCallback ${label ?? 'unlabeled'}: $error');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

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...');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'd probably put the name in quotes

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@Hixie

Hixie commented Jul 9, 2021

Copy link
Copy Markdown
Contributor

(re the customer_testing-linux failure, #86143 to add more debugging)

@zanderso

zanderso commented Jul 9, 2021

Copy link
Copy Markdown
Member

@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.

@jonahwilliams

Copy link
Copy Markdown
Contributor Author

Still working on changing how timeouts work

@jonahwilliams
jonahwilliams requested a review from Hixie July 12, 2021 18:10
@jonahwilliams

Copy link
Copy Markdown
Contributor Author

This is ready for another review

@zanderso zanderso left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tool changes lgtm

@jonahwilliams

Copy link
Copy Markdown
Contributor Author

Friendly ping @Hixie

/// /// be re-acquired.
/// late final Pointer _context = _createContext();
/// }
/// ```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you know if this code is getting analyzed?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why postEvent/postEvent rather than Timeline.startSync/finishSync?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(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.
///

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these docs should say what happens if you register the same callback twice

FlutterRunTestDriver flutter;

setUp(() async {
tempDir = createResolvedTempDirectorySync('hot_reload_test.');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style guide says temp directories must start with flutter_

@Hixie

Hixie commented Jul 22, 2021

Copy link
Copy Markdown
Contributor

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();
/// }
/// ```

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe also document that the callback is persistent?

/// class NativeResourceService {
/// NativeResourceService() {
/// if (kDebugMode) {
/// WidgetsBinding.instance!.registerHotRestartCallback(() => _destroyContext(_context), debugLabel: 'NativeResourceService');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

@christopherfujino

Copy link
Copy Markdown
Contributor

@jonahwilliams are you still working on this one?

@christopherfujino christopherfujino added waiting for tree to go green waiting for response The Flutter team cannot make further progress on this issue until the original reporter responds and removed waiting for tree to go green labels Aug 12, 2021
@jonahwilliams

Copy link
Copy Markdown
Contributor Author

I'm not, closing

@jonahwilliams
jonahwilliams deleted the hot_restart_hook branch August 12, 2021 22:14
@jonahwilliams
jonahwilliams restored the hot_restart_hook branch September 5, 2021 19:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

framework flutter/packages/flutter repository. See also f: labels. tool Affects the "flutter" command-line tool. See also t: labels. waiting for response The Flutter team cannot make further progress on this issue until the original reporter responds

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add hot restart hooks to support dart:ffi

5 participants