Skip to content

Commit 6c1fba4

Browse files
ryantremRyan Tremblay
andauthored
Add setInterval and tests (#104)
This PR adds a `setInterval` implementation. This is largely copied over from the related change in Babylon Native: BabylonJS/BabylonNative@b2d5b2e#diff-23ceed54341331df9f4df6a9141772cd3613446bef20c7d96c62f5bebdbcf426 However, with the tests, I did discover a pre-existing timing/threading problem. Specifically, if the callback was scheduled in the JS thread via `m_runtime.Dispatch`, it is possible that the JS code could call `clearTimeout` before the dispatched callback is executed. This would result in the callback being called even after a call to `clearTimeout`. This became more obvious with `setInterval`, especially with small intervals. I changed the logic so that now the entries are removed from the time map on the timer thread, but the entries in the timer id map are left in place and removed only by calls on the JS thread - either a call to `clearTimeout`, or in the callback on the JS thread. This way, we know for sure in the callback on the JS thread whether the timer has been cleared. --------- Co-authored-by: Ryan Tremblay <[email protected]>
1 parent 89b8988 commit 6c1fba4

File tree

6 files changed

+153
-45
lines changed

6 files changed

+153
-45
lines changed

.github/jobs/ios.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ parameters:
66

77
jobs:
88
- job: ${{parameters.name}}
9-
timeoutInMinutes: 15
9+
timeoutInMinutes: 30
1010

1111
pool:
1212
vmImage: ${{parameters.vmImage}}

Polyfills/Scheduling/Source/Scheduling.cpp

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ namespace
44
{
55
constexpr auto JS_SET_TIMEOUT_NAME = "setTimeout";
66
constexpr auto JS_CLEAR_TIMEOUT_NAME = "clearTimeout";
7+
constexpr auto JS_SET_INTERVAL_NAME = "setInterval";
8+
constexpr auto JS_CLEAR_INTERVAL_NAME = "clearInterval";
79

8-
Napi::Value SetTimeout(const Napi::CallbackInfo& info, Babylon::Polyfills::Internal::TimeoutDispatcher& timeoutDispatcher)
10+
Napi::Value SetTimeout(const Napi::CallbackInfo& info, Babylon::Polyfills::Internal::TimeoutDispatcher& timeoutDispatcher, bool repeat)
911
{
1012
auto function =
1113
info[0].IsFunction()
@@ -14,7 +16,7 @@ namespace
1416

1517
auto delay = std::chrono::milliseconds{info[1].ToNumber().Int32Value()};
1618

17-
return Napi::Value::From(info.Env(), timeoutDispatcher.Dispatch(function, delay));
19+
return Napi::Value::From(info.Env(), timeoutDispatcher.Dispatch(function, delay, repeat));
1820
}
1921

2022
void ClearTimeout(const Napi::CallbackInfo& info, Babylon::Polyfills::Internal::TimeoutDispatcher& timeoutDispatcher)
@@ -33,14 +35,14 @@ namespace Babylon::Polyfills::Scheduling
3335
void BABYLON_API Initialize(Napi::Env env)
3436
{
3537
auto global = env.Global();
38+
auto timeoutDispatcher = std::make_shared<Internal::TimeoutDispatcher>(JsRuntime::GetFromJavaScript(env));
39+
3640
if (global.Get(JS_SET_TIMEOUT_NAME).IsUndefined() && global.Get(JS_CLEAR_TIMEOUT_NAME).IsUndefined())
3741
{
38-
auto timeoutDispatcher = std::make_shared<Internal::TimeoutDispatcher>(JsRuntime::GetFromJavaScript(env));
39-
4042
global.Set(JS_SET_TIMEOUT_NAME,
4143
Napi::Function::New(
4244
env, [timeoutDispatcher](const Napi::CallbackInfo& info) {
43-
return SetTimeout(info, *timeoutDispatcher);
45+
return SetTimeout(info, *timeoutDispatcher, false);
4446
},
4547
JS_SET_TIMEOUT_NAME));
4648

@@ -51,5 +53,22 @@ namespace Babylon::Polyfills::Scheduling
5153
},
5254
JS_CLEAR_TIMEOUT_NAME));
5355
}
56+
57+
if (global.Get(JS_SET_INTERVAL_NAME).IsUndefined() && global.Get(JS_CLEAR_INTERVAL_NAME).IsUndefined())
58+
{
59+
global.Set(JS_SET_INTERVAL_NAME,
60+
Napi::Function::New(
61+
env, [timeoutDispatcher](const Napi::CallbackInfo& info) {
62+
return SetTimeout(info, *timeoutDispatcher, true);
63+
},
64+
JS_SET_INTERVAL_NAME));
65+
66+
global.Set(JS_CLEAR_INTERVAL_NAME,
67+
Napi::Function::New(
68+
env, [timeoutDispatcher](const Napi::CallbackInfo& info) {
69+
ClearTimeout(info, *timeoutDispatcher);
70+
},
71+
JS_CLEAR_INTERVAL_NAME));
72+
}
5473
}
5574
}

Polyfills/Scheduling/Source/Scheduling.h

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,5 @@ namespace Babylon::Polyfills::Internal
1212
{
1313
public:
1414
static void Initialize(Napi::Env env);
15-
16-
Scheduling(const Napi::CallbackInfo& info);
17-
18-
private:
19-
JsRuntime& m_runtime;
20-
std::optional<TimeoutDispatcher> m_timeoutDispatcher;
21-
22-
static Napi::Value SetTimeout(const Napi::CallbackInfo& info);
23-
static void ClearTimeout(const Napi::CallbackInfo& info);
2415
};
2516
}

Polyfills/Scheduling/Source/TimeoutDispatcher.cpp

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "TimeoutDispatcher.h"
22

33
#include <cassert>
4+
#include <optional>
45

56
namespace Babylon::Polyfills::Internal
67
{
@@ -23,10 +24,13 @@ namespace Babylon::Polyfills::Internal
2324

2425
TimePoint time;
2526

26-
Timeout(TimeoutId id, std::shared_ptr<Napi::FunctionReference> function, TimePoint time)
27+
std::optional<std::chrono::milliseconds> interval;
28+
29+
Timeout(TimeoutId id, std::shared_ptr<Napi::FunctionReference> function, TimePoint time, std::optional<std::chrono::milliseconds> interval)
2730
: id{id}
2831
, function{std::move(function)}
2932
, time{time}
33+
, interval{interval}
3034
{
3135
}
3236

@@ -43,7 +47,7 @@ namespace Babylon::Polyfills::Internal
4347
TimeoutDispatcher::~TimeoutDispatcher()
4448
{
4549
{
46-
std::unique_lock<std::mutex> lk{m_mutex};
50+
std::unique_lock<std::recursive_mutex> lk{m_mutex};
4751
m_idMap.clear();
4852
m_timeMap.clear();
4953
}
@@ -53,43 +57,47 @@ namespace Babylon::Polyfills::Internal
5357
m_thread.join();
5458
}
5559

56-
TimeoutDispatcher::TimeoutId TimeoutDispatcher::Dispatch(std::shared_ptr<Napi::FunctionReference> function, std::chrono::milliseconds delay)
60+
TimeoutDispatcher::TimeoutId TimeoutDispatcher::Dispatch(std::shared_ptr<Napi::FunctionReference> function, std::chrono::milliseconds delay, bool repeat)
61+
{
62+
return DispatchImpl(function, delay, repeat, 0);
63+
}
64+
65+
TimeoutDispatcher::TimeoutId TimeoutDispatcher::DispatchImpl(std::shared_ptr<Napi::FunctionReference> function, std::chrono::milliseconds delay, bool repeat, TimeoutId id)
5766
{
5867
if (delay.count() < 0)
5968
{
6069
delay = std::chrono::milliseconds{0};
6170
}
6271

63-
std::unique_lock<std::mutex> lk{m_mutex};
72+
std::unique_lock<std::recursive_mutex> lk{m_mutex};
6473

65-
const auto id = NextTimeoutId();
66-
const auto earliestTime = m_timeMap.empty() ? TimePoint::max()
67-
: m_timeMap.cbegin()->second->time;
74+
if (id == 0)
75+
{
76+
id = NextTimeoutId();
77+
}
78+
const auto earliestTime = m_timeMap.empty() ? TimePoint::max() : m_timeMap.cbegin()->second->time;
6879
const auto time = Now() + delay;
69-
const auto result = m_idMap.insert({id, std::make_unique<Timeout>(id, std::move(function), time)});
80+
const auto result = m_idMap.insert({id, std::make_unique<Timeout>(id, std::move(function), time, repeat ? std::make_optional<std::chrono::milliseconds>(delay) : std::nullopt)});
7081
m_timeMap.insert({time, result.first->second.get()});
7182

7283
if (time <= earliestTime)
7384
{
74-
m_runtime.Dispatch([this](Napi::Env) {
75-
m_condVariable.notify_one();
76-
});
85+
m_condVariable.notify_one();
7786
}
7887

7988
return id;
8089
}
8190

8291
void TimeoutDispatcher::Clear(TimeoutId id)
8392
{
84-
std::unique_lock<std::mutex> lk{m_mutex};
93+
std::unique_lock<std::recursive_mutex> lk{m_mutex};
8594
const auto itId = m_idMap.find(id);
8695
if (itId != m_idMap.end())
8796
{
8897
const auto& timeout = itId->second;
8998
const auto timeRange = m_timeMap.equal_range(timeout->time);
9099

91-
assert(timeRange.first != m_timeMap.end() && "m_idMap and m_timeMap are out of sync");
92-
100+
// Remove any pending entries that have not yet been dispatched.
93101
for (auto itTime = timeRange.first; itTime != timeRange.second; itTime++)
94102
{
95103
if (itTime->second->id == id)
@@ -125,7 +133,7 @@ namespace Babylon::Polyfills::Internal
125133
{
126134
while (!m_shutdown)
127135
{
128-
std::unique_lock<std::mutex> lk{m_mutex};
136+
std::unique_lock<std::recursive_mutex> lk{m_mutex};
129137
TimePoint nextTimePoint{};
130138

131139
while (!m_timeMap.empty())
@@ -141,11 +149,15 @@ namespace Babylon::Polyfills::Internal
141149

142150
while (!m_timeMap.empty() && m_timeMap.begin()->second->time == nextTimePoint)
143151
{
144-
const auto* timeout = m_timeMap.begin()->second;
145-
auto function = std::move(timeout->function);
152+
const auto id = m_timeMap.begin()->second->id;
146153
m_timeMap.erase(m_timeMap.begin());
147-
m_idMap.erase(timeout->id);
148-
CallFunction(std::move(function));
154+
const auto repeat = m_idMap[id]->interval.has_value();
155+
if (repeat)
156+
{
157+
const auto timeout = std::move(m_idMap.extract(id).mapped());
158+
DispatchImpl(std::move(timeout->function), *timeout->interval, true, timeout->id);
159+
}
160+
CallFunction(id);
149161
}
150162

151163
while (!m_shutdown && m_timeMap.empty())
@@ -155,13 +167,32 @@ namespace Babylon::Polyfills::Internal
155167
}
156168
}
157169

158-
void TimeoutDispatcher::CallFunction(std::shared_ptr<Napi::FunctionReference> function)
170+
void TimeoutDispatcher::CallFunction(TimeoutId id)
159171
{
160-
if (function)
161-
{
162-
m_runtime.Dispatch([function = std::move(function)](Napi::Env) {
172+
m_runtime.Dispatch([id, this](Napi::Env) {
173+
std::shared_ptr<Napi::FunctionReference> function{};
174+
{
175+
std::unique_lock<std::recursive_mutex> lk{m_mutex};
176+
const auto it = m_idMap.find(id);
177+
if (it != m_idMap.end())
178+
{
179+
const auto repeat = it->second->interval.has_value();
180+
if (repeat)
181+
{
182+
function = it->second->function;
183+
}
184+
else
185+
{
186+
const auto timeout = std::move(m_idMap.extract(id).mapped());
187+
function = std::move(timeout->function);
188+
}
189+
}
190+
}
191+
192+
if (function)
193+
{
163194
function->Call({});
164-
});
165-
}
195+
}
196+
});
166197
}
167198
}

Polyfills/Scheduling/Source/TimeoutDispatcher.h

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,21 @@ namespace Babylon::Polyfills::Internal
2222
TimeoutDispatcher(Babylon::JsRuntime& runtime);
2323
~TimeoutDispatcher();
2424

25-
TimeoutId Dispatch(std::shared_ptr<Napi::FunctionReference> function, std::chrono::milliseconds delay);
25+
TimeoutId Dispatch(std::shared_ptr<Napi::FunctionReference> function, std::chrono::milliseconds delay, bool repeat = false);
2626
void Clear(TimeoutId id);
2727

2828
private:
2929
using TimePoint = std::chrono::time_point<std::chrono::steady_clock, std::chrono::microseconds>;
3030

31+
TimeoutId DispatchImpl(std::shared_ptr<Napi::FunctionReference> function, std::chrono::milliseconds delay, bool repeat, TimeoutId id);
32+
3133
TimeoutId NextTimeoutId();
3234
void ThreadFunction();
33-
void CallFunction(std::shared_ptr<Napi::FunctionReference> function);
35+
void CallFunction(TimeoutId id);
3436

3537
Babylon::JsRuntime& m_runtime;
36-
std::mutex m_mutex{};
37-
std::condition_variable m_condVariable{};
38+
std::recursive_mutex m_mutex{};
39+
std::condition_variable_any m_condVariable{};
3840
TimeoutId m_lastTimeoutId{0};
3941
std::unordered_map<TimeoutId, std::unique_ptr<Timeout>> m_idMap;
4042
std::multimap<TimePoint, Timeout*> m_timeMap;

Tests/UnitTests/Scripts/tests.js

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,20 +299,84 @@ describe("setTimeout", function () {
299299
});
300300

301301
describe("clearTimeout", function () {
302-
this.timeout(0);
302+
this.timeout(1000);
303+
303304
it("should stop the timeout matching the given timeout id", function (done) {
304305
const id = setTimeout(() => {
305306
done(new Error("Timeout was not cleared"));
306307
}, 0);
307308
clearTimeout(id);
308309
setTimeout(done, 100);
309310
});
311+
310312
it("should do nothing if the given timeout id is undefined", function (done) {
311313
setTimeout(() => { done(); }, 0);
312314
clearTimeout(undefined);
313315
});
316+
317+
it("should be interchangeable with clearInterval", function (done) {
318+
const id = setTimeout(() => {
319+
done(new Error("Interval was not cleared"));
320+
}, 0);
321+
clearInterval(id);
322+
setTimeout(done, 100);
323+
});
314324
});
315325

326+
describe("setInterval", function () {
327+
this.timeout(1000);
328+
329+
it("should return an id greater than zero", function () {
330+
const id = setInterval(() => { }, 0);
331+
clearInterval(id);
332+
expect(id).to.be.greaterThan(0);
333+
});
334+
335+
it("should call the given function at the given interval", function (done) {
336+
let startTime = new Date().getTime();
337+
let tickCount = 0;
338+
const id = setInterval(() => {
339+
try {
340+
tickCount++;
341+
expect(new Date().getTime() - startTime).to.be.at.least(tickCount * 10);
342+
if (tickCount > 2) {
343+
clearInterval(id);
344+
done();
345+
}
346+
}
347+
catch (e) {
348+
console.log(`finished with error: ${e}`);
349+
clearInterval(id);
350+
done(e);
351+
}
352+
}, 10);
353+
});
354+
});
355+
356+
describe("clearInterval", function () {
357+
this.timeout(1000);
358+
359+
it("should stop the interval matching the given interval id", function (done) {
360+
const id = setInterval(() => {
361+
done(new Error("Interval was not cleared"));
362+
}, 0);
363+
clearInterval(id);
364+
setTimeout(done, 100);
365+
});
366+
367+
it("should do nothing if the given interval id is undefined", function (done) {
368+
setTimeout(() => { done(); }, 0);
369+
clearInterval(undefined);
370+
});
371+
372+
it("should be interchangeable with clearTimeout", function (done) {
373+
const id = setInterval(() => {
374+
done(new Error("Interval was not cleared"));
375+
}, 0);
376+
clearTimeout(id);
377+
setTimeout(done, 100);
378+
});
379+
});
316380

317381
// Websocket
318382
if (hostPlatform !== "Unix") {
@@ -662,6 +726,7 @@ describe("Console", function () {
662726
expect(() => console.log("%d %f %s", 0 / 0, 0 / 0, 0 / 0)).to.not.throw();
663727
});
664728
});
729+
665730
function runTests() {
666731
mocha.run(failures => {
667732
// Test program will wait for code to be set before exiting

0 commit comments

Comments
 (0)