[K/N] Add RunLoopTimer ^KT-63423
This commit is contained in:
committed by
Space Team
parent
b2d957b990
commit
d8fdc4d31a
@@ -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 <chrono>
|
||||||
|
#include <CoreFoundation/CFDate.h>
|
||||||
|
|
||||||
|
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<rep, period>;
|
||||||
|
using time_point = std::chrono::time_point<cf_clock>;
|
||||||
|
|
||||||
|
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
|
||||||
@@ -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 <CoreFoundation/CoreFoundation.h>
|
||||||
|
#include <CoreFoundation/CFRunLoop.h>
|
||||||
|
|
||||||
|
#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<void()>`, 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<RunLoopTimer> owner_;
|
||||||
|
object_ptr<CFRunLoopRef> 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<void()> callback, std::chrono::duration<double> 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<Subscription> attachToCurrentRunLoop(CFRunLoopMode mode = kCFRunLoopDefaultMode) noexcept {
|
||||||
|
return std::unique_ptr<Subscription>(new Subscription(*this, mode));
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static void perform(CFRunLoopTimerRef, void* callback) noexcept { static_cast<decltype(callback_)*>(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<void()> callback_; // TODO: std::function_ref?
|
||||||
|
CFRunLoopTimerContext timerContext_;
|
||||||
|
object_ptr<CFRunLoopTimerRef> timer_;
|
||||||
|
std::atomic<Subscription*> activeSubscription_ = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace kotlin::objc_support
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -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<testing::MockFunction<void()>> 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<testing::MockFunction<void()>> callback;
|
||||||
|
objc_support::RunLoopTimer timer(
|
||||||
|
callback.AsStdFunction(), std::chrono::microseconds(10), objc_support::cf_clock::now() + std::chrono::seconds(0));
|
||||||
|
std::atomic<int> 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<testing::MockFunction<void()>> callback;
|
||||||
|
objc_support::RunLoopTimer timer(
|
||||||
|
callback.AsStdFunction(), std::chrono::hours(10), objc_support::cf_clock::now() + std::chrono::microseconds(10));
|
||||||
|
std::atomic<int> 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<testing::MockFunction<void()>> 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<testing::MockFunction<void()>> 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<int> 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
|
||||||
Reference in New Issue
Block a user