Add scheduleWarmUpFrame (flutter/engine#50570)

This PR adds `PlatformDispatcher.scheduleWarmUpFrame`.

This PR is needed for the follow up changes:
* The framework will switch to using this function to render warmup
frames in https://github.com/flutter/flutter/pull/143290.
* Then the engine will finally be able to switch to multiview pipeline
with no regression on startup timing in
https://github.com/flutter/engine/pull/49950.

For why the warm up frame must involve the engine to render, see
https://github.com/flutter/flutter/issues/142851.


## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide] and the [C++,
Objective-C, Java style guides].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I added new tests to check the change I am making or feature I am
adding, or the PR is [test-exempt]. See [testing the engine] for
instructions on writing and running engine tests.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I signed the [CLA].
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#overview
[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene
[test-exempt]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo
[C++, Objective-C, Java style guides]:
https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
[testing the engine]:
https://github.com/flutter/flutter/wiki/Testing-the-engine
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes
[Discord]: https://github.com/flutter/flutter/wiki/Chat
This commit is contained in:
Tong Mu 2024-02-21 16:46:11 -08:00 committed by GitHub
parent b357d7a45e
commit c5e0858a01
18 changed files with 405 additions and 7 deletions

View File

@ -98,6 +98,7 @@ typedef CanvasPath Path;
V(NativeStringAttribute::initSpellOutStringAttribute) \
V(PlatformConfigurationNativeApi::DefaultRouteName) \
V(PlatformConfigurationNativeApi::ScheduleFrame) \
V(PlatformConfigurationNativeApi::EndWarmUpFrame) \
V(PlatformConfigurationNativeApi::Render) \
V(PlatformConfigurationNativeApi::UpdateSemantics) \
V(PlatformConfigurationNativeApi::SetNeedsReportTimings) \

View File

@ -801,11 +801,47 @@ class PlatformDispatcher {
///
/// * [SchedulerBinding], the Flutter framework class which manages the
/// scheduling of frames.
/// * [scheduleWarmUpFrame], which should only be used to schedule warm up
/// frames.
void scheduleFrame() => _scheduleFrame();
@Native<Void Function()>(symbol: 'PlatformConfigurationNativeApi::ScheduleFrame')
external static void _scheduleFrame();
/// Schedule a frame to run as soon as possible, rather than waiting for the
/// engine to request a frame in response to a system "Vsync" signal.
///
/// The application can call this method as soon as it starts up so that the
/// first frame (which is likely to be quite expensive) can start a few extra
/// milliseconds earlier. Using it in other situations might lead to
/// unintended results, such as screen tearing. Depending on platforms and
/// situations, the warm up frame might or might not be actually rendered onto
/// the screen.
///
/// For more introduction to the warm up frame, see
/// [SchedulerBinding.scheduleWarmUpFrame].
///
/// This method uses the provided callbacks as the begin frame callback and
/// the draw frame callback instead of [onBeginFrame] and [onDrawFrame].
///
/// See also:
///
/// * [SchedulerBinding.scheduleWarmUpFrame], which uses this method, and
/// introduces the warm up frame in more details.
/// * [scheduleFrame], which schedules the frame at the next appropriate
/// opportunity and should be used to render regular frames.
void scheduleWarmUpFrame({required VoidCallback beginFrame, required VoidCallback drawFrame}) {
// We use timers here to ensure that microtasks flush in between.
Timer.run(beginFrame);
Timer.run(() {
drawFrame();
_endWarmUpFrame();
});
}
@Native<Void Function()>(symbol: 'PlatformConfigurationNativeApi::EndWarmUpFrame')
external static void _endWarmUpFrame();
/// Additional accessibility features that may be enabled by the platform.
AccessibilityFeatures get accessibilityFeatures => _configuration.accessibilityFeatures;

View File

@ -589,6 +589,11 @@ void PlatformConfigurationNativeApi::ScheduleFrame() {
UIDartState::Current()->platform_configuration()->client()->ScheduleFrame();
}
void PlatformConfigurationNativeApi::EndWarmUpFrame() {
UIDartState::ThrowIfUIOperationsProhibited();
UIDartState::Current()->platform_configuration()->client()->EndWarmUpFrame();
}
void PlatformConfigurationNativeApi::UpdateSemantics(SemanticsUpdate* update) {
UIDartState::ThrowIfUIOperationsProhibited();
UIDartState::Current()->platform_configuration()->client()->UpdateSemantics(

View File

@ -65,6 +65,13 @@ class PlatformConfigurationClient {
///
virtual void ScheduleFrame() = 0;
//--------------------------------------------------------------------------
/// @brief Called when a warm up frame has ended.
///
/// For more introduction, see `Animator::EndWarmUpFrame`.
///
virtual void EndWarmUpFrame() = 0;
//--------------------------------------------------------------------------
/// @brief Updates the client's rendering on the GPU with the newly
/// provided Scene.
@ -557,6 +564,8 @@ class PlatformConfigurationNativeApi {
static void ScheduleFrame();
static void EndWarmUpFrame();
static void Render(int64_t view_id,
Scene* scene,
double width,

View File

@ -90,6 +90,8 @@ abstract class PlatformDispatcher {
void scheduleFrame();
void scheduleWarmUpFrame({required VoidCallback beginFrame, required VoidCallback drawFrame});
AccessibilityFeatures get accessibilityFeatures;
VoidCallback? get onAccessibilityFeaturesChanged;

View File

@ -187,7 +187,8 @@ Future<void> initializeEngineServices({
// TODO(yjbanov): technically Flutter flushes microtasks between
// onBeginFrame and onDrawFrame. We don't, which hasn't
// been an issue yet, but eventually we'll have to
// implement it properly.
// implement it properly. (Also see the to-do in
// `EnginePlatformDispatcher.scheduleWarmUpFrame`).
EnginePlatformDispatcher.instance.invokeOnDrawFrame();
}
});

View File

@ -782,6 +782,19 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
scheduleFrameCallback!();
}
@override
void scheduleWarmUpFrame({required ui.VoidCallback beginFrame, required ui.VoidCallback drawFrame}) {
Timer.run(beginFrame);
// We use timers here to ensure that microtasks flush in between.
//
// TODO(dkwingsmt): This logic was moved from the framework and is different
// from how Web renders a regular frame, which doesn't flush microtasks
// between the callbacks at all (see `initializeEngineServices`). We might
// want to change this. See the to-do in `initializeEngineServices` and
// https://github.com/flutter/engine/pull/50570#discussion_r1496671676
Timer.run(drawFrame);
}
/// Updates the application's rendering on the GPU with the newly provided
/// [Scene]. This function must be called within the scope of the
/// [onBeginFrame] or [onDrawFrame] callbacks being invoked. If this function

View File

@ -417,6 +417,23 @@ void testMain() {
dispatcher.dispose();
expect(dispatcher.accessibilityPlaceholder.isConnected, isFalse);
});
test('scheduleWarmupFrame should call both callbacks', () async {
bool beginFrameCalled = false;
final Completer<void> drawFrameCalled = Completer<void>();
dispatcher.scheduleWarmUpFrame(beginFrame: () {
expect(drawFrameCalled.isCompleted, false);
expect(beginFrameCalled, false);
beginFrameCalled = true;
}, drawFrame: () {
expect(beginFrameCalled, true);
expect(drawFrameCalled.isCompleted, false);
drawFrameCalled.complete();
});
await drawFrameCalled.future;
expect(beginFrameCalled, true);
expect(drawFrameCalled.isCompleted, true);
});
});
}

View File

@ -340,6 +340,11 @@ void RuntimeController::ScheduleFrame() {
client_.ScheduleFrame();
}
// |PlatformConfigurationClient|
void RuntimeController::EndWarmUpFrame() {
client_.EndWarmUpFrame();
}
// |PlatformConfigurationClient|
void RuntimeController::Render(Scene* scene, double width, double height) {
// TODO(dkwingsmt): Currently only supports a single window.

View File

@ -657,6 +657,9 @@ class RuntimeController : public PlatformConfigurationClient {
// |PlatformConfigurationClient|
void ScheduleFrame() override;
// |PlatformConfigurationClient|
void EndWarmUpFrame() override;
// |PlatformConfigurationClient|
void Render(Scene* scene, double width, double height) override;

View File

@ -25,6 +25,8 @@ class RuntimeDelegate {
virtual void ScheduleFrame(bool regenerate_layer_trees = true) = 0;
virtual void EndWarmUpFrame() = 0;
virtual void Render(std::unique_ptr<flutter::LayerTree> layer_tree,
float device_pixel_ratio) = 0;

View File

@ -264,6 +264,12 @@ void Animator::AwaitVSync() {
}
}
void Animator::EndWarmUpFrame() {
// Do nothing. The warm up frame does not need any additional work to end the
// frame for now. This will change once the pipeline supports multi-view.
// https://github.com/flutter/flutter/issues/142851
}
void Animator::ScheduleSecondaryVsyncCallback(uintptr_t id,
const fml::closure& callback) {
waiter_->ScheduleSecondaryCallback(id, callback);

View File

@ -53,6 +53,22 @@ class Animator final {
void RequestFrame(bool regenerate_layer_trees = true);
//--------------------------------------------------------------------------
/// @brief Tells the Animator that a warm up frame has ended.
///
/// In a warm up frame, `Animator::Render` is called out of vsync
/// tasks, and Animator requires an explicit end-of-frame call to
/// know when to send the layer trees to the pipeline.
///
/// This is different from regular frames, where Animator::Render is
/// always called within a vsync task, and Animator can send
/// the views at the end of the vsync task.
///
/// For more about warm up frames, see
/// `PlatformDispatcher.scheduleWarmUpFrame`.
///
void EndWarmUpFrame();
//--------------------------------------------------------------------------
/// @brief Tells the Animator that this frame needs to render another view.
///

View File

@ -462,6 +462,10 @@ void Engine::ScheduleFrame(bool regenerate_layer_trees) {
animator_->RequestFrame(regenerate_layer_trees);
}
void Engine::EndWarmUpFrame() {
animator_->EndWarmUpFrame();
}
void Engine::Render(std::unique_ptr<flutter::LayerTree> layer_tree,
float device_pixel_ratio) {
if (!layer_tree) {

View File

@ -837,6 +837,9 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate {
/// tree.
void ScheduleFrame() { ScheduleFrame(true); }
// |RuntimeDelegate|
void EndWarmUpFrame() override;
// |RuntimeDelegate|
FontCollection& GetFontCollection() override;

View File

@ -6,7 +6,10 @@
#include <cstring>
#include "flutter/common/constants.h"
#include "flutter/runtime/dart_vm_lifecycle.h"
#include "flutter/shell/common/shell.h"
#include "flutter/shell/common/shell_test.h"
#include "flutter/shell/common/thread_host.h"
#include "flutter/testing/fixture_test.h"
#include "flutter/testing/testing.h"
@ -19,6 +22,19 @@ namespace flutter {
namespace {
using ::testing::Invoke;
using ::testing::ReturnRef;
static void PostSync(const fml::RefPtr<fml::TaskRunner>& task_runner,
const fml::closure& task) {
fml::AutoResetWaitableEvent latch;
fml::TaskRunner::RunNowOrPostTask(task_runner, [&latch, &task] {
task();
latch.Signal();
});
latch.Wait();
}
class MockDelegate : public Engine::Delegate {
public:
MOCK_METHOD(void,
@ -63,6 +79,7 @@ class MockRuntimeDelegate : public RuntimeDelegate {
public:
MOCK_METHOD(std::string, DefaultRouteName, (), (override));
MOCK_METHOD(void, ScheduleFrame, (bool), (override));
MOCK_METHOD(void, EndWarmUpFrame, (), (override));
MOCK_METHOD(void,
Render,
(std::unique_ptr<flutter::LayerTree>, float),
@ -117,6 +134,51 @@ class MockRuntimeController : public RuntimeController {
MOCK_METHOD(bool, NotifyIdle, (fml::TimeDelta), (override));
};
class MockAnimatorDelegate : public Animator::Delegate {
public:
/* Animator::Delegate */
MOCK_METHOD(void,
OnAnimatorBeginFrame,
(fml::TimePoint frame_target_time, uint64_t frame_number),
(override));
MOCK_METHOD(void,
OnAnimatorNotifyIdle,
(fml::TimeDelta deadline),
(override));
MOCK_METHOD(void,
OnAnimatorUpdateLatestFrameTargetTime,
(fml::TimePoint frame_target_time),
(override));
MOCK_METHOD(void,
OnAnimatorDraw,
(std::shared_ptr<FramePipeline> pipeline),
(override));
MOCK_METHOD(void,
OnAnimatorDrawLastLayerTrees,
(std::unique_ptr<FrameTimingsRecorder> frame_timings_recorder),
(override));
};
class MockPlatformMessageHandler : public PlatformMessageHandler {
public:
MOCK_METHOD(void,
HandlePlatformMessage,
(std::unique_ptr<PlatformMessage> message),
(override));
MOCK_METHOD(bool,
DoesHandlePlatformMessageOnPlatformThread,
(),
(const, override));
MOCK_METHOD(void,
InvokePlatformMessageResponseCallback,
(int response_id, std::unique_ptr<fml::Mapping> mapping),
(override));
MOCK_METHOD(void,
InvokePlatformMessageEmptyResponseCallback,
(int response_id),
(override));
};
std::unique_ptr<PlatformMessage> MakePlatformMessage(
const std::string& channel,
const std::map<std::string, std::string>& values,
@ -185,6 +247,96 @@ class EngineTest : public testing::FixtureTest {
std::shared_ptr<fml::ConcurrentTaskRunner> image_decoder_task_runner_;
fml::TaskRunnerAffineWeakPtr<SnapshotDelegate> snapshot_delegate_;
};
// A class that can launch an Engine with the specified Engine::Delegate.
//
// To use this class, contruct this class with Create, call Run, and use the
// engine with EngineTaskSync().
class EngineContext {
public:
using EngineCallback = std::function<void(Engine&)>;
[[nodiscard]] static std::unique_ptr<EngineContext> Create(
Engine::Delegate& delegate, //
Settings settings, //
const TaskRunners& task_runners, //
std::unique_ptr<Animator> animator) {
auto [vm, isolate_snapshot] = Shell::InferVmInitDataFromSettings(settings);
FML_CHECK(vm) << "Must be able to initialize the VM.";
// Construct the class with `new` because `make_unique` has no access to the
// private constructor.
EngineContext* raw_pointer =
new EngineContext(delegate, settings, task_runners, std::move(animator),
std::move(vm), isolate_snapshot);
return std::unique_ptr<EngineContext>(raw_pointer);
}
void Run(RunConfiguration configuration) {
PostSync(task_runners_.GetUITaskRunner(), [this, &configuration] {
Engine::RunStatus run_status = engine_->Run(std::move(configuration));
FML_CHECK(run_status == Engine::RunStatus::Success)
<< "Engine failed to run.";
(void)run_status; // Suppress unused-variable warning
});
}
// Run a task that operates the Engine on the UI thread, and wait for the
// task to end.
//
// If called on the UI thread, the task is executed synchronously.
void EngineTaskSync(EngineCallback task) {
ASSERT_TRUE(engine_);
ASSERT_TRUE(task);
auto runner = task_runners_.GetUITaskRunner();
if (runner->RunsTasksOnCurrentThread()) {
task(*engine_);
} else {
PostSync(task_runners_.GetUITaskRunner(), [&]() { task(*engine_); });
}
}
~EngineContext() {
PostSync(task_runners_.GetUITaskRunner(), [this] { engine_.reset(); });
}
private:
EngineContext(Engine::Delegate& delegate, //
Settings settings, //
const TaskRunners& task_runners, //
std::unique_ptr<Animator> animator, //
DartVMRef vm, //
fml::RefPtr<const DartSnapshot> isolate_snapshot)
: task_runners_(task_runners), vm_(std::move(vm)) {
PostSync(task_runners.GetUITaskRunner(), [this, &settings, &animator,
&delegate, &isolate_snapshot] {
auto dispatcher_maker =
[](DefaultPointerDataDispatcher::Delegate& delegate) {
return std::make_unique<DefaultPointerDataDispatcher>(delegate);
};
engine_ = std::make_unique<Engine>(
/*delegate=*/delegate,
/*dispatcher_maker=*/dispatcher_maker,
/*vm=*/*&vm_,
/*isolate_snapshot=*/std::move(isolate_snapshot),
/*task_runners=*/task_runners_,
/*platform_data=*/PlatformData(),
/*settings=*/settings,
/*animator=*/std::move(animator),
/*io_manager=*/io_manager_,
/*unref_queue=*/nullptr,
/*snapshot_delegate=*/snapshot_delegate_,
/*volatile_path_tracker=*/nullptr,
/*gpu_disabled_switch=*/std::make_shared<fml::SyncSwitch>());
});
}
TaskRunners task_runners_;
DartVMRef vm_;
std::unique_ptr<Engine> engine_;
fml::WeakPtr<IOManager> io_manager_;
fml::TaskRunnerAffineWeakPtr<SnapshotDelegate> snapshot_delegate_;
};
} // namespace
TEST_F(EngineTest, Create) {
@ -418,4 +570,69 @@ TEST_F(EngineTest, PassesLoadDartDeferredLibraryErrorToRuntime) {
});
}
// The animator should submit to the pipeline the implicit view rendered in a
// warm up frame if there's already a continuation (i.e. Animator::BeginFrame
// has been called)
TEST_F(EngineTest, AnimatorSubmitWarmUpImplicitView) {
MockAnimatorDelegate animator_delegate;
std::unique_ptr<EngineContext> engine_context;
std::shared_ptr<PlatformMessageHandler> platform_message_handler =
std::make_shared<MockPlatformMessageHandler>();
EXPECT_CALL(delegate_, GetPlatformMessageHandler)
.WillOnce(ReturnRef(platform_message_handler));
fml::AutoResetWaitableEvent continuation_ready_latch;
fml::AutoResetWaitableEvent draw_latch;
EXPECT_CALL(animator_delegate, OnAnimatorDraw)
.WillOnce(Invoke([&draw_latch](
const std::shared_ptr<FramePipeline>& pipeline) {
auto status = pipeline->Consume([&](std::unique_ptr<FrameItem> item) {
EXPECT_EQ(item->layer_tree_tasks.size(), 1u);
EXPECT_EQ(item->layer_tree_tasks[0]->view_id, kFlutterImplicitViewId);
});
EXPECT_EQ(status, PipelineConsumeResult::Done);
draw_latch.Signal();
}));
EXPECT_CALL(animator_delegate, OnAnimatorBeginFrame)
.WillRepeatedly(
Invoke([&engine_context, &continuation_ready_latch](
fml::TimePoint frame_target_time, uint64_t frame_number) {
continuation_ready_latch.Signal();
engine_context->EngineTaskSync([&](Engine& engine) {
engine.BeginFrame(frame_target_time, frame_number);
});
}));
std::unique_ptr<Animator> animator;
PostSync(task_runners_.GetUITaskRunner(),
[&animator, &animator_delegate, &task_runners = task_runners_] {
animator = std::make_unique<Animator>(
animator_delegate, task_runners,
static_cast<std::unique_ptr<VsyncWaiter>>(
std::make_unique<testing::ConstantFiringVsyncWaiter>(
task_runners)));
});
engine_context = EngineContext::Create(delegate_, settings_, task_runners_,
std::move(animator));
engine_context->EngineTaskSync([](Engine& engine) {
// Schedule a frame to trigger Animator::BeginFrame to create a
// continuation. The continuation needs to be available before `Engine::Run`
// since the Dart program immediately schedules a warm up frame.
engine.ScheduleFrame(true);
// Add the implicit view so that the engine recognizes it and that its
// metrics is not empty.
engine.AddView(kFlutterImplicitViewId, ViewportMetrics{1.0, 10, 10, 1, 0});
});
continuation_ready_latch.Wait();
auto configuration = RunConfiguration::InferFromSettings(settings_);
configuration.SetEntrypoint("renderWarmUpImplicitView");
engine_context->Run(std::move(configuration));
draw_latch.Wait();
}
} // namespace flutter

View File

@ -2,11 +2,18 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert' show utf8, json;
import 'dart:async' show scheduleMicrotask;
import 'dart:convert' show json, utf8;
import 'dart:isolate';
import 'dart:typed_data';
import 'dart:ui';
void expect(Object? a, Object? b) {
if (a != b) {
throw AssertionError('Expected $a to == $b');
}
}
void main() {}
@pragma('vm:entry-point')
@ -349,11 +356,6 @@ Future<void> toImageSync() async {
onBeforeToImageSync();
final Image image = picture.toImageSync(20, 25);
void expect(Object? a, Object? b) {
if (a != b) {
throw 'Expected $a to == $b';
}
}
expect(image.width, 20);
expect(image.height, 25);
@ -529,3 +531,31 @@ void testReportViewWidths() {
nativeReportViewWidthsCallback(getCurrentViewWidths());
};
}
@pragma('vm:entry-point')
void renderWarmUpImplicitView() {
bool beginFrameCalled = false;
PlatformDispatcher.instance.scheduleWarmUpFrame(
beginFrame: () {
expect(beginFrameCalled, false);
beginFrameCalled = true;
},
drawFrame: () {
expect(beginFrameCalled, true);
final SceneBuilder builder = SceneBuilder();
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
canvas.drawPaint(Paint()..color = const Color(0xFFABCDEF));
final Picture picture = recorder.endRecording();
builder.addPicture(Offset.zero, picture);
final Scene scene = builder.build();
PlatformDispatcher.instance.implicitView!.render(scene);
scene.dispose();
picture.dispose();
},
);
}

View File

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:ui';
import 'package:litetest/litetest.dart';
@ -46,4 +47,31 @@ void main() {
expect(constraints.isSatisfiedBy(const Size(400, 500)), false);
expect(constraints / 2, const ViewConstraints(minWidth: 50, maxWidth: 100, minHeight: 150, maxHeight: 200));
});
test('scheduleWarmupFrame should call both callbacks and flush microtasks', () async {
bool microtaskFlushed = false;
bool beginFrameCalled = false;
final Completer<void> drawFrameCalled = Completer<void>();
PlatformDispatcher.instance.scheduleWarmUpFrame(beginFrame: () {
expect(microtaskFlushed, false);
expect(drawFrameCalled.isCompleted, false);
expect(beginFrameCalled, false);
beginFrameCalled = true;
scheduleMicrotask(() {
expect(microtaskFlushed, false);
expect(drawFrameCalled.isCompleted, false);
microtaskFlushed = true;
});
expect(microtaskFlushed, false);
}, drawFrame: () {
expect(beginFrameCalled, true);
expect(microtaskFlushed, true);
expect(drawFrameCalled.isCompleted, false);
drawFrameCalled.complete();
});
await drawFrameCalled.future;
expect(beginFrameCalled, true);
expect(drawFrameCalled.isCompleted, true);
expect(microtaskFlushed, true);
});
}