Request DartPerformanceMode.latency
during transitions (#110600)
This commit is contained in:
parent
547de47a93
commit
d5f372bccd
@ -5,7 +5,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
import 'dart:developer' show Flow, Timeline, TimelineTask;
|
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:collection/collection.dart' show HeapPriorityQueue, PriorityQueue;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
@ -183,6 +183,34 @@ enum SchedulerPhase {
|
|||||||
postFrameCallbacks,
|
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:
|
/// Scheduler for running the following:
|
||||||
///
|
///
|
||||||
/// * _Transient callbacks_, triggered by the system's
|
/// * _Transient callbacks_, triggered by the system's
|
||||||
@ -605,6 +633,20 @@ mixin SchedulerBinding on BindingBase {
|
|||||||
return true;
|
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.
|
/// Prints the stack for where the current transient callback was registered.
|
||||||
///
|
///
|
||||||
/// A transient frame callback is one that was registered with
|
/// 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.
|
/// Called by the engine to produce a new frame.
|
||||||
///
|
///
|
||||||
/// This method is called immediately after [handleBeginFrame]. It calls all
|
/// This method is called immediately after [handleBeginFrame]. It calls all
|
||||||
|
@ -108,6 +108,14 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
|
|||||||
Future<T?> get completed => _transitionCompleter.future;
|
Future<T?> get completed => _transitionCompleter.future;
|
||||||
final Completer<T?> _transitionCompleter = Completer<T?>();
|
final Completer<T?> _transitionCompleter = Completer<T?>();
|
||||||
|
|
||||||
|
/// 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}
|
/// {@template flutter.widgets.TransitionRoute.transitionDuration}
|
||||||
/// The duration the transition going forwards.
|
/// The duration the transition going forwards.
|
||||||
///
|
///
|
||||||
@ -221,12 +229,17 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
|
|||||||
if (overlayEntries.isNotEmpty) {
|
if (overlayEntries.isNotEmpty) {
|
||||||
overlayEntries.first.opaque = opaque;
|
overlayEntries.first.opaque = opaque;
|
||||||
}
|
}
|
||||||
|
_performanceModeRequestHandle?.dispose();
|
||||||
|
_performanceModeRequestHandle = null;
|
||||||
break;
|
break;
|
||||||
case AnimationStatus.forward:
|
case AnimationStatus.forward:
|
||||||
case AnimationStatus.reverse:
|
case AnimationStatus.reverse:
|
||||||
if (overlayEntries.isNotEmpty) {
|
if (overlayEntries.isNotEmpty) {
|
||||||
overlayEntries.first.opaque = false;
|
overlayEntries.first.opaque = false;
|
||||||
}
|
}
|
||||||
|
_performanceModeRequestHandle ??=
|
||||||
|
SchedulerBinding.instance
|
||||||
|
.requestPerformanceMode(ui.DartPerformanceMode.latency);
|
||||||
break;
|
break;
|
||||||
case AnimationStatus.dismissed:
|
case AnimationStatus.dismissed:
|
||||||
// We might still be an active route if a subclass is controlling the
|
// We might still be an active route if a subclass is controlling the
|
||||||
@ -236,6 +249,8 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
|
|||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
navigator!.finalizeRoute(this);
|
navigator!.finalizeRoute(this);
|
||||||
_popFinalized = true;
|
_popFinalized = true;
|
||||||
|
_performanceModeRequestHandle?.dispose();
|
||||||
|
_performanceModeRequestHandle = null;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -465,6 +480,8 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
assert(!_transitionCompleter.isCompleted, 'Cannot dispose a $runtimeType twice.');
|
assert(!_transitionCompleter.isCompleted, 'Cannot dispose a $runtimeType twice.');
|
||||||
_animation?.removeStatusListener(_handleStatusChanged);
|
_animation?.removeStatusListener(_handleStatusChanged);
|
||||||
|
_performanceModeRequestHandle?.dispose();
|
||||||
|
_performanceModeRequestHandle = null;
|
||||||
if (willDisposeAnimationController) {
|
if (willDisposeAnimationController) {
|
||||||
_controller?.dispose();
|
_controller?.dispose();
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,11 @@
|
|||||||
// Fails with "flutter test --test-randomize-ordering-seed=456"
|
// Fails with "flutter test --test-randomize-ordering-seed=456"
|
||||||
@Tags(<String>['no-shuffle'])
|
@Tags(<String>['no-shuffle'])
|
||||||
|
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
Future<void> startTransitionBetween(
|
Future<void> 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 {
|
testWidgets('Multiple nav bars tags do not conflict if in different navigators', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
CupertinoApp(
|
CupertinoApp(
|
||||||
|
56
packages/flutter/test/scheduler/performance_mode_test.dart
Normal file
56
packages/flutter/test/scheduler/performance_mode_test.dart
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
}
|
@ -885,6 +885,9 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
|
|||||||
assert(debugAssertNoTransientCallbacks(
|
assert(debugAssertNoTransientCallbacks(
|
||||||
'An animation is still running even after the widget tree was disposed.'
|
'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(
|
assert(debugAssertAllFoundationVarsUnset(
|
||||||
'The value of a foundation debug variable was changed by the test.',
|
'The value of a foundation debug variable was changed by the test.',
|
||||||
debugPrintOverride: debugPrintOverride,
|
debugPrintOverride: debugPrintOverride,
|
||||||
|
@ -39,6 +39,7 @@ Future<void> main() async {
|
|||||||
));
|
));
|
||||||
expect(tester.binding, binding);
|
expect(tester.binding, binding);
|
||||||
binding.reportData = <String, dynamic>{'answer': 42};
|
binding.reportData = <String, dynamic>{'answer': 42};
|
||||||
|
await tester.pump();
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('hitTesting works when using setSurfaceSize', (WidgetTester tester) async {
|
testWidgets('hitTesting works when using setSurfaceSize', (WidgetTester tester) async {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user