From 2ecbd293fd03023490c1f4be4e20e3be91e20dd3 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Sun, 23 Jul 2023 22:38:40 -0300 Subject: [PATCH] feat: add native timers --- NativeScript/runtime/ModuleBinding.hpp | 3 +- NativeScript/runtime/Timers.cpp | 275 +++++++++++++++++++++++++ NativeScript/runtime/Timers.hpp | 75 +++++++ TestRunner/app/tests/Timers.js | 140 +++++++++++++ TestRunner/app/tests/index.js | 2 + v8ios.xcodeproj/project.pbxproj | 8 + 6 files changed, 502 insertions(+), 1 deletion(-) create mode 100644 NativeScript/runtime/Timers.cpp create mode 100644 NativeScript/runtime/Timers.hpp create mode 100644 TestRunner/app/tests/Timers.js diff --git a/NativeScript/runtime/ModuleBinding.hpp b/NativeScript/runtime/ModuleBinding.hpp index 5ebba5f9..5b3790ca 100644 --- a/NativeScript/runtime/ModuleBinding.hpp +++ b/NativeScript/runtime/ModuleBinding.hpp @@ -57,7 +57,8 @@ namespace tns { // V(worker) #define NODE_BINDINGS_WITH_PER_ISOLATE_INIT(V) \ -V(worker) +V(worker) \ +V(timers) enum { NM_F_BUILTIN = 1 << 0, // Unused. diff --git a/NativeScript/runtime/Timers.cpp b/NativeScript/runtime/Timers.cpp new file mode 100644 index 00000000..cfea6974 --- /dev/null +++ b/NativeScript/runtime/Timers.cpp @@ -0,0 +1,275 @@ +// +// Timers.cpp +// NativeScript +// +// Created by Eduardo Speroni on 7/23/23. +// Copyright © 2023 Progress. All rights reserved. +// + +#include "Timers.hpp" +#include +#include "Helpers.h" +#include "ModuleBinding.hpp" +#include "Caches.h" +#include "Runtime.h" + +using namespace v8; + + + +// Takes a value and transform into a positive number +// returns a negative number if the number is negative or invalid +inline static double +ToMaybePositiveValue(const v8::Local &v, const v8::Local &ctx) { + double value = -1; + if (v->IsNullOrUndefined()) { + return -1; + } + Local numberValue; + auto success = v->ToNumber(ctx).ToLocal(&numberValue); + if (success) { + value = numberValue->Value(); + if (isnan(value)) { + value = -1; + } + } + return value; +} + +static double now_ms() { + struct timespec res; + clock_gettime(CLOCK_MONOTONIC, &res); + return 1000.0 * res.tv_sec + (double) res.tv_nsec / 1e6; +} + +namespace tns { + +class TimerState { +public: + std::mutex timerMutex_; + std::atomic currentTimerId = 0; + robin_hood::unordered_map> timerMap_; + CFRunLoopRef runloop; + + void removeTask(const std::shared_ptr &task) { + removeTask(task->id_); + } + + void removeTask(const int &taskId) { + auto it = timerMap_.find(taskId); + if (it != timerMap_.end()) { + //auto wasScheduled = it->second->queued_; + auto timer = it->second->timer; + it->second->Unschedule(); + timerMap_.erase(it); + CFRunLoopTimerInvalidate(timer); + // timer and context will be released by the retain function + //CFRunLoopTimerContext context; + //CFRunLoopTimerGetContext(timer, &context); + //delete static_cast*>(context.info); + // CFRelease(timer); + } + } + + // this all comes from the android runtime implementation + void addTask(std::shared_ptr task) { + if (task->queued_) { + return; + } + // auto now = now_ms(); + // task->nestingLevel_ = nesting + 1; + task->queued_ = true; + // theoretically this should be >5 on the spec, but we're following chromium behavior here again + // if (task->nestingLevel_ >= 5 && task->frequency_ < 4) { + // task->frequency_ = 4; + // task->startTime_ = now; + // } + timerMap_.emplace(task->id_, task); + // not needed on the iOS runtime for now + // auto newTime = task->NextTime(now); + // task->dueTime_ = newTime; + } + +}; + +// this class is attached to the timer object itself +// we use a retain/release flow because we want to bind this to the Timer itself +// additionally it helps if we deal with timers on different threads +// The current implementation puts the timers on the runtime's runloop, so it shouldn't be necessary. +class TimerContext { +public: + std::atomic retainCount{0}; + std::shared_ptr task; + TimerState* state; + ~TimerContext() { + task->Unschedule(); + CFRelease(task->timer); + } + + static const void* TimerRetain(const void* ret) { + auto v = (TimerContext*)(ret); + v->retainCount++; + return ret; + } + + static void TimerRelease(const void* ret) { + auto v = (TimerContext*)(ret); + if(--v->retainCount <= 0) { + delete v; + }; + } +}; + + + + + +void Timers::Init(Isolate* isolate, Local globalTemplate) { + auto timerState = new TimerState(); + timerState->runloop = Runtime::GetRuntime(isolate)->RuntimeLoop(); + Caches::Get(isolate)->registerCacheBoundObject(timerState); + tns::NewFunctionTemplate(isolate, Timers::SetTimeoutCallback, v8::External::New(isolate, timerState)); + tns::SetMethod(isolate, globalTemplate, "__ns__setTimeout", Timers::SetTimeoutCallback, v8::External::New(isolate, timerState)); + tns::SetMethod(isolate, globalTemplate, "__ns__setInterval", Timers::SetIntervalCallback, v8::External::New(isolate, timerState)); + tns::SetMethod(isolate, globalTemplate, "__ns__clearTimeout", Timers::ClearTimeoutCallback, v8::External::New(isolate, timerState)); + tns::SetMethod(isolate, globalTemplate, "__ns__clearInterval", Timers::ClearTimeoutCallback, v8::External::New(isolate, timerState)); + Caches::Get(isolate)->registerCacheBoundObject(new TimerState()); + +} + + + +void TimerCallback(CFRunLoopTimerRef timer, void *info) { + TimerContext* data = (TimerContext*)info; + auto task = data->task; + // we check for this first so we can be 100% sure that this task is still alive + // since we're always dealing with the runtime's runloop, it should always work + // if we even support firing the timers in a another runloop, then this is useful as it'll avoid use-after-free issues + if (!task->queued_ || !Runtime::IsAlive(task->isolate_) || !task->wrapper.IsValid()) { + return; + } + auto isolate = task->isolate_; + + v8::Locker locker(isolate); + v8::Isolate::Scope isolate_scope(isolate); + v8::HandleScope handleScope(isolate); + // ensure we're still queued after locking + if(!task->queued_) { + return; + } + + v8::Local cb = task->callback_.Get(isolate); + v8::Local context = cb->GetCreationContextChecked(); + Context::Scope context_scope(context); + TryCatch tc(isolate); + auto argc = task->args_.get() == nullptr ? 0 : task->args_->size(); + if (argc > 0) { + Local argv[argc]; + for (int i = 0; i < argc; i++) { + argv[i] = task->args_->at(i)->Get(isolate); + } + (void)cb->Call(context, context->Global(), (int)argc, argv); + } else { + (void)cb->Call(context, context->Global(), 0, nullptr); + } + tc.ReThrow(); + + if (!task->repeats_) { + data->state->removeTask(task); + } +} + +void Timers::SetTimer(const v8::FunctionCallbackInfo& args, bool repeatable) { + auto argLength = args.Length(); + auto extData = args.Data().As(); + TimerState* state = reinterpret_cast(extData->Value()); + int id = ++state->currentTimerId; + if (argLength >= 1) { + if (!args[0]->IsFunction()) { + args.GetReturnValue().Set(-1); + return; + } + auto handler = args[0].As(); + auto isolate = args.GetIsolate(); + auto ctx = isolate->GetCurrentContext(); + long timeout = 0; + if (argLength >= 2) { + timeout = (long) ToMaybePositiveValue(args[1], ctx); + if (timeout < 0) { + timeout = 0; + } + } + std::shared_ptr>>> argArray; + if (argLength >= 3) { + auto otherArgLength = argLength - 2; + argArray = std::make_shared>>>( + otherArgLength); + for (int i = 0; i < otherArgLength; i++) { + (*argArray)[i] = std::make_shared>(isolate, args[i + 2]); + } + } + + auto task = std::make_shared(isolate, handler, timeout, repeatable, argArray, id, now_ms()); + + CFRunLoopTimerContext timerContext = {0, NULL, NULL, NULL, NULL}; + auto timerData = new TimerContext(); + timerData->task = task; + timerData->state = state; + timerContext.info = timerData; + timerContext.retain = TimerContext::TimerRetain; + timerContext.release = TimerContext::TimerRelease; + + // we do this because the timer should take hold of exactly 1 retaincount after scheduling + // so if by our manual release the retain is 0 then we need to cleanup the TimerContext + TimerContext::TimerRetain(timerData); + + + auto timeoutInSeconds = timeout / 1000.f; + auto timer = CFRunLoopTimerCreate(kCFAllocatorDefault, + CFAbsoluteTimeGetCurrent() + timeoutInSeconds, + repeatable ? timeoutInSeconds : 0, + 0, 0, + TimerCallback, + &timerContext); + state->addTask(task); + // set the actual timer we created + task->timer = timer; + CFRunLoopAddTimer(state->runloop, timer, kCFRunLoopCommonModes); + TimerContext::TimerRelease(timerData); + // auto task = std::make_shared(isolate, handler, timeout, repeatable, + // argArray, id, now_ms()); + // thiz->addTask(task); + } + args.GetReturnValue().Set(id); +} + +void Timers::SetTimeoutCallback(const v8::FunctionCallbackInfo& args) { + Timers::SetTimer(args, false); + +} + +void Timers::SetIntervalCallback(const v8::FunctionCallbackInfo& args) { + Timers::SetTimer(args, true); + +} + +void Timers::ClearTimeoutCallback(const v8::FunctionCallbackInfo &args) { + auto argLength = args.Length(); + auto extData = args.Data().As(); + auto thiz = reinterpret_cast(extData->Value()); + int id = -1; + if (argLength > 0) { + auto isolate = args.GetIsolate(); + auto ctx = isolate->GetCurrentContext(); + id = (int) ToMaybePositiveValue(args[0], ctx); + } + // ids start at 1 + if (id > 0) { + thiz->removeTask(id); + } +} + +} + + +NODE_BINDING_PER_ISOLATE_INIT_OBJ(timers, tns::Timers::Init) diff --git a/NativeScript/runtime/Timers.hpp b/NativeScript/runtime/Timers.hpp new file mode 100644 index 00000000..2181949e --- /dev/null +++ b/NativeScript/runtime/Timers.hpp @@ -0,0 +1,75 @@ +// +// Timers.hpp +// NativeScript +// +// Created by Eduardo Speroni on 7/23/23. +// Copyright © 2023 Progress. All rights reserved. +// + +#ifndef Timers_hpp +#define Timers_hpp + +#include "Common.h" +#include "robin_hood.h" +#include +#include "IsolateWrapper.h" + +namespace tns { + +class TimerTask { +public: + TimerTask(v8::Isolate *isolate, + const v8::Local &callback, double frequency, + bool repeats, + const std::shared_ptr>>> &args, + int id, double startTime) : isolate_(isolate), callback_(isolate, callback),args_(args), + frequency_(frequency), repeats_(repeats), startTime_(startTime), id_(id), wrapper(isolate) + { } + + inline double NextTime(double targetTime) { + if (frequency_ <= 0) { + return targetTime; + } + auto timeDiff = targetTime - startTime_; + auto div = std::div((long) timeDiff, (long) frequency_); + return startTime_ + frequency_ * (div.quot + 1); + } + + inline void Unschedule() { + callback_.Reset(); + args_.reset(); + isolate_ = nullptr; + queued_ = false; + } + + // unused for now as we're using CFRunLoopTimers + // + int nestingLevel_ = 0; + v8::Isolate *isolate_; + v8::Persistent callback_; + std::shared_ptr>>> args_; + double frequency_ = 0; + bool repeats_ = false; + bool queued_ = false; + + double dueTime_ = -1; + double startTime_ = -1; + int id_; + IsolateWrapper wrapper; + CFRunLoopTimerRef timer = nullptr; +}; +class Timers { +public: + static void Init(v8::Isolate* isolate, v8::Local globalTemplate); + +private: + static void SetTimeoutCallback(const v8::FunctionCallbackInfo& info); + static void SetIntervalCallback(const v8::FunctionCallbackInfo& info); + static void ClearTimeoutCallback(const v8::FunctionCallbackInfo& info); + static void SetTimer(const v8::FunctionCallbackInfo& info, bool repeatable); +}; +} + +#include + +#endif /* Timers_hpp */ diff --git a/TestRunner/app/tests/Timers.js b/TestRunner/app/tests/Timers.js new file mode 100644 index 00000000..bafece47 --- /dev/null +++ b/TestRunner/app/tests/Timers.js @@ -0,0 +1,140 @@ +describe("native timer", () => { + /** @type {global.setTimeout} */ + let setTimeout = global.__ns__setTimeout; + /** @type {global.setInterval} */ + let setInterval = global.__ns__setInterval; + /** @type global.setTimeout */ + /** @type {global.clearTimeout} */ + let clearTimeout = global.__ns__clearTimeout; + /** @type {global.clearInterval} */ + let clearInterval = global.__ns__clearInterval; + + it("exists", () => { + expect(setTimeout).toBeDefined(); + expect(setInterval).toBeDefined(); + expect(clearTimeout).toBeDefined(); + expect(clearInterval).toBeDefined(); + }); + + it("triggers timeout", (done) => { + const now = Date.now(); + setTimeout(() => { + expect(Date.now() - now).not.toBeLessThan(100); + done(); + }, 100); + }); + + it("triggers timeout", (done) => { + const now = Date.now(); + setTimeout(() => { + expect(Date.now() - now).not.toBeLessThan(100); + done(); + }, 100); + }); + + it("triggers interval", (done) => { + let calls = 0; + const itv = setInterval(() => { + calls++; + }, 100); + setTimeout(() => { + clearInterval(itv); + expect(calls).toBe(10); + done(); + }, 1000); + }); + + it("cancels timeout", (done) => { + let triggered = false; + const now = Date.now(); + const timeout = setTimeout(() => { + triggered = true; + }, 100); + clearTimeout(timeout); + setTimeout(() => { + expect(triggered).toBe(false); + done(); + }, 200); + }); + + it("cancels interval", (done) => { + let triggered = false; + const now = Date.now(); + const timeout = setInterval(() => { + triggered = true; + }, 100); + clearInterval(timeout); + setTimeout(() => { + expect(triggered).toBe(false); + done(); + }, 200); + }); + + it("cancels interval inside function", (done) => { + let calls = 0; + const itv = setInterval(() => { + calls++; + clearInterval(itv); + }, 10); + setTimeout(() => { + expect(calls).toBe(1); + done(); + }, 100); + }); + + it("preserves order", (done) => { + let calls = 0; + setTimeout(() => { + expect(calls).toBe(0); + calls++; + }); + setTimeout(() => { + expect(calls).toBe(1); + calls++; + done(); + }); + }); + it("frees up resources after complete", (done) => { + let timeout = 0; + let interval = 0; + let weakRef; + { + let obj = { + value: 0, + }; + weakRef = new WeakRef(obj); + timeout = setTimeout(() => { + obj.value++; + }, 100); + interval = setInterval(() => { + obj.value++; + }, 50); + } + setTimeout(() => { + // use !! here because if you pass weakRef.get() it creates a strong reference (side effect of expect) + expect(!!weakRef.get()).toBe(true); + clearInterval(interval); + clearTimeout(timeout); + // use another timeout as native weakrefs can't be gced until we leave the isolate after being used once + setTimeout(() => { + gc(); + expect(!!weakRef.get()).toBe(false); + done(); + }); + }, 200); + }); + + it("dispatches when invoked in another queue", (done) => { + const background_queue = dispatch_get_global_queue( + qos_class_t.QOS_CLASS_DEFAULT, + 0 + ); + const current_queue = dispatch_get_current_queue(); + dispatch_async(background_queue, () => { + setTimeout(() => { + expect(dispatch_get_current_queue()).toBe(current_queue); + done(); + }) + }); + }); +}); diff --git a/TestRunner/app/tests/index.js b/TestRunner/app/tests/index.js index f5e505d7..5d6baf0f 100644 --- a/TestRunner/app/tests/index.js +++ b/TestRunner/app/tests/index.js @@ -78,6 +78,8 @@ require("./Modules"); // require("./RuntimeImplementedAPIs"); +require("./Timers"); + // Tests common for all runtimes. require("./shared/index").runAllTests(); diff --git a/v8ios.xcodeproj/project.pbxproj b/v8ios.xcodeproj/project.pbxproj index b37d1da6..4548a29c 100644 --- a/v8ios.xcodeproj/project.pbxproj +++ b/v8ios.xcodeproj/project.pbxproj @@ -9,6 +9,8 @@ /* Begin PBXBuildFile section */ 2B7EA6AF2353477000E5184E /* NativeScriptException.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2B7EA6AD2353476F00E5184E /* NativeScriptException.mm */; }; 2B7EA6B02353477000E5184E /* NativeScriptException.h in Headers */ = {isa = PBXBuildFile; fileRef = 2B7EA6AE2353477000E5184E /* NativeScriptException.h */; }; + 3C1850542A6DCB2D002ACC81 /* Timers.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3C1850522A6DCB2D002ACC81 /* Timers.cpp */; }; + 3C1850552A6DCB2D002ACC81 /* Timers.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 3C1850532A6DCB2D002ACC81 /* Timers.hpp */; }; 3C78BA5C2A0D600100C20A88 /* ModuleBinding.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 3C78BA5A2A0D600100C20A88 /* ModuleBinding.cpp */; }; 3C78BA5D2A0D600100C20A88 /* ModuleBinding.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 3C78BA5B2A0D600100C20A88 /* ModuleBinding.hpp */; }; 3CA6E53529A78C6000D30F8B /* IsolateWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 3CA6E53429A78C6000D30F8B /* IsolateWrapper.h */; }; @@ -420,6 +422,8 @@ /* Begin PBXFileReference section */ 2B7EA6AD2353476F00E5184E /* NativeScriptException.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = NativeScriptException.mm; sourceTree = ""; }; 2B7EA6AE2353477000E5184E /* NativeScriptException.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NativeScriptException.h; sourceTree = ""; }; + 3C1850522A6DCB2D002ACC81 /* Timers.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Timers.cpp; sourceTree = ""; }; + 3C1850532A6DCB2D002ACC81 /* Timers.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Timers.hpp; sourceTree = ""; }; 3C78BA5A2A0D600100C20A88 /* ModuleBinding.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ModuleBinding.cpp; sourceTree = ""; }; 3C78BA5B2A0D600100C20A88 /* ModuleBinding.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ModuleBinding.hpp; sourceTree = ""; }; 3CA6E53429A78C6000D30F8B /* IsolateWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IsolateWrapper.h; sourceTree = ""; }; @@ -1413,6 +1417,8 @@ 3CA6E53429A78C6000D30F8B /* IsolateWrapper.h */, 3C78BA5A2A0D600100C20A88 /* ModuleBinding.cpp */, 3C78BA5B2A0D600100C20A88 /* ModuleBinding.hpp */, + 3C1850522A6DCB2D002ACC81 /* Timers.cpp */, + 3C1850532A6DCB2D002ACC81 /* Timers.hpp */, ); path = runtime; sourceTree = ""; @@ -1490,6 +1496,7 @@ C23E8F7322CB386F0078FD4C /* ConcurrentQueue.h in Headers */, C22536B8241A318900192740 /* ffi.h in Headers */, C247C16F22F82842001D2CA2 /* v8-util.h in Headers */, + 3C1850552A6DCB2D002ACC81 /* Timers.hpp in Headers */, C2C8EE7222CE323C001F8CEC /* ConcurrentMap.h in Headers */, C2A6EF3123745A0B00E8FBE7 /* MetadataInlines.h in Headers */, C2F4D0AE232F85E20008A2EB /* SymbolIterator.h in Headers */, @@ -2093,6 +2100,7 @@ C2DDEB99229EAC8300345BFE /* Metadata.mm in Sources */, 6573B9D4291FE29F00B0ED7C /* V8RuntimeFactory.cpp in Sources */, C2DDEBB4229EAC8300345BFE /* DictionaryAdapter.mm in Sources */, + 3C1850542A6DCB2D002ACC81 /* Timers.cpp in Sources */, C298C027233C9AEA000DDF54 /* TSHelpers.cpp in Sources */, C2FEA16F22A3C75C00A5C0FC /* InlineFunctions.cpp in Sources */, C2DDEB9A229EAC8300345BFE /* MetadataBuilder.mm in Sources */,