diff --git a/packages/flutter/lib/src/scheduler/binding.dart b/packages/flutter/lib/src/scheduler/binding.dart index 41dce5f7ff..d06f4bbd28 100644 --- a/packages/flutter/lib/src/scheduler/binding.dart +++ b/packages/flutter/lib/src/scheduler/binding.dart @@ -5,7 +5,7 @@ import 'dart:async'; import 'dart:collection'; import 'dart:developer' show Flow, Timeline, TimelineTask; -import 'dart:ui' show AppLifecycleState, FramePhase, FrameTiming, PlatformDispatcher, TimingsCallback; +import 'dart:ui' show AppLifecycleState, DartPerformanceMode, FramePhase, FrameTiming, PlatformDispatcher, TimingsCallback; import 'package:collection/collection.dart' show HeapPriorityQueue, PriorityQueue; import 'package:flutter/foundation.dart'; @@ -183,6 +183,34 @@ enum SchedulerPhase { postFrameCallbacks, } +/// This callback is invoked when a request for [DartPerformanceMode] is disposed. +/// +/// See also: +/// +/// * [PerformanceModeRequestHandle] for more information on the lifecycle of the handle. +typedef _PerformanceModeCleaupCallback = VoidCallback; + +/// An opaque handle that keeps a request for [DartPerformanceMode] active until +/// disposed. +/// +/// To create a [PerformanceModeRequestHandle], use [SchedulerBinding.requestPerformanceMode]. +/// The component that makes the request is responsible for disposing the handle. +class PerformanceModeRequestHandle { + PerformanceModeRequestHandle._(_PerformanceModeCleaupCallback this._cleanup); + + _PerformanceModeCleaupCallback? _cleanup; + + /// Call this method to signal to [SchedulerBinding] that a request for a [DartPerformanceMode] + /// is no longer needed. + /// + /// This method must only be called once per object. + void dispose() { + assert(_cleanup != null); + _cleanup!(); + _cleanup = null; + } +} + /// Scheduler for running the following: /// /// * _Transient callbacks_, triggered by the system's @@ -605,6 +633,20 @@ mixin SchedulerBinding on BindingBase { return true; } + /// Asserts that there are no pending performance mode requests in debug mode. + /// + /// Throws a [FlutterError] if there are pending performance mode requests, + /// as this indicates a potential memory leak. + bool debugAssertNoPendingPerformanceModeRequests(String reason) { + assert(() { + if (_performanceMode != null) { + throw FlutterError(reason); + } + return true; + }()); + return true; + } + /// Prints the stack for where the current transient callback was registered. /// /// A transient frame callback is one that was registered with @@ -1085,6 +1127,59 @@ mixin SchedulerBinding on BindingBase { } } + DartPerformanceMode? _performanceMode; + int _numPerformanceModeRequests = 0; + + /// Request a specific [DartPerformanceMode]. + /// + /// Returns `null` if the request was not successful due to conflicting performance mode requests. + /// Two requests are said to be in conflict if they are not of the same [DartPerformanceMode] type, + /// and an explicit request for a performance mode has been made prior. + /// + /// Requestor is responsible for calling [PerformanceModeRequestHandle.dispose] when it no longer + /// requires the performance mode. + PerformanceModeRequestHandle? requestPerformanceMode(DartPerformanceMode mode) { + // conflicting requests are not allowed. + if (_performanceMode != null && _performanceMode != mode) { + return null; + } + + if (_performanceMode == mode) { + assert(_numPerformanceModeRequests > 0); + _numPerformanceModeRequests++; + } else if (_performanceMode == null) { + assert(_numPerformanceModeRequests == 0); + _performanceMode = mode; + _numPerformanceModeRequests = 1; + } + + return PerformanceModeRequestHandle._(_disposePerformanceModeRequest); + } + + /// Remove a request for a specific [DartPerformanceMode]. + /// + /// If all the pending requests have been disposed, the engine will revert to the + /// [DartPerformanceMode.balanced] performance mode. + void _disposePerformanceModeRequest() { + _numPerformanceModeRequests--; + if (_numPerformanceModeRequests == 0) { + _performanceMode = null; + PlatformDispatcher.instance.requestDartPerformanceMode(DartPerformanceMode.balanced); + } + } + + /// Returns the current [DartPerformanceMode] requested or `null` if no requests have + /// been made. + /// + /// This is only supported in debug and profile modes, returns `null` in release mode. + DartPerformanceMode? debugGetRequestedPerformanceMode() { + if (!(kDebugMode || kProfileMode)) { + return null; + } else { + return _performanceMode; + } + } + /// Called by the engine to produce a new frame. /// /// This method is called immediately after [handleBeginFrame]. It calls all diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart index e0606556aa..b258842b62 100644 --- a/packages/flutter/lib/src/widgets/routes.dart +++ b/packages/flutter/lib/src/widgets/routes.dart @@ -108,6 +108,14 @@ abstract class TransitionRoute extends OverlayRoute { Future get completed => _transitionCompleter.future; final Completer _transitionCompleter = Completer(); + /// Handle to the performance mode request. + /// + /// When the route is animating, the performance mode is requested. It is then + /// disposed when the animation ends. Requesting [DartPerformanceMode.latency] + /// indicates to the engine that the transition is latency sensitive and to delay + /// non-essential work while this handle is active. + PerformanceModeRequestHandle? _performanceModeRequestHandle; + /// {@template flutter.widgets.TransitionRoute.transitionDuration} /// The duration the transition going forwards. /// @@ -221,12 +229,17 @@ abstract class TransitionRoute extends OverlayRoute { if (overlayEntries.isNotEmpty) { overlayEntries.first.opaque = opaque; } + _performanceModeRequestHandle?.dispose(); + _performanceModeRequestHandle = null; break; case AnimationStatus.forward: case AnimationStatus.reverse: if (overlayEntries.isNotEmpty) { overlayEntries.first.opaque = false; } + _performanceModeRequestHandle ??= + SchedulerBinding.instance + .requestPerformanceMode(ui.DartPerformanceMode.latency); break; case AnimationStatus.dismissed: // We might still be an active route if a subclass is controlling the @@ -236,6 +249,8 @@ abstract class TransitionRoute extends OverlayRoute { if (!isActive) { navigator!.finalizeRoute(this); _popFinalized = true; + _performanceModeRequestHandle?.dispose(); + _performanceModeRequestHandle = null; } break; } @@ -465,6 +480,8 @@ abstract class TransitionRoute extends OverlayRoute { void dispose() { assert(!_transitionCompleter.isCompleted, 'Cannot dispose a $runtimeType twice.'); _animation?.removeStatusListener(_handleStatusChanged); + _performanceModeRequestHandle?.dispose(); + _performanceModeRequestHandle = null; if (willDisposeAnimationController) { _controller?.dispose(); } diff --git a/packages/flutter/test/cupertino/nav_bar_transition_test.dart b/packages/flutter/test/cupertino/nav_bar_transition_test.dart index dc4dbebc38..f739bc5759 100644 --- a/packages/flutter/test/cupertino/nav_bar_transition_test.dart +++ b/packages/flutter/test/cupertino/nav_bar_transition_test.dart @@ -9,8 +9,11 @@ // Fails with "flutter test --test-randomize-ordering-seed=456" @Tags(['no-shuffle']) +import 'dart:ui'; + import 'package:flutter/cupertino.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_test/flutter_test.dart'; Future startTransitionBetween( @@ -443,6 +446,26 @@ void main() { ); }); + testWidgets('DartPerformanceMode is latency mid-animation', (WidgetTester tester) async { + DartPerformanceMode? mode; + + // before the animation starts, no requests are active. + mode = SchedulerBinding.instance.debugGetRequestedPerformanceMode(); + expect(mode, isNull); + + await startTransitionBetween(tester, fromTitle: 'Page 1'); + + // mid-transition, latency mode is expected. + await tester.pump(const Duration(milliseconds: 50)); + mode = SchedulerBinding.instance.debugGetRequestedPerformanceMode(); + expect(mode, equals(DartPerformanceMode.latency)); + + // end of transitio, go back to no requests active. + await tester.pump(const Duration(milliseconds: 500)); + mode = SchedulerBinding.instance.debugGetRequestedPerformanceMode(); + expect(mode, isNull); + }); + testWidgets('Multiple nav bars tags do not conflict if in different navigators', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( diff --git a/packages/flutter/test/scheduler/performance_mode_test.dart b/packages/flutter/test/scheduler/performance_mode_test.dart new file mode 100644 index 0000000000..7a44796cd0 --- /dev/null +++ b/packages/flutter/test/scheduler/performance_mode_test.dart @@ -0,0 +1,56 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui'; + +import 'package:flutter/scheduler.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + late SchedulerBinding binding; + + setUpAll(() { + WidgetsFlutterBinding.ensureInitialized(); + binding = SchedulerBinding.instance; + }); + + test('PerformanceModeHandler make one request', () async { + final PerformanceModeRequestHandle? requestHandle = binding.requestPerformanceMode(DartPerformanceMode.latency); + expect(requestHandle, isNotNull); + expect(binding.debugGetRequestedPerformanceMode(), equals(DartPerformanceMode.latency)); + requestHandle?.dispose(); + expect(binding.debugGetRequestedPerformanceMode(), isNull); + }); + + test('PerformanceModeHandler make conflicting requests', () async { + final PerformanceModeRequestHandle? requestHandle1 = binding.requestPerformanceMode(DartPerformanceMode.latency); + expect(requestHandle1, isNotNull); + + final PerformanceModeRequestHandle? requestHandle2 = binding.requestPerformanceMode(DartPerformanceMode.throughput); + expect(requestHandle2, isNull); + + expect(binding.debugGetRequestedPerformanceMode(), equals(DartPerformanceMode.latency)); + + requestHandle1?.dispose(); + expect(binding.debugGetRequestedPerformanceMode(), isNull); + }); + + test('PerformanceModeHandler revert only after last requestor disposed', + () async { + final PerformanceModeRequestHandle? requestHandle1 = binding.requestPerformanceMode(DartPerformanceMode.latency); + expect(requestHandle1, isNotNull); + + expect(binding.debugGetRequestedPerformanceMode(), equals(DartPerformanceMode.latency)); + + final PerformanceModeRequestHandle? requestHandle2 = binding.requestPerformanceMode(DartPerformanceMode.latency); + expect(requestHandle2, isNotNull); + + expect(binding.debugGetRequestedPerformanceMode(), equals(DartPerformanceMode.latency)); + requestHandle1?.dispose(); + expect(binding.debugGetRequestedPerformanceMode(), equals(DartPerformanceMode.latency)); + requestHandle2?.dispose(); + expect(binding.debugGetRequestedPerformanceMode(), isNull); + }); +} diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index a20d6fce24..758b32ce0a 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -885,6 +885,9 @@ abstract class TestWidgetsFlutterBinding extends BindingBase assert(debugAssertNoTransientCallbacks( 'An animation is still running even after the widget tree was disposed.' )); + assert(debugAssertNoPendingPerformanceModeRequests( + 'A performance mode was requested and not disposed by a test.' + )); assert(debugAssertAllFoundationVarsUnset( 'The value of a foundation debug variable was changed by the test.', debugPrintOverride: debugPrintOverride, diff --git a/packages/integration_test/test/binding_test.dart b/packages/integration_test/test/binding_test.dart index 79e2b6b4a2..b8ad8fe1ff 100644 --- a/packages/integration_test/test/binding_test.dart +++ b/packages/integration_test/test/binding_test.dart @@ -39,6 +39,7 @@ Future main() async { )); expect(tester.binding, binding); binding.reportData = {'answer': 42}; + await tester.pump(); }); testWidgets('hitTesting works when using setSurfaceSize', (WidgetTester tester) async {