Commit 1ac17c14 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by Flutter GitHub Bot

Re-land "Add option to delay rendering the first frame (#45135)" (#45941)

parent 1b05cb2b
...@@ -284,6 +284,62 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture ...@@ -284,6 +284,62 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
_mouseTracker.schedulePostFrameCheck(); _mouseTracker.schedulePostFrameCheck();
} }
int _firstFrameDeferredCount = 0;
bool _firstFrameSent = false;
/// Whether frames produced by [drawFrame] are sent to the engine.
///
/// If false the framework will do all the work to produce a frame,
/// but the frame is never sent to the engine to actually appear on screen.
///
/// See also:
///
/// * [deferFirstFrame], which defers when the first frame is sent to the
/// engine.
bool get sendFramesToEngine => _firstFrameSent || _firstFrameDeferredCount == 0;
/// Tell the framework to not send the first frames to the engine until there
/// is a corresponding call to [allowFirstFrame].
///
/// Call this to perform asynchronous initialization work before the first
/// frame is rendered (which takes down the splash screen). The framework
/// will still do all the work to produce frames, but those frames are never
/// sent to the engine and will not appear on screen.
///
/// Calling this has no effect after the first frame has been sent to the
/// engine.
void deferFirstFrame() {
assert(_firstFrameDeferredCount >= 0);
_firstFrameDeferredCount += 1;
}
/// Called after [deferFirstFrame] to tell the framework that it is ok to
/// send the first frame to the engine now.
///
/// For best performance, this method should only be called while the
/// [schedulerPhase] is [SchedulerPhase.idle].
///
/// This method may only be called once for each corresponding call
/// to [deferFirstFrame].
void allowFirstFrame() {
assert(_firstFrameDeferredCount > 0);
_firstFrameDeferredCount -= 1;
// Always schedule a warm up frame even if the deferral count is not down to
// zero yet since the removal of a deferral may uncover new deferrals that
// are lower in the widget tree.
if (!_firstFrameSent)
scheduleWarmUpFrame();
}
/// Call this to pretend that no frames have been sent to the engine yet.
///
/// This is useful for tests that want to call [deferFirstFrame] and
/// [allowFirstFrame] since those methods only have an effect if no frames
/// have been sent to the engine yet.
void resetFirstFrameSent() {
_firstFrameSent = false;
}
/// Pump the rendering pipeline to generate a frame. /// Pump the rendering pipeline to generate a frame.
/// ///
/// This method is called by [handleDrawFrame], which itself is called /// This method is called by [handleDrawFrame], which itself is called
...@@ -345,8 +401,11 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture ...@@ -345,8 +401,11 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
pipelineOwner.flushLayout(); pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits(); pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint(); pipelineOwner.flushPaint();
renderView.compositeFrame(); // this sends the bits to the GPU if (sendFramesToEngine) {
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS. renderView.compositeFrame(); // this sends the bits to the GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
_firstFrameSent = true;
}
} }
@override @override
......
...@@ -574,9 +574,6 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -574,9 +574,6 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
} }
bool _needToReportFirstFrame = true; bool _needToReportFirstFrame = true;
int _deferFirstFrameReportCount = 0;
bool get _reportFirstFrame => _deferFirstFrameReportCount == 0;
final Completer<void> _firstFrameCompleter = Completer<void>(); final Completer<void> _firstFrameCompleter = Completer<void>();
...@@ -602,11 +599,6 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -602,11 +599,6 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
/// Whether the first frame has finished building. /// Whether the first frame has finished building.
/// ///
/// Only useful in profile and debug builds; in release builds, this always
/// return false. This can be deferred using [deferFirstFrameReport] and
/// [allowFirstFrameReport]. The value is set at the end of the call to
/// [drawFrame].
///
/// This value can also be obtained over the VM service protocol as /// This value can also be obtained over the VM service protocol as
/// `ext.flutter.didSendFirstFrameEvent`. /// `ext.flutter.didSendFirstFrameEvent`.
/// ///
...@@ -618,27 +610,30 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -618,27 +610,30 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
/// Tell the framework not to report the frame it is building as a "useful" /// Tell the framework not to report the frame it is building as a "useful"
/// first frame until there is a corresponding call to [allowFirstFrameReport]. /// first frame until there is a corresponding call to [allowFirstFrameReport].
/// ///
/// This is used by [WidgetsApp] to avoid reporting frames that aren't useful /// Deprecated. Use [deferFirstFrame]/[allowFirstFrame] to delay rendering the
/// during startup as the "first frame". /// first frame.
@Deprecated(
'Use deferFirstFrame/allowFirstFrame to delay rendering the first frame. '
'This feature was deprecated after v1.12.4.'
)
void deferFirstFrameReport() { void deferFirstFrameReport() {
if (!kReleaseMode) { if (!kReleaseMode) {
assert(_deferFirstFrameReportCount >= 0); deferFirstFrame();
_deferFirstFrameReportCount += 1;
} }
} }
/// When called after [deferFirstFrameReport]: tell the framework to report /// When called after [deferFirstFrameReport]: tell the framework to report
/// the frame it is building as a "useful" first frame. /// the frame it is building as a "useful" first frame.
/// ///
/// This method may only be called once for each corresponding call /// Deprecated. Use [deferFirstFrame]/[allowFirstFrame] to delay rendering the
/// to [deferFirstFrameReport]. /// first frame.
/// @Deprecated(
/// This is used by [WidgetsApp] to report when the first useful frame is 'Use deferFirstFrame/allowFirstFrame to delay rendering the first frame. '
/// painted. 'This feature was deprecated after v1.12.4.'
)
void allowFirstFrameReport() { void allowFirstFrameReport() {
if (!kReleaseMode) { if (!kReleaseMode) {
assert(_deferFirstFrameReportCount >= 1); allowFirstFrame();
_deferFirstFrameReportCount -= 1;
} }
} }
...@@ -755,18 +750,23 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -755,18 +750,23 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
return true; return true;
}()); }());
if (_needToReportFirstFrame && _reportFirstFrame) { TimingsCallback firstFrameCallback;
if (_needToReportFirstFrame) {
assert(!_firstFrameCompleter.isCompleted); assert(!_firstFrameCompleter.isCompleted);
TimingsCallback firstFrameCallback;
firstFrameCallback = (List<FrameTiming> timings) { firstFrameCallback = (List<FrameTiming> timings) {
assert(sendFramesToEngine);
if (!kReleaseMode) { if (!kReleaseMode) {
developer.Timeline.instantSync('Rasterized first useful frame'); developer.Timeline.instantSync('Rasterized first useful frame');
developer.postEvent('Flutter.FirstFrame', <String, dynamic>{}); developer.postEvent('Flutter.FirstFrame', <String, dynamic>{});
} }
SchedulerBinding.instance.removeTimingsCallback(firstFrameCallback); SchedulerBinding.instance.removeTimingsCallback(firstFrameCallback);
firstFrameCallback = null;
_firstFrameCompleter.complete(); _firstFrameCompleter.complete();
}; };
// Callback is only invoked when [Window.render] is called. When
// [sendFramesToEngine] is set to false during the frame, it will not
// be called and we need to remove the callback (see below).
SchedulerBinding.instance.addTimingsCallback(firstFrameCallback); SchedulerBinding.instance.addTimingsCallback(firstFrameCallback);
} }
...@@ -782,11 +782,14 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -782,11 +782,14 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
}()); }());
} }
if (!kReleaseMode) { if (!kReleaseMode) {
if (_needToReportFirstFrame && _reportFirstFrame) { if (_needToReportFirstFrame && sendFramesToEngine) {
developer.Timeline.instantSync('Widgets built first useful frame'); developer.Timeline.instantSync('Widgets built first useful frame');
} }
} }
_needToReportFirstFrame = false; _needToReportFirstFrame = false;
if (firstFrameCallback != null && !sendFramesToEngine) {
SchedulerBinding.instance.removeTimingsCallback(firstFrameCallback);
}
} }
/// The [Element] that is at the root of the hierarchy (and which wraps the /// The [Element] that is at the root of the hierarchy (and which wraps the
...@@ -834,12 +837,9 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -834,12 +837,9 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
return true; return true;
}()); }());
deferFirstFrameReport();
if (renderViewElement != null) if (renderViewElement != null)
buildOwner.reassemble(renderViewElement); buildOwner.reassemble(renderViewElement);
return super.performReassemble().then((void value) { return super.performReassemble();
allowFirstFrameReport();
});
} }
} }
......
...@@ -6,6 +6,7 @@ import 'dart:async'; ...@@ -6,6 +6,7 @@ import 'dart:async';
import 'dart:ui' show Locale; import 'dart:ui' show Locale;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'basic.dart'; import 'basic.dart';
import 'binding.dart'; import 'binding.dart';
...@@ -519,15 +520,15 @@ class _LocalizationsState extends State<Localizations> { ...@@ -519,15 +520,15 @@ class _LocalizationsState extends State<Localizations> {
// have finished loading. Until then the old locale will continue to be used. // have finished loading. Until then the old locale will continue to be used.
// - If we're running at app startup time then defer reporting the first // - If we're running at app startup time then defer reporting the first
// "useful" frame until after the async load has completed. // "useful" frame until after the async load has completed.
WidgetsBinding.instance.deferFirstFrameReport(); RendererBinding.instance.deferFirstFrame();
typeToResourcesFuture.then<void>((Map<Type, dynamic> value) { typeToResourcesFuture.then<void>((Map<Type, dynamic> value) {
WidgetsBinding.instance.allowFirstFrameReport(); if (mounted) {
if (!mounted) setState(() {
return; _typeToResources = value;
setState(() { _locale = locale;
_typeToResources = value; });
_locale = locale; }
}); RendererBinding.instance.allowFirstFrame();
}); });
} }
} }
......
// 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:async';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
const String _actualContent = 'Actual Content';
const String _loading = 'Loading...';
void main() {
testWidgets('deferFirstFrame/allowFirstFrame stops sending frames to engine', (WidgetTester tester) async {
expect(RendererBinding.instance.sendFramesToEngine, isTrue);
final Completer<void> completer = Completer<void>();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: _DeferringWidget(
key: UniqueKey(),
loader: completer.future,
),
),
);
final _DeferringWidgetState state = tester.state<_DeferringWidgetState>(find.byType(_DeferringWidget));
expect(find.text(_loading), findsOneWidget);
expect(find.text(_actualContent), findsNothing);
expect(RendererBinding.instance.sendFramesToEngine, isFalse);
await tester.pump();
expect(find.text(_loading), findsOneWidget);
expect(find.text(_actualContent), findsNothing);
expect(RendererBinding.instance.sendFramesToEngine, isFalse);
expect(state.doneLoading, isFalse);
// Complete the future to start sending frames.
completer.complete();
await tester.idle();
expect(state.doneLoading, isTrue);
expect(RendererBinding.instance.sendFramesToEngine, isTrue);
await tester.pump();
expect(find.text(_loading), findsNothing);
expect(find.text(_actualContent), findsOneWidget);
expect(RendererBinding.instance.sendFramesToEngine, isTrue);
});
testWidgets('Two widgets can defer frames', (WidgetTester tester) async {
expect(RendererBinding.instance.sendFramesToEngine, isTrue);
final Completer<void> completer1 = Completer<void>();
final Completer<void> completer2 = Completer<void>();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Row(
children: <Widget>[
_DeferringWidget(
key: UniqueKey(),
loader: completer1.future,
),
_DeferringWidget(
key: UniqueKey(),
loader: completer2.future,
),
],
),
),
);
expect(find.text(_loading), findsNWidgets(2));
expect(find.text(_actualContent), findsNothing);
expect(RendererBinding.instance.sendFramesToEngine, isFalse);
completer1.complete();
completer2.complete();
await tester.idle();
await tester.pump();
expect(find.text(_loading), findsNothing);
expect(find.text(_actualContent), findsNWidgets(2));
expect(RendererBinding.instance.sendFramesToEngine, isTrue);
});
}
class _DeferringWidget extends StatefulWidget {
const _DeferringWidget({Key key, this.loader}) : super(key: key);
final Future<void> loader;
@override
State<_DeferringWidget> createState() => _DeferringWidgetState();
}
class _DeferringWidgetState extends State<_DeferringWidget> {
bool doneLoading = false;
@override
void initState() {
super.initState();
RendererBinding.instance.deferFirstFrame();
widget.loader.then((_) {
setState(() {
doneLoading = true;
RendererBinding.instance.allowFirstFrame();
});
});
}
@override
Widget build(BuildContext context) {
return doneLoading
? const Text(_actualContent)
: const Text(_loading);
}
}
// 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:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
void main() {
final TestAutomatedTestWidgetsFlutterBinding binding = TestAutomatedTestWidgetsFlutterBinding();
testWidgets('Locale is available when Localizations widget stops defering frames', (WidgetTester tester) async {
final FakeLocalizationsDelegate delegate = FakeLocalizationsDelegate();
await tester.pumpWidget(Localizations(
locale: const Locale('fo'),
delegates: <LocalizationsDelegate<dynamic>>[
WidgetsLocalizationsDelegate(),
delegate,
],
child: const Text('loaded')
));
final dynamic state = tester.state(find.byType(Localizations));
expect(state.locale, isNull);
expect(find.text('loaded'), findsNothing);
Locale locale;
binding.onAllowFrame = () {
locale = state.locale;
};
delegate.completer.complete('foo');
await tester.idle();
expect(locale, const Locale('fo'));
await tester.pump();
expect(find.text('loaded'), findsOneWidget);
});
}
class FakeLocalizationsDelegate extends LocalizationsDelegate<String> {
final Completer<String> completer = Completer<String>();
@override
bool isSupported(Locale locale) => true;
@override
Future<String> load(Locale locale) => completer.future;
@override
bool shouldReload(LocalizationsDelegate<String> old) => false;
}
class TestAutomatedTestWidgetsFlutterBinding extends AutomatedTestWidgetsFlutterBinding {
VoidCallback onAllowFrame;
@override
void allowFirstFrame() {
if (onAllowFrame != null)
onAllowFrame();
super.allowFirstFrame();
}
}
class WidgetsLocalizationsDelegate extends LocalizationsDelegate<WidgetsLocalizations> {
@override
bool isSupported(Locale locale) => true;
@override
Future<WidgetsLocalizations> load(Locale locale) => DefaultWidgetsLocalizations.load(locale);
@override
bool shouldReload(WidgetsLocalizationsDelegate old) => false;
}
...@@ -687,6 +687,9 @@ abstract class TestWidgetsFlutterBinding extends BindingBase ...@@ -687,6 +687,9 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
runApp(Container(key: UniqueKey(), child: _preTestMessage)); // Reset the tree to a known state. runApp(Container(key: UniqueKey(), child: _preTestMessage)); // Reset the tree to a known state.
await pump(); await pump();
// Pretend that the first frame produced in the test body is the first frame
// sent to the engine.
resetFirstFrameSent();
final bool autoUpdateGoldensBeforeTest = autoUpdateGoldenFiles && !isBrowser; final bool autoUpdateGoldensBeforeTest = autoUpdateGoldenFiles && !isBrowser;
final TestExceptionReporter reportTestExceptionBeforeTest = reportTestException; final TestExceptionReporter reportTestExceptionBeforeTest = reportTestException;
...@@ -963,6 +966,31 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { ...@@ -963,6 +966,31 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
return result; return result;
} }
int _firstFrameDeferredCount = 0;
bool _firstFrameSent = false;
@override
bool get sendFramesToEngine => _firstFrameSent || _firstFrameDeferredCount == 0;
@override
void deferFirstFrame() {
assert(_firstFrameDeferredCount >= 0);
_firstFrameDeferredCount += 1;
}
@override
void allowFirstFrame() {
assert(_firstFrameDeferredCount > 0);
_firstFrameDeferredCount -= 1;
// Unlike in RendererBinding.allowFirstFrame we do not force a frame her
// to give the test full control over frame scheduling.
}
@override
void resetFirstFrameSent() {
_firstFrameSent = false;
}
EnginePhase _phase = EnginePhase.sendSemanticsUpdate; EnginePhase _phase = EnginePhase.sendSemanticsUpdate;
// Cloned from RendererBinding.drawFrame() but with early-exit semantics. // Cloned from RendererBinding.drawFrame() but with early-exit semantics.
...@@ -979,7 +1007,8 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { ...@@ -979,7 +1007,8 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
pipelineOwner.flushCompositingBits(); pipelineOwner.flushCompositingBits();
if (_phase != EnginePhase.compositingBits) { if (_phase != EnginePhase.compositingBits) {
pipelineOwner.flushPaint(); pipelineOwner.flushPaint();
if (_phase != EnginePhase.paint) { if (_phase != EnginePhase.paint && sendFramesToEngine) {
_firstFrameSent = true;
renderView.compositeFrame(); // this sends the bits to the GPU renderView.compositeFrame(); // this sends the bits to the GPU
if (_phase != EnginePhase.composite) { if (_phase != EnginePhase.composite) {
pipelineOwner.flushSemantics(); pipelineOwner.flushSemantics();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment