diff --git a/flutter/shell/platform/tizen/BUILD.gn b/flutter/shell/platform/tizen/BUILD.gn index 3c21911..899babc 100644 --- a/flutter/shell/platform/tizen/BUILD.gn +++ b/flutter/shell/platform/tizen/BUILD.gn @@ -115,6 +115,7 @@ template("embedder") { "ecore_input", "ecore_wl2", "eina", + "glib-2.0", "gio-2.0", "feedback", "flutter_engine", @@ -236,6 +237,7 @@ executable("flutter_tizen_unittests") { "flutter_tizen_engine_unittest.cc", "flutter_tizen_texture_registrar_unittests.cc", "flutter_tizen_view_unittests.cc", + "tizen_event_loop_unittests.cc", ] ldflags = [ "-Wl,--unresolved-symbols=ignore-in-shared-libs" ] diff --git a/flutter/shell/platform/tizen/channels/window_channel.cc b/flutter/shell/platform/tizen/channels/window_channel.cc index 5e618e5..242f4bf 100644 --- a/flutter/shell/platform/tizen/channels/window_channel.cc +++ b/flutter/shell/platform/tizen/channels/window_channel.cc @@ -8,7 +8,6 @@ #include "flutter/shell/platform/tizen/channels/encodable_value_holder.h" #include "flutter/shell/platform/tizen/logger.h" #include "flutter/shell/platform/tizen/tizen_window.h" -#include "flutter/shell/platform/tizen/tizen_window_ecore_wl2.h" namespace flutter { diff --git a/flutter/shell/platform/tizen/flutter_tizen_display_monitor.cc b/flutter/shell/platform/tizen/flutter_tizen_display_monitor.cc index 0a5bdf6..740260a 100644 --- a/flutter/shell/platform/tizen/flutter_tizen_display_monitor.cc +++ b/flutter/shell/platform/tizen/flutter_tizen_display_monitor.cc @@ -3,7 +3,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include #include #include "flutter/shell/platform/tizen/flutter_tizen_display_monitor.h" @@ -29,12 +28,7 @@ void FlutterTizenDisplayMonitor::UpdateDisplays() { display.display_id = 0; display.single_display = true; - double fps = ecore_animator_frametime_get(); - if (fps <= 0.0) { - display.refresh_rate = 0.0; - } else { - display.refresh_rate = 1 / fps; - } + display.refresh_rate = 60.0; int32_t width = 0, height = 0, dpi = 0; FlutterTizenView* view = engine_->view(); diff --git a/flutter/shell/platform/tizen/flutter_tizen_engine_unittest.cc b/flutter/shell/platform/tizen/flutter_tizen_engine_unittest.cc index 1566558..a566555 100644 --- a/flutter/shell/platform/tizen/flutter_tizen_engine_unittest.cc +++ b/flutter/shell/platform/tizen/flutter_tizen_engine_unittest.cc @@ -4,8 +4,6 @@ #include "flutter/shell/platform/tizen/flutter_tizen_engine.h" -#include - #include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h" #include "flutter/shell/platform/tizen/testing/engine_modifier.h" #include "gtest/gtest.h" @@ -14,9 +12,6 @@ namespace flutter { namespace testing { class FlutterTizenEngineTest : public ::testing::Test { - public: - FlutterTizenEngineTest() { ecore_init(); } - protected: void SetUp() { FlutterDesktopEngineProperties engine_prop = {}; diff --git a/flutter/shell/platform/tizen/flutter_tizen_texture_registrar_unittests.cc b/flutter/shell/platform/tizen/flutter_tizen_texture_registrar_unittests.cc index b76074e..3d7e7e5 100644 --- a/flutter/shell/platform/tizen/flutter_tizen_texture_registrar_unittests.cc +++ b/flutter/shell/platform/tizen/flutter_tizen_texture_registrar_unittests.cc @@ -4,8 +4,6 @@ #include "flutter/shell/platform/tizen/flutter_tizen_texture_registrar.h" -#include - #include #include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h" @@ -17,9 +15,6 @@ namespace flutter { namespace testing { class FlutterTizenTextureRegistrarTest : public ::testing::Test { - public: - FlutterTizenTextureRegistrarTest() { ecore_init(); } - protected: void SetUp() { FlutterDesktopEngineProperties engine_prop = {}; diff --git a/flutter/shell/platform/tizen/flutter_tizen_view_unittests.cc b/flutter/shell/platform/tizen/flutter_tizen_view_unittests.cc index 0ac71df..35fc9b7 100644 --- a/flutter/shell/platform/tizen/flutter_tizen_view_unittests.cc +++ b/flutter/shell/platform/tizen/flutter_tizen_view_unittests.cc @@ -4,8 +4,6 @@ #include "flutter/shell/platform/tizen/flutter_tizen_view.h" -#include - #include #include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h" @@ -47,7 +45,6 @@ class TestTizenView : public TizenWindow { }; TEST(FlutterTizenViewTest, SendsAndRequestsViewFocus) { - ecore_init(); { FlutterDesktopEngineProperties properties = {}; properties.assets_path = "/foo/flutter_assets"; @@ -110,7 +107,6 @@ TEST(FlutterTizenViewTest, SendsAndRequestsViewFocus) { view.OnFocusChangeRequest(request); EXPECT_FALSE(tizen_view_ptr->activated); } - ecore_shutdown(); } } // namespace diff --git a/flutter/shell/platform/tizen/tizen_event_loop.cc b/flutter/shell/platform/tizen/tizen_event_loop.cc index 0724b86..0f50007 100644 --- a/flutter/shell/platform/tizen/tizen_event_loop.cc +++ b/flutter/shell/platform/tizen/tizen_event_loop.cc @@ -6,109 +6,115 @@ #include "tizen_event_loop.h" #include +#include namespace flutter { +namespace { + +GSourceFuncs kTaskSourceFuncs = { + nullptr, + nullptr, + [](GSource*, GSourceFunc callback, gpointer user_data) -> gboolean { + return callback(user_data); + }, + nullptr, + nullptr, + nullptr, +}; + +} // namespace + TizenEventLoop::TizenEventLoop(std::thread::id main_thread_id, CurrentTimeProc get_current_time, TaskExpiredCallback on_task_expired) : main_thread_id_(main_thread_id), get_current_time_(get_current_time), - on_task_expired_(std::move(on_task_expired)) { - ecore_pipe_ = ecore_pipe_add( - [](void* data, void* buffer, unsigned int nbyte) -> void { - auto* self = static_cast(data); - self->ExecuteTaskEvents(); + on_task_expired_(std::move(on_task_expired)), + task_source_(g_source_new(&kTaskSourceFuncs, sizeof(GSource))) { + g_source_set_callback( + task_source_, + [](gpointer data) -> gboolean { + static_cast(data)->ExecuteTaskEvents(); + return G_SOURCE_CONTINUE; }, - this); + this, nullptr); + + g_source_set_can_recurse(task_source_, TRUE); + g_source_attach(task_source_, nullptr); } TizenEventLoop::~TizenEventLoop() { - if (ecore_pipe_) { - ecore_pipe_del(ecore_pipe_); - } + *alive_ = false; + g_source_destroy(task_source_); + g_source_unref(task_source_); } bool TizenEventLoop::RunsTasksOnCurrentThread() const { return std::this_thread::get_id() == main_thread_id_; } +void TizenEventLoop::PostTask(FlutterTask flutter_task, + uint64_t flutter_target_time_nanos) { + std::lock_guard lock(task_queue_mutex_); + const bool should_reschedule = + task_queue_.empty() || + flutter_target_time_nanos < task_queue_.top().target_time_nanos; + task_queue_.push({flutter_target_time_nanos, task_order_++, flutter_task}); + if (should_reschedule) { + UpdateSourceReadyTimeLocked(get_current_time_()); + } +} + void TizenEventLoop::ExecuteTaskEvents() { - const TaskTimePoint now = TaskTimePoint::clock::now(); + std::vector expired_tasks; { - std::lock_guard lock1(task_queue_mutex_); - std::lock_guard lock2(expired_tasks_mutex_); - while (!task_queue_.empty()) { - const Task& top = task_queue_.top(); - - if (top.fire_time > now) { - break; - } - - expired_tasks_.push_back(task_queue_.top()); + std::lock_guard lock(task_queue_mutex_); + const uint64_t now = get_current_time_(); + while (!task_queue_.empty() && task_queue_.top().target_time_nanos <= now) { + expired_tasks.push_back(task_queue_.top().task); task_queue_.pop(); } + UpdateSourceReadyTimeLocked(now); } - OnTaskExpired(); -} -TizenEventLoop::TaskTimePoint TizenEventLoop::TimePointFromFlutterTime( - uint64_t flutter_target_time_nanos) { - const TaskTimePoint now = TaskTimePoint::clock::now(); - const uint64_t flutter_duration = - flutter_target_time_nanos - get_current_time_(); - return now + std::chrono::nanoseconds(flutter_duration); + std::shared_ptr alive = alive_; + for (const FlutterTask& task : expired_tasks) { + OnTaskExpired(&task); + if (!*alive) { + return; + } + } } -void TizenEventLoop::PostTask(FlutterTask flutter_task, - uint64_t flutter_target_time_nanos) { - Task task; - task.order = ++task_order_; - task.fire_time = TimePointFromFlutterTime(flutter_target_time_nanos); - task.task = flutter_task; - { - std::lock_guard lock(task_queue_mutex_); - task_queue_.push(task); +void TizenEventLoop::UpdateSourceReadyTimeLocked(uint64_t now) { + if (task_queue_.empty()) { + g_source_set_ready_time(task_source_, -1); + return; } - const double flutter_duration = - static_cast(flutter_target_time_nanos) - get_current_time_(); - if (flutter_duration > 0) { - ecore_timer_add( - flutter_duration / 1000000000.0, - [](void* data) -> Eina_Bool { - auto* self = static_cast(data); - if (self->ecore_pipe_) { - ecore_pipe_write(self->ecore_pipe_, nullptr, 0); - } - return ECORE_CALLBACK_CANCEL; - }, - this); - } else { - if (ecore_pipe_) { - ecore_pipe_write(ecore_pipe_, nullptr, 0); - } + const uint64_t target_time_nanos = task_queue_.top().target_time_nanos; + if (target_time_nanos <= now) { + g_source_set_ready_time(task_source_, 0); + return; } + + const uint64_t delay_nanos = target_time_nanos - now; + const uint64_t delay_micros = delay_nanos / 1000 + (delay_nanos % 1000 != 0); + g_source_set_ready_time( + task_source_, g_get_monotonic_time() + static_cast(delay_micros)); } TizenPlatformEventLoop::TizenPlatformEventLoop( std::thread::id main_thread_id, CurrentTimeProc get_current_time, TaskExpiredCallback on_task_expired) - : TizenEventLoop(main_thread_id, get_current_time, on_task_expired) {} - -TizenPlatformEventLoop::~TizenPlatformEventLoop() {} - -void TizenPlatformEventLoop::OnTaskExpired() { - std::vector local_expired_tasks; - { - std::lock_guard lock(expired_tasks_mutex_); - local_expired_tasks = std::move(expired_tasks_); - } + : TizenEventLoop(main_thread_id, + get_current_time, + std::move(on_task_expired)) {} - for (const Task& task : local_expired_tasks) { - on_task_expired_(&task.task); - } +void TizenPlatformEventLoop::OnTaskExpired(const FlutterTask* task) { + on_task_expired_(task); } } // namespace flutter diff --git a/flutter/shell/platform/tizen/tizen_event_loop.h b/flutter/shell/platform/tizen/tizen_event_loop.h index bf7817e..7472940 100644 --- a/flutter/shell/platform/tizen/tizen_event_loop.h +++ b/flutter/shell/platform/tizen/tizen_event_loop.h @@ -6,22 +6,19 @@ #ifndef EMBEDDER_TIZEN_EVENT_LOOP_H_ #define EMBEDDER_TIZEN_EVENT_LOOP_H_ -#include +#include -#include -#include -#include #include +#include #include #include #include #include "flutter/shell/platform/embedder/embedder.h" -#include "flutter/shell/platform/tizen/tizen_renderer.h" namespace flutter { -typedef uint64_t (*CurrentTimeProc)(); +using CurrentTimeProc = uint64_t (*)(); class TizenEventLoop { public: @@ -32,61 +29,52 @@ class TizenEventLoop { TaskExpiredCallback on_task_expired); virtual ~TizenEventLoop(); - // Prevent copying. TizenEventLoop(const TizenEventLoop&) = delete; TizenEventLoop& operator=(const TizenEventLoop&) = delete; bool RunsTasksOnCurrentThread() const; - - void ExecuteTaskEvents(); - - // Post a Flutter engine tasks to the event loop for delayed execution. void PostTask(FlutterTask flutter_task, uint64_t flutter_target_time_nanos); - virtual void OnTaskExpired() = 0; + virtual void OnTaskExpired(const FlutterTask* task) = 0; protected: - using TaskTimePoint = std::chrono::steady_clock::time_point; + std::thread::id main_thread_id_; + CurrentTimeProc get_current_time_; + TaskExpiredCallback on_task_expired_; + private: struct Task { + uint64_t target_time_nanos; uint64_t order; - TaskTimePoint fire_time; FlutterTask task; - struct Comparer { - bool operator()(const Task& a, const Task& b) { - if (a.fire_time == b.fire_time) { - return a.order > b.order; - } - return a.fire_time > b.fire_time; + bool operator<(const Task& other) const { + if (target_time_nanos == other.target_time_nanos) { + return order > other.order; } - }; + return target_time_nanos > other.target_time_nanos; + } }; - std::thread::id main_thread_id_; - CurrentTimeProc get_current_time_; - TaskExpiredCallback on_task_expired_; - std::mutex task_queue_mutex_; - std::priority_queue, Task::Comparer> task_queue_; - std::vector expired_tasks_; - std::mutex expired_tasks_mutex_; - std::atomic task_order_ = 0; - - private: - Ecore_Pipe* ecore_pipe_ = nullptr; + void ExecuteTaskEvents(); + void UpdateSourceReadyTimeLocked(uint64_t now); - // Returns a TaskTimePoint computed from the given target time from Flutter. - TaskTimePoint TimePointFromFlutterTime(uint64_t flutter_target_time_nanos); + std::mutex task_queue_mutex_; + std::priority_queue task_queue_; + uint64_t task_order_ = 0; + GSource* task_source_ = nullptr; + // Detects destruction of this object by a task run in ExecuteTaskEvents. + std::shared_ptr alive_ = std::make_shared(true); }; -class TizenPlatformEventLoop : public TizenEventLoop { +class TizenPlatformEventLoop final : public TizenEventLoop { public: TizenPlatformEventLoop(std::thread::id main_thread_id, CurrentTimeProc get_current_time, TaskExpiredCallback on_task_expired); - virtual ~TizenPlatformEventLoop(); + ~TizenPlatformEventLoop() override = default; - virtual void OnTaskExpired() override; + void OnTaskExpired(const FlutterTask* task) override; }; } // namespace flutter diff --git a/flutter/shell/platform/tizen/tizen_event_loop_unittests.cc b/flutter/shell/platform/tizen/tizen_event_loop_unittests.cc new file mode 100644 index 0000000..d67667a --- /dev/null +++ b/flutter/shell/platform/tizen/tizen_event_loop_unittests.cc @@ -0,0 +1,145 @@ +// Copyright 2026 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/shell/platform/tizen/tizen_event_loop.h" + +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" + +namespace flutter { +namespace testing { + +namespace { + +using namespace std::chrono_literals; + +uint64_t GetCurrentTimeNanos() { + return static_cast(g_get_monotonic_time()) * 1000; +} + +FlutterTask MakeTask(uint64_t id) { + FlutterTask task = {}; + task.task = id; + return task; +} + +} // namespace + +class TizenEventLoopTest : public ::testing::Test { + protected: + void SetUp() override { + event_loop_ = std::make_unique( + std::this_thread::get_id(), GetCurrentTimeNanos, + [this](const FlutterTask* task) { + executed_tasks_.push_back(task->task); + execution_time_ = GetCurrentTimeNanos(); + execution_thread_ = std::this_thread::get_id(); + if (on_task_) { + on_task_(*task); + } + }); + } + + void TearDown() override { event_loop_.reset(); } + + bool RunUntil(const std::function& condition, + std::chrono::milliseconds timeout = 1s) { + const auto deadline = std::chrono::steady_clock::now() + timeout; + while (!condition() && std::chrono::steady_clock::now() < deadline) { + g_main_context_iteration(nullptr, FALSE); + std::this_thread::sleep_for(1ms); + } + return condition(); + } + + std::unique_ptr event_loop_; + std::vector executed_tasks_; + uint64_t execution_time_ = 0; + std::thread::id execution_thread_; + std::function on_task_; +}; + +TEST_F(TizenEventLoopTest, ExecutesPastAndImmediateTasksInPostingOrder) { + const uint64_t now = GetCurrentTimeNanos(); + event_loop_->PostTask(MakeTask(1), now - 1); + event_loop_->PostTask(MakeTask(2), now); + event_loop_->PostTask(MakeTask(3), now); + + ASSERT_TRUE(RunUntil([this] { return executed_tasks_.size() == 3; })); + EXPECT_EQ(executed_tasks_, (std::vector{1, 2, 3})); +} + +TEST_F(TizenEventLoopTest, ReschedulesEarlierTaskWithoutRunningItEarly) { + const uint64_t now = GetCurrentTimeNanos(); + const uint64_t earlier_target = now + 20'000'000; + event_loop_->PostTask(MakeTask(1), now + 500'000'000); + event_loop_->PostTask(MakeTask(2), earlier_target); + + ASSERT_TRUE(RunUntil([this] { return !executed_tasks_.empty(); }, 250ms)); + EXPECT_EQ(executed_tasks_, (std::vector{2})); + EXPECT_GE(execution_time_, earlier_target); +} + +TEST_F(TizenEventLoopTest, RunsWorkerThreadTaskOnMainThread) { + std::thread worker( + [this] { event_loop_->PostTask(MakeTask(1), GetCurrentTimeNanos()); }); + worker.join(); + + ASSERT_TRUE(RunUntil([this] { return !executed_tasks_.empty(); })); + EXPECT_EQ(execution_thread_, std::this_thread::get_id()); +} + +TEST_F(TizenEventLoopTest, SupportsReentrantPostTask) { + on_task_ = [this](const FlutterTask& task) { + if (task.task == 1) { + event_loop_->PostTask(MakeTask(2), GetCurrentTimeNanos()); + } + }; + event_loop_->PostTask(MakeTask(1), GetCurrentTimeNanos()); + + ASSERT_TRUE(RunUntil([this] { return executed_tasks_.size() == 2; })); + EXPECT_EQ(executed_tasks_, (std::vector{1, 2})); +} + +TEST_F(TizenEventLoopTest, DispatchesTasksInsideNestedLoop) { + bool dispatched_in_nested_loop = false; + on_task_ = [&](const FlutterTask& task) { + if (task.task == 1) { + event_loop_->PostTask(MakeTask(2), GetCurrentTimeNanos()); + dispatched_in_nested_loop = + RunUntil([this] { return executed_tasks_.size() == 2; }, 250ms); + } + }; + event_loop_->PostTask(MakeTask(1), GetCurrentTimeNanos()); + + ASSERT_TRUE(RunUntil([this] { return executed_tasks_.size() == 2; })); + EXPECT_TRUE(dispatched_in_nested_loop); + EXPECT_EQ(executed_tasks_, (std::vector{1, 2})); +} + +TEST_F(TizenEventLoopTest, SurvivesDestructionFromTask) { + on_task_ = [this](const FlutterTask&) { event_loop_.reset(); }; + const uint64_t now = GetCurrentTimeNanos(); + event_loop_->PostTask(MakeTask(1), now - 1); + event_loop_->PostTask(MakeTask(2), now); + + ASSERT_TRUE(RunUntil([this] { return !executed_tasks_.empty(); })); + EXPECT_EQ(executed_tasks_, (std::vector{1})); +} + +TEST_F(TizenEventLoopTest, RemovesPendingSourceOnDestruction) { + event_loop_->PostTask(MakeTask(1), GetCurrentTimeNanos()); + event_loop_.reset(); + + g_main_context_iteration(nullptr, FALSE); + EXPECT_TRUE(executed_tasks_.empty()); +} + +} // namespace testing +} // namespace flutter