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:
parent
b357d7a45e
commit
c5e0858a01
@ -98,6 +98,7 @@ typedef CanvasPath Path;
|
|||||||
V(NativeStringAttribute::initSpellOutStringAttribute) \
|
V(NativeStringAttribute::initSpellOutStringAttribute) \
|
||||||
V(PlatformConfigurationNativeApi::DefaultRouteName) \
|
V(PlatformConfigurationNativeApi::DefaultRouteName) \
|
||||||
V(PlatformConfigurationNativeApi::ScheduleFrame) \
|
V(PlatformConfigurationNativeApi::ScheduleFrame) \
|
||||||
|
V(PlatformConfigurationNativeApi::EndWarmUpFrame) \
|
||||||
V(PlatformConfigurationNativeApi::Render) \
|
V(PlatformConfigurationNativeApi::Render) \
|
||||||
V(PlatformConfigurationNativeApi::UpdateSemantics) \
|
V(PlatformConfigurationNativeApi::UpdateSemantics) \
|
||||||
V(PlatformConfigurationNativeApi::SetNeedsReportTimings) \
|
V(PlatformConfigurationNativeApi::SetNeedsReportTimings) \
|
||||||
|
@ -801,11 +801,47 @@ class PlatformDispatcher {
|
|||||||
///
|
///
|
||||||
/// * [SchedulerBinding], the Flutter framework class which manages the
|
/// * [SchedulerBinding], the Flutter framework class which manages the
|
||||||
/// scheduling of frames.
|
/// scheduling of frames.
|
||||||
|
/// * [scheduleWarmUpFrame], which should only be used to schedule warm up
|
||||||
|
/// frames.
|
||||||
void scheduleFrame() => _scheduleFrame();
|
void scheduleFrame() => _scheduleFrame();
|
||||||
|
|
||||||
@Native<Void Function()>(symbol: 'PlatformConfigurationNativeApi::ScheduleFrame')
|
@Native<Void Function()>(symbol: 'PlatformConfigurationNativeApi::ScheduleFrame')
|
||||||
external static void _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.
|
/// Additional accessibility features that may be enabled by the platform.
|
||||||
AccessibilityFeatures get accessibilityFeatures => _configuration.accessibilityFeatures;
|
AccessibilityFeatures get accessibilityFeatures => _configuration.accessibilityFeatures;
|
||||||
|
|
||||||
|
@ -589,6 +589,11 @@ void PlatformConfigurationNativeApi::ScheduleFrame() {
|
|||||||
UIDartState::Current()->platform_configuration()->client()->ScheduleFrame();
|
UIDartState::Current()->platform_configuration()->client()->ScheduleFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlatformConfigurationNativeApi::EndWarmUpFrame() {
|
||||||
|
UIDartState::ThrowIfUIOperationsProhibited();
|
||||||
|
UIDartState::Current()->platform_configuration()->client()->EndWarmUpFrame();
|
||||||
|
}
|
||||||
|
|
||||||
void PlatformConfigurationNativeApi::UpdateSemantics(SemanticsUpdate* update) {
|
void PlatformConfigurationNativeApi::UpdateSemantics(SemanticsUpdate* update) {
|
||||||
UIDartState::ThrowIfUIOperationsProhibited();
|
UIDartState::ThrowIfUIOperationsProhibited();
|
||||||
UIDartState::Current()->platform_configuration()->client()->UpdateSemantics(
|
UIDartState::Current()->platform_configuration()->client()->UpdateSemantics(
|
||||||
|
@ -65,6 +65,13 @@ class PlatformConfigurationClient {
|
|||||||
///
|
///
|
||||||
virtual void ScheduleFrame() = 0;
|
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
|
/// @brief Updates the client's rendering on the GPU with the newly
|
||||||
/// provided Scene.
|
/// provided Scene.
|
||||||
@ -557,6 +564,8 @@ class PlatformConfigurationNativeApi {
|
|||||||
|
|
||||||
static void ScheduleFrame();
|
static void ScheduleFrame();
|
||||||
|
|
||||||
|
static void EndWarmUpFrame();
|
||||||
|
|
||||||
static void Render(int64_t view_id,
|
static void Render(int64_t view_id,
|
||||||
Scene* scene,
|
Scene* scene,
|
||||||
double width,
|
double width,
|
||||||
|
@ -90,6 +90,8 @@ abstract class PlatformDispatcher {
|
|||||||
|
|
||||||
void scheduleFrame();
|
void scheduleFrame();
|
||||||
|
|
||||||
|
void scheduleWarmUpFrame({required VoidCallback beginFrame, required VoidCallback drawFrame});
|
||||||
|
|
||||||
AccessibilityFeatures get accessibilityFeatures;
|
AccessibilityFeatures get accessibilityFeatures;
|
||||||
|
|
||||||
VoidCallback? get onAccessibilityFeaturesChanged;
|
VoidCallback? get onAccessibilityFeaturesChanged;
|
||||||
|
@ -187,7 +187,8 @@ Future<void> initializeEngineServices({
|
|||||||
// TODO(yjbanov): technically Flutter flushes microtasks between
|
// TODO(yjbanov): technically Flutter flushes microtasks between
|
||||||
// onBeginFrame and onDrawFrame. We don't, which hasn't
|
// onBeginFrame and onDrawFrame. We don't, which hasn't
|
||||||
// been an issue yet, but eventually we'll have to
|
// 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();
|
EnginePlatformDispatcher.instance.invokeOnDrawFrame();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -782,6 +782,19 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
|
|||||||
scheduleFrameCallback!();
|
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
|
/// Updates the application's rendering on the GPU with the newly provided
|
||||||
/// [Scene]. This function must be called within the scope of the
|
/// [Scene]. This function must be called within the scope of the
|
||||||
/// [onBeginFrame] or [onDrawFrame] callbacks being invoked. If this function
|
/// [onBeginFrame] or [onDrawFrame] callbacks being invoked. If this function
|
||||||
|
@ -417,6 +417,23 @@ void testMain() {
|
|||||||
dispatcher.dispose();
|
dispatcher.dispose();
|
||||||
expect(dispatcher.accessibilityPlaceholder.isConnected, isFalse);
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -340,6 +340,11 @@ void RuntimeController::ScheduleFrame() {
|
|||||||
client_.ScheduleFrame();
|
client_.ScheduleFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// |PlatformConfigurationClient|
|
||||||
|
void RuntimeController::EndWarmUpFrame() {
|
||||||
|
client_.EndWarmUpFrame();
|
||||||
|
}
|
||||||
|
|
||||||
// |PlatformConfigurationClient|
|
// |PlatformConfigurationClient|
|
||||||
void RuntimeController::Render(Scene* scene, double width, double height) {
|
void RuntimeController::Render(Scene* scene, double width, double height) {
|
||||||
// TODO(dkwingsmt): Currently only supports a single window.
|
// TODO(dkwingsmt): Currently only supports a single window.
|
||||||
|
@ -657,6 +657,9 @@ class RuntimeController : public PlatformConfigurationClient {
|
|||||||
// |PlatformConfigurationClient|
|
// |PlatformConfigurationClient|
|
||||||
void ScheduleFrame() override;
|
void ScheduleFrame() override;
|
||||||
|
|
||||||
|
// |PlatformConfigurationClient|
|
||||||
|
void EndWarmUpFrame() override;
|
||||||
|
|
||||||
// |PlatformConfigurationClient|
|
// |PlatformConfigurationClient|
|
||||||
void Render(Scene* scene, double width, double height) override;
|
void Render(Scene* scene, double width, double height) override;
|
||||||
|
|
||||||
|
@ -25,6 +25,8 @@ class RuntimeDelegate {
|
|||||||
|
|
||||||
virtual void ScheduleFrame(bool regenerate_layer_trees = true) = 0;
|
virtual void ScheduleFrame(bool regenerate_layer_trees = true) = 0;
|
||||||
|
|
||||||
|
virtual void EndWarmUpFrame() = 0;
|
||||||
|
|
||||||
virtual void Render(std::unique_ptr<flutter::LayerTree> layer_tree,
|
virtual void Render(std::unique_ptr<flutter::LayerTree> layer_tree,
|
||||||
float device_pixel_ratio) = 0;
|
float device_pixel_ratio) = 0;
|
||||||
|
|
||||||
|
@ -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,
|
void Animator::ScheduleSecondaryVsyncCallback(uintptr_t id,
|
||||||
const fml::closure& callback) {
|
const fml::closure& callback) {
|
||||||
waiter_->ScheduleSecondaryCallback(id, callback);
|
waiter_->ScheduleSecondaryCallback(id, callback);
|
||||||
|
@ -53,6 +53,22 @@ class Animator final {
|
|||||||
|
|
||||||
void RequestFrame(bool regenerate_layer_trees = true);
|
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.
|
/// @brief Tells the Animator that this frame needs to render another view.
|
||||||
///
|
///
|
||||||
|
@ -462,6 +462,10 @@ void Engine::ScheduleFrame(bool regenerate_layer_trees) {
|
|||||||
animator_->RequestFrame(regenerate_layer_trees);
|
animator_->RequestFrame(regenerate_layer_trees);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Engine::EndWarmUpFrame() {
|
||||||
|
animator_->EndWarmUpFrame();
|
||||||
|
}
|
||||||
|
|
||||||
void Engine::Render(std::unique_ptr<flutter::LayerTree> layer_tree,
|
void Engine::Render(std::unique_ptr<flutter::LayerTree> layer_tree,
|
||||||
float device_pixel_ratio) {
|
float device_pixel_ratio) {
|
||||||
if (!layer_tree) {
|
if (!layer_tree) {
|
||||||
|
@ -837,6 +837,9 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate {
|
|||||||
/// tree.
|
/// tree.
|
||||||
void ScheduleFrame() { ScheduleFrame(true); }
|
void ScheduleFrame() { ScheduleFrame(true); }
|
||||||
|
|
||||||
|
// |RuntimeDelegate|
|
||||||
|
void EndWarmUpFrame() override;
|
||||||
|
|
||||||
// |RuntimeDelegate|
|
// |RuntimeDelegate|
|
||||||
FontCollection& GetFontCollection() override;
|
FontCollection& GetFontCollection() override;
|
||||||
|
|
||||||
|
@ -6,7 +6,10 @@
|
|||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "flutter/common/constants.h"
|
||||||
#include "flutter/runtime/dart_vm_lifecycle.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/shell/common/thread_host.h"
|
||||||
#include "flutter/testing/fixture_test.h"
|
#include "flutter/testing/fixture_test.h"
|
||||||
#include "flutter/testing/testing.h"
|
#include "flutter/testing/testing.h"
|
||||||
@ -19,6 +22,19 @@ namespace flutter {
|
|||||||
|
|
||||||
namespace {
|
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 {
|
class MockDelegate : public Engine::Delegate {
|
||||||
public:
|
public:
|
||||||
MOCK_METHOD(void,
|
MOCK_METHOD(void,
|
||||||
@ -63,6 +79,7 @@ class MockRuntimeDelegate : public RuntimeDelegate {
|
|||||||
public:
|
public:
|
||||||
MOCK_METHOD(std::string, DefaultRouteName, (), (override));
|
MOCK_METHOD(std::string, DefaultRouteName, (), (override));
|
||||||
MOCK_METHOD(void, ScheduleFrame, (bool), (override));
|
MOCK_METHOD(void, ScheduleFrame, (bool), (override));
|
||||||
|
MOCK_METHOD(void, EndWarmUpFrame, (), (override));
|
||||||
MOCK_METHOD(void,
|
MOCK_METHOD(void,
|
||||||
Render,
|
Render,
|
||||||
(std::unique_ptr<flutter::LayerTree>, float),
|
(std::unique_ptr<flutter::LayerTree>, float),
|
||||||
@ -117,6 +134,51 @@ class MockRuntimeController : public RuntimeController {
|
|||||||
MOCK_METHOD(bool, NotifyIdle, (fml::TimeDelta), (override));
|
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(
|
std::unique_ptr<PlatformMessage> MakePlatformMessage(
|
||||||
const std::string& channel,
|
const std::string& channel,
|
||||||
const std::map<std::string, std::string>& values,
|
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_;
|
std::shared_ptr<fml::ConcurrentTaskRunner> image_decoder_task_runner_;
|
||||||
fml::TaskRunnerAffineWeakPtr<SnapshotDelegate> snapshot_delegate_;
|
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
|
} // namespace
|
||||||
|
|
||||||
TEST_F(EngineTest, Create) {
|
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
|
} // namespace flutter
|
||||||
|
@ -2,11 +2,18 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// 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:isolate';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
|
void expect(Object? a, Object? b) {
|
||||||
|
if (a != b) {
|
||||||
|
throw AssertionError('Expected $a to == $b');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void main() {}
|
void main() {}
|
||||||
|
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
@ -349,11 +356,6 @@ Future<void> toImageSync() async {
|
|||||||
|
|
||||||
onBeforeToImageSync();
|
onBeforeToImageSync();
|
||||||
final Image image = picture.toImageSync(20, 25);
|
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.width, 20);
|
||||||
expect(image.height, 25);
|
expect(image.height, 25);
|
||||||
|
|
||||||
@ -529,3 +531,31 @@ void testReportViewWidths() {
|
|||||||
nativeReportViewWidthsCallback(getCurrentViewWidths());
|
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();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:litetest/litetest.dart';
|
import 'package:litetest/litetest.dart';
|
||||||
@ -46,4 +47,31 @@ void main() {
|
|||||||
expect(constraints.isSatisfiedBy(const Size(400, 500)), false);
|
expect(constraints.isSatisfiedBy(const Size(400, 500)), false);
|
||||||
expect(constraints / 2, const ViewConstraints(minWidth: 50, maxWidth: 100, minHeight: 150, maxHeight: 200));
|
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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user