From d285b8843fce6cf039eb197a09b01ca5cc00d92b Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Thu, 21 Nov 2019 12:13:45 -0800 Subject: [PATCH] Add option to delay rendering the first frame (#45135) --- .../flutter/lib/src/rendering/binding.dart | 54 +++++++++++- packages/flutter/lib/src/widgets/binding.dart | 52 ++++++------ .../lib/src/widgets/localizations.dart | 5 +- .../binding_deferred_first_frame_test.dart | 84 +++++++++++++++++++ 4 files changed, 165 insertions(+), 30 deletions(-) create mode 100644 packages/flutter/test/widgets/binding_deferred_first_frame_test.dart diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart index adf10377f3..9cfb5759ae 100644 --- a/packages/flutter/lib/src/rendering/binding.dart +++ b/packages/flutter/lib/src/rendering/binding.dart @@ -283,6 +283,53 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture drawFrame(); } + 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 send to the engine to actually appear on screen. + /// + /// See also: + /// + /// * [deferFirstFrame], which defers when the first frame is send 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 initialisation 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 + /// send to the engine and will not appear on screen. + /// + /// Calling this has no effect after the first frame has been send 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(); + } + /// Pump the rendering pipeline to generate a frame. /// /// This method is called by [handleDrawFrame], which itself is called @@ -344,8 +391,11 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture pipelineOwner.flushLayout(); pipelineOwner.flushCompositingBits(); pipelineOwner.flushPaint(); - renderView.compositeFrame(); // this sends the bits to the GPU - pipelineOwner.flushSemantics(); // this also sends the semantics to the OS. + if (sendFramesToEngine) { + renderView.compositeFrame(); // this sends the bits to the GPU + pipelineOwner.flushSemantics(); // this also sends the semantics to the OS. + _firstFrameSent = true; + } } @override diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart index c4eb95857e..1fd7d81610 100644 --- a/packages/flutter/lib/src/widgets/binding.dart +++ b/packages/flutter/lib/src/widgets/binding.dart @@ -572,9 +572,6 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB } bool _needToReportFirstFrame = true; - int _deferFirstFrameReportCount = 0; - bool get _reportFirstFrame => _deferFirstFrameReportCount == 0; - final Completer _firstFrameCompleter = Completer(); @@ -600,11 +597,6 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB /// 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 /// `ext.flutter.didSendFirstFrameEvent`. /// @@ -616,27 +608,30 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB /// Tell the framework not to report the frame it is building as a "useful" /// first frame until there is a corresponding call to [allowFirstFrameReport]. /// - /// This is used by [WidgetsApp] to avoid reporting frames that aren't useful - /// during startup as the "first frame". + /// Deprecated. Use [deferFirstFrame]/[allowFirstFrame] to delay rendering the + /// first frame. + @Deprecated( + 'Use deferFirstFrame/allowFirstFrame to delay rendering the first frame. ' + 'This feature was deprecated after v1.12.4.' + ) void deferFirstFrameReport() { if (!kReleaseMode) { - assert(_deferFirstFrameReportCount >= 0); - _deferFirstFrameReportCount += 1; + deferFirstFrame(); } } /// When called after [deferFirstFrameReport]: tell the framework to report /// the frame it is building as a "useful" first frame. /// - /// This method may only be called once for each corresponding call - /// to [deferFirstFrameReport]. - /// - /// This is used by [WidgetsApp] to report when the first useful frame is - /// painted. + /// Deprecated. Use [deferFirstFrame]/[allowFirstFrame] to delay rendering the + /// first frame. + @Deprecated( + 'Use deferFirstFrame/allowFirstFrame to delay rendering the first frame. ' + 'This feature was deprecated after v1.12.4.' + ) void allowFirstFrameReport() { if (!kReleaseMode) { - assert(_deferFirstFrameReportCount >= 1); - _deferFirstFrameReportCount -= 1; + allowFirstFrame(); } } @@ -753,18 +748,23 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB return true; }()); - if (_needToReportFirstFrame && _reportFirstFrame) { + TimingsCallback firstFrameCallback; + if (_needToReportFirstFrame) { assert(!_firstFrameCompleter.isCompleted); - TimingsCallback firstFrameCallback; firstFrameCallback = (List timings) { + assert(sendFramesToEngine); if (!kReleaseMode) { developer.Timeline.instantSync('Rasterized first useful frame'); developer.postEvent('Flutter.FirstFrame', {}); } SchedulerBinding.instance.removeTimingsCallback(firstFrameCallback); + firstFrameCallback = null; _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); } @@ -780,11 +780,14 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB }()); } if (!kReleaseMode) { - if (_needToReportFirstFrame && _reportFirstFrame) { + if (_needToReportFirstFrame && sendFramesToEngine) { developer.Timeline.instantSync('Widgets built first useful frame'); } } _needToReportFirstFrame = false; + if (firstFrameCallback != null && !sendFramesToEngine) { + SchedulerBinding.instance.removeTimingsCallback(firstFrameCallback); + } } /// The [Element] that is at the root of the hierarchy (and which wraps the @@ -832,12 +835,9 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB return true; }()); - deferFirstFrameReport(); if (renderViewElement != null) buildOwner.reassemble(renderViewElement); - return super.performReassemble().then((void value) { - allowFirstFrameReport(); - }); + return super.performReassemble(); } } diff --git a/packages/flutter/lib/src/widgets/localizations.dart b/packages/flutter/lib/src/widgets/localizations.dart index 0cc93a84e5..29196b3cee 100644 --- a/packages/flutter/lib/src/widgets/localizations.dart +++ b/packages/flutter/lib/src/widgets/localizations.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:ui' show Locale; import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; import 'basic.dart'; import 'binding.dart'; @@ -519,9 +520,9 @@ class _LocalizationsState extends State { // 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 // "useful" frame until after the async load has completed. - WidgetsBinding.instance.deferFirstFrameReport(); + RendererBinding.instance.deferFirstFrame(); typeToResourcesFuture.then((Map value) { - WidgetsBinding.instance.allowFirstFrameReport(); + RendererBinding.instance.allowFirstFrame(); if (!mounted) return; setState(() { diff --git a/packages/flutter/test/widgets/binding_deferred_first_frame_test.dart b/packages/flutter/test/widgets/binding_deferred_first_frame_test.dart new file mode 100644 index 0000000000..0f6d01926f --- /dev/null +++ b/packages/flutter/test/widgets/binding_deferred_first_frame_test.dart @@ -0,0 +1,84 @@ +// Copyright 2019 The Chromium 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 completer = Completer(); + 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); + }); +} + +class _DeferringWidget extends StatefulWidget { + const _DeferringWidget({Key key, this.loader}) : super(key: key); + + final Future 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); + } +}