From d8fdc4d31af9f68d3aedf3312c321fe7d361fb12 Mon Sep 17 00:00:00 2001 From: Alexander Shabalin Date: Wed, 29 Nov 2023 12:20:49 +0100 Subject: [PATCH] [K/N] Add RunLoopTimer ^KT-63423 --- .../src/main/cpp/objc_support/CFClock.hpp | 35 ++++++ .../main/cpp/objc_support/RunLoopTimer.hpp | 113 ++++++++++++++++++ .../cpp/objc_support/RunLoopTimerTest.cpp | 93 ++++++++++++++ 3 files changed, 241 insertions(+) create mode 100644 kotlin-native/runtime/src/main/cpp/objc_support/CFClock.hpp create mode 100644 kotlin-native/runtime/src/main/cpp/objc_support/RunLoopTimer.hpp create mode 100644 kotlin-native/runtime/src/main/cpp/objc_support/RunLoopTimerTest.cpp diff --git a/kotlin-native/runtime/src/main/cpp/objc_support/CFClock.hpp b/kotlin-native/runtime/src/main/cpp/objc_support/CFClock.hpp new file mode 100644 index 00000000000..7efd2299892 --- /dev/null +++ b/kotlin-native/runtime/src/main/cpp/objc_support/CFClock.hpp @@ -0,0 +1,35 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ + +#pragma once + +#if KONAN_HAS_FOUNDATION_FRAMEWORK + +#include +#include + +namespace kotlin::objc_support { + +// A clock like `std::chrono::system_clock`, but uses CoreFoundation under the hood. +// Unlike `system_clock`, the epoch of this clock is 1 Jan 2001 00:00:00 GMT +class cf_clock { +public: + using rep = CFTimeInterval; + using period = std::ratio<1>; + using duration = std::chrono::duration; + using time_point = std::chrono::time_point; + + static constexpr bool is_steady = false; + + static time_point now() noexcept { return fromCFAbsoluteTime(CFAbsoluteTimeGetCurrent()); } + + static CFAbsoluteTime toCFAbsoluteTime(const time_point& t) noexcept { return t.time_since_epoch().count(); } + + static time_point fromCFAbsoluteTime(CFAbsoluteTime t) noexcept { return time_point(duration(t)); } +}; + +} // namespace kotlin::objc_support + +#endif \ No newline at end of file diff --git a/kotlin-native/runtime/src/main/cpp/objc_support/RunLoopTimer.hpp b/kotlin-native/runtime/src/main/cpp/objc_support/RunLoopTimer.hpp new file mode 100644 index 00000000000..1e6e4b2cef7 --- /dev/null +++ b/kotlin-native/runtime/src/main/cpp/objc_support/RunLoopTimer.hpp @@ -0,0 +1,113 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ + +#pragma once + +#if KONAN_HAS_FOUNDATION_FRAMEWORK + +#include +#include + +#include "RawPtr.hpp" +#include "Utils.hpp" +#include "objc_support/CFClock.hpp" +#include "objc_support/ObjectPtr.hpp" + +namespace kotlin::objc_support { + +// Smart pointer around `CFRunLoopTimer`. +// +// To attach to the current run loop use `attachToCurrentRunLoop`, it'll be detached when the returned `Subscription` +// is destroyed. +// `RunLoopTimer` is initialized with: +// - `std::function`, which will be called every time the run loop processes this timer. +// - `interval`, which is the desired average interval between firings +// - `initialFiring`, which is the minimum time before the first timer can fire. +// To override when the next firing can be performed, use `setNextFiring`. +// The underlying raw pointer is available via `handle`. +class RunLoopTimer : private Pinned { +public: + // A token that `RunLoopTimer` is attached to a run loop. + // + // Must be destroyed on the same thread that called `attachToCurrentRunLoop`. + class [[nodiscard]] Subscription : private Pinned { + // TODO: Consider making it movable. + public: + ~Subscription() { + RuntimeAssert(*runLoop_ == CFRunLoopGetCurrent(), "Must be destroyed on the same thread as created"); + CFRunLoopRemoveTimer(*runLoop_, *owner_->timer_, mode_); + owner_->unregisterSubscription(*this); + } + + private: + friend class RunLoopTimer; + + Subscription(RunLoopTimer& owner, CFRunLoopMode mode) noexcept : + owner_(&owner), runLoop_(object_ptr_retain, CFRunLoopGetCurrent()), mode_(mode) { + owner_->registerSubscription(*this); + CFRunLoopAddTimer(*runLoop_, *owner_->timer_, mode); + } + + raw_ptr owner_; + object_ptr runLoop_; + CFRunLoopMode mode_; + }; + + // Create `RunLoopSource` with `callback` that will be invoked each time, when an attached run loop processes this timer. + // `interval` sets the desired time between timer firing (the system will try to make the average time between firings be at least + // `interval`, but the time between 2 consecutive tasks may be smaller if the current average is larger). + // `fireDate` sets the minimum time before the timer can be fired for the first time. + RunLoopTimer(std::function callback, std::chrono::duration interval, cf_clock::time_point fireDate) noexcept : + callback_(std::move(callback)), + timerContext_{0, &callback_, nullptr, nullptr, nullptr}, + timer_(CFRunLoopTimerCreate(nullptr, cf_clock::toCFAbsoluteTime(fireDate), interval.count(), 0, 0, &perform, &timerContext_)) {} + + ~RunLoopTimer() { + auto* subscription = activeSubscription_.load(std::memory_order_relaxed); + RuntimeAssert(subscription == nullptr, "Expected no active subscription, but was %p", subscription); + } + + // Raw pointer to `CFRunLoopTimer`. + auto handle() noexcept { return timer_; } + + // `at` overrides the minimum time before the next timer firing. The override is for the next firing + // only, after it, the initially supplied `interval` will be used again. + void setNextFiring(cf_clock::time_point at) noexcept { CFRunLoopTimerSetNextFireDate(*timer_, cf_clock::toCFAbsoluteTime(at)); } + + // `interval` overrides the minimum time before the next timer firing. The override is for the next firing + // only, after it, the initially supplied `interval` will be used again. + void setNextFiring(cf_clock::duration interval) noexcept { setNextFiring(cf_clock::now() + interval); } + + // Attach this `RunLoopTimer` to the current thread's run loop. + [[nodiscard]] std::unique_ptr attachToCurrentRunLoop(CFRunLoopMode mode = kCFRunLoopDefaultMode) noexcept { + return std::unique_ptr(new Subscription(*this, mode)); + } + +private: + static void perform(CFRunLoopTimerRef, void* callback) noexcept { static_cast(callback)->operator()(); } + + void registerSubscription(Subscription& subscription) noexcept { + Subscription* actual = nullptr; + activeSubscription_.compare_exchange_strong(actual, &subscription, std::memory_order_relaxed); + RuntimeAssert( + actual == nullptr, "Cannot have more than one active subscription. Trying to regiser %p but %p is already active", + &subscription, actual); + } + + void unregisterSubscription(Subscription& subscription) noexcept { + Subscription* actual = &subscription; + activeSubscription_.compare_exchange_strong(actual, nullptr, std::memory_order_relaxed); + RuntimeAssert(actual == &subscription, "Expected %p to be active subscription. But was %p", &subscription, actual); + } + + std::function callback_; // TODO: std::function_ref? + CFRunLoopTimerContext timerContext_; + object_ptr timer_; + std::atomic activeSubscription_ = nullptr; +}; + +} // namespace kotlin::objc_support + +#endif \ No newline at end of file diff --git a/kotlin-native/runtime/src/main/cpp/objc_support/RunLoopTimerTest.cpp b/kotlin-native/runtime/src/main/cpp/objc_support/RunLoopTimerTest.cpp new file mode 100644 index 00000000000..1c0ac2313f9 --- /dev/null +++ b/kotlin-native/runtime/src/main/cpp/objc_support/RunLoopTimerTest.cpp @@ -0,0 +1,93 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the LICENSE file. + */ + +#if KONAN_HAS_FOUNDATION_FRAMEWORK + +#include "RunLoopTimer.hpp" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include "RunLoopTestSupport.hpp" +#include "Clock.hpp" + +using namespace kotlin; + +TEST(RunLoopTimerTest, MainThread) { + ASSERT_EQ(CFRunLoopGetMain(), CFRunLoopGetCurrent()); + testing::StrictMock> callback; + constexpr auto initialTimeout = std::chrono::milliseconds(50); + constexpr auto interval = std::chrono::milliseconds(10); + steady_clock::time_point start = steady_clock::now(); + objc_support::RunLoopTimer timer(callback.AsStdFunction(), interval, objc_support::cf_clock::now() + initialTimeout); + auto subscription = timer.attachToCurrentRunLoop(); + { + testing::InSequence seq; + EXPECT_CALL(callback, Call()).WillOnce([&] { + auto callbackTime = steady_clock::now(); + EXPECT_GE(callbackTime, start + initialTimeout); + }); + EXPECT_CALL(callback, Call()).WillOnce([&] { + auto callbackTime = steady_clock::now(); + EXPECT_GE(callbackTime, start + initialTimeout + interval); + }); + EXPECT_CALL(callback, Call()).WillOnce([&] { + auto callbackTime = steady_clock::now(); + EXPECT_GE(callbackTime, start + initialTimeout + interval + interval); + CFRunLoopStop(CFRunLoopGetMain()); + }); + } + CFRunLoopRun(); +} + +TEST(RunLoopTimerTest, RepeatedTimer) { + testing::StrictMock> callback; + objc_support::RunLoopTimer timer( + callback.AsStdFunction(), std::chrono::microseconds(10), objc_support::cf_clock::now() + std::chrono::seconds(0)); + std::atomic counter = 0; + EXPECT_CALL(callback, Call()).WillRepeatedly([&] { counter.fetch_add(1, std::memory_order_relaxed); }); + objc_support::test_support::RunLoopInScopedThread runLoop([&]() { return timer.attachToCurrentRunLoop(); }); + while (counter.load(std::memory_order_relaxed) < 2) { + std::this_thread::yield(); + } +} + +TEST(RunLoopTimerTest, EffectivelyOneShotTimer) { + testing::StrictMock> callback; + objc_support::RunLoopTimer timer( + callback.AsStdFunction(), std::chrono::hours(10), objc_support::cf_clock::now() + std::chrono::microseconds(10)); + std::atomic counter = 0; + EXPECT_CALL(callback, Call()).WillOnce([&] { counter.fetch_add(1, std::memory_order_relaxed); }); + objc_support::test_support::RunLoopInScopedThread runLoop([&]() { return timer.attachToCurrentRunLoop(); }); + while (counter.load(std::memory_order_relaxed) < 1) { + std::this_thread::yield(); + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); +} + +TEST(RunLoopTimerTest, EffectivelyNeverTimer) { + testing::StrictMock> callback; + objc_support::RunLoopTimer timer( + callback.AsStdFunction(), std::chrono::hours(10), objc_support::cf_clock::now() + std::chrono::hours(10)); + EXPECT_CALL(callback, Call()).Times(0); + objc_support::test_support::RunLoopInScopedThread runLoop([&]() { return timer.attachToCurrentRunLoop(); }); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); +} + +TEST(RunLoopTimerTest, RescheduleTimer) { + testing::StrictMock> callback; + objc_support::RunLoopTimer timer( + callback.AsStdFunction(), std::chrono::hours(10), objc_support::cf_clock::now() + std::chrono::hours(10)); + objc_support::test_support::RunLoopInScopedThread runLoop([&]() { return timer.attachToCurrentRunLoop(); }); + std::atomic counter = 0; + EXPECT_CALL(callback, Call()).WillOnce([&] { counter.fetch_add(1, std::memory_order_relaxed); }); + timer.setNextFiring(std::chrono::microseconds(10)); + while (counter.load(std::memory_order_relaxed) < 1) { + std::this_thread::yield(); + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); +} + +#endif \ No newline at end of file