Skip to content

Commit d414bcf

Browse files
authored
Make setTimeout asynchronous and add clearTimeout (#1146)
The current implementation of `setTimeout` dispatches repeatedly until the given delay is reached. This causes performance issues. This change improves performance by implementing `setTimeout` asynchronously on a separate thread. The `clearTimeout` function is currently not implemented and since it is closely related to `setTimeout`, it makes sense to implement it in this change, too.
1 parent a11132b commit d414bcf

File tree

11 files changed

+360
-32
lines changed

11 files changed

+360
-32
lines changed

Apps/UnitTests/Apple/App.mm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#include "../Shared/Tests.cpp"
1+
#include "../Shared/Tests.h"
22

33
int main() {
44
Babylon::Graphics::DeviceConfiguration graphicsConfig{};

Apps/UnitTests/CMakeLists.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ set(NPM_SCRIPTS
1414
"../node_modules/chai/chai.js"
1515
"../node_modules/mocha/mocha.js")
1616

17+
set(HEADERS
18+
"Shared/Tests.h")
19+
1720
set(ADDITIONAL_LIBRARIES "")
1821

1922
if(APPLE)
@@ -26,7 +29,7 @@ elseif(WIN32)
2629
set(TEST_APP "Win32/App.cpp")
2730
endif()
2831

29-
add_executable(UnitTests ${LOCAL_SCRIPTS} ${NPM_SCRIPTS} ${TEST_APP})
32+
add_executable(UnitTests ${LOCAL_SCRIPTS} ${NPM_SCRIPTS} ${TEST_APP} ${HEADERS})
3033

3134
target_link_to_dependencies(UnitTests
3235
UrlLib

Apps/UnitTests/Scripts/tests.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,119 @@ describe("PostProcesses", function() {
327327
});*/
328328
})
329329

330+
describe("setTimeout", function () {
331+
this.timeout(1000);
332+
it("should return an id greater than zero", function () {
333+
const id = setTimeout(() => { }, 0);
334+
expect(id).to.be.greaterThan(0);
335+
});
336+
it("should return an id greater than zero when given an undefined function", function () {
337+
const id = setTimeout(undefined, 0);
338+
expect(id).to.be.greaterThan(0);
339+
});
340+
it("should call the given function after the given delay", function (done) {
341+
const startTime = new Date().getTime();
342+
setTimeout(() => {
343+
try {
344+
expect(new Date().getTime() - startTime).to.be.at.least(10);
345+
done();
346+
}
347+
catch (e) {
348+
done(e);
349+
}
350+
}, 10);
351+
});
352+
it("should call the given nested function after the given delay", function (done) {
353+
const startTime = new Date().getTime();
354+
setTimeout(() => {
355+
setTimeout(() => {
356+
try {
357+
expect(new Date().getTime() - startTime).to.be.at.least(20);
358+
done();
359+
}
360+
catch (e) {
361+
done(e);
362+
}
363+
}, 10);
364+
}, 10);
365+
});
366+
it("should call the given function after the given delay when the delay is a string representing a valid number", function (done) {
367+
const startTime = new Date().getTime();
368+
setTimeout(() => {
369+
try {
370+
expect(new Date().getTime() - startTime).to.be.at.least(10);
371+
done();
372+
}
373+
catch (e) {
374+
done(e);
375+
}
376+
}, "10");
377+
});
378+
it("should call the given function after zero milliseconds when the delay is a string representing an invalid number", function (done) {
379+
setTimeout(() => {
380+
done();
381+
}, "a");
382+
});
383+
it("should call the given function after other tasks execute when the given delay is zero", function (done) {
384+
let trailingCodeExecuted = false;
385+
setTimeout(() => {
386+
try {
387+
expect(trailingCodeExecuted).to.be.true;
388+
done();
389+
}
390+
catch (e) {
391+
done(e);
392+
}
393+
}, 0);
394+
trailingCodeExecuted = true;
395+
});
396+
it("should call the given function after other tasks execute when the given delay is undefined", function (done) {
397+
let trailingCodeExecuted = false;
398+
setTimeout(() => {
399+
try {
400+
expect(trailingCodeExecuted).to.be.true;
401+
done();
402+
}
403+
catch (e) {
404+
done(e);
405+
}
406+
}, undefined);
407+
trailingCodeExecuted = true;
408+
});
409+
it("should call the given functions in the correct order", function (done) {
410+
const called = [];
411+
for (let i = 9; i >= 0; i--) {
412+
setTimeout(() => {
413+
called.push(i * 2);
414+
if (called.length === 10) {
415+
try {
416+
expect(called).to.deep.equal([0, 2, 4, 6, 8, 10, 12, 14, 16, 18]);
417+
done();
418+
}
419+
catch (e) {
420+
done(e);
421+
}
422+
}
423+
}, i * 2);
424+
}
425+
});
426+
})
427+
428+
describe("clearTimeout", function () {
429+
this.timeout(0);
430+
it("should stop the timeout matching the given timeout id", function (done) {
431+
const id = setTimeout(() => {
432+
done(new Error("Timeout was not cleared"));
433+
}, 0);
434+
clearTimeout(id);
435+
setTimeout(done, 100);
436+
});
437+
it("should do nothing if the given timeout id is undefined", function (done) {
438+
setTimeout(() => { done(); }, 0);
439+
clearTimeout(undefined);
440+
});
441+
})
442+
330443
mocha.run(failures => {
331444
// Test program will wait for code to be set before exiting
332445
if (failures > 0) {

Apps/UnitTests/Shared/Tests.cpp renamed to Apps/UnitTests/Shared/Tests.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ int Run(std::unique_ptr<Babylon::Graphics::Device> device)
4848
});
4949
Babylon::ScriptLoader loader{*runtime};
5050
loader.Eval("global = {};", ""); // Required for Chai.js as we do not have global in Babylon Native
51-
loader.Eval("window.clearTimeout = () => {};", ""); // TODO: implement clear timeout, required for Mocha timeouts to work correctly
5251
loader.Eval("location = {href: ''};", ""); // Required for Mocha.js as we do not have a location in Babylon Native
5352
loader.LoadScript("app:///Scripts/babylon.max.js");
5453
loader.LoadScript("app:///Scripts/babylonjs.materials.js");

Apps/UnitTests/Win32/App.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#include "../Shared/Tests.cpp"
1+
#include "../Shared/Tests.h"
22

33
LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
44
{

Apps/UnitTests/X11/App.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#include <X11/Xlib.h> // will include X11 which #defines None... Don't mess with order of includes.
44
#include <X11/Xutil.h>
55
#undef None
6-
#include "../Shared/Tests.cpp"
6+
#include "../Shared/Tests.h"
77

88
namespace
99
{

Polyfills/Window/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
set(SOURCES
22
"Include/Babylon/Polyfills/Window.h"
3+
"Source/TimeoutDispatcher.h"
4+
"Source/TimeoutDispatcher.cpp"
35
"Source/Window.h"
46
"Source/Window.cpp")
57

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
#include "TimeoutDispatcher.h"
2+
3+
#include <cassert>
4+
5+
namespace Babylon::Polyfills::Internal
6+
{
7+
namespace
8+
{
9+
using TimePoint = std::chrono::time_point<std::chrono::steady_clock, std::chrono::microseconds>;
10+
11+
TimePoint Now()
12+
{
13+
return std::chrono::time_point_cast<std::chrono::microseconds, std::chrono::steady_clock>(std::chrono::steady_clock::now());
14+
}
15+
}
16+
17+
struct TimeoutDispatcher::Timeout
18+
{
19+
TimeoutId id;
20+
21+
// Make this non-shared when JsRuntime::Dispatch supports it.
22+
std::shared_ptr<Napi::FunctionReference> function;
23+
24+
TimePoint time;
25+
26+
Timeout(TimeoutId id, std::shared_ptr<Napi::FunctionReference> function, TimePoint time)
27+
: id{id}
28+
, function{std::move(function)}
29+
, time{time}
30+
{
31+
}
32+
33+
Timeout(const Timeout&) = delete;
34+
Timeout(Timeout&&) = delete;
35+
};
36+
37+
TimeoutDispatcher::TimeoutDispatcher(Babylon::JsRuntime& runtime)
38+
: m_runtime{runtime}
39+
, m_thread{std::thread{&TimeoutDispatcher::ThreadFunction, this}}
40+
{
41+
}
42+
43+
TimeoutDispatcher::~TimeoutDispatcher()
44+
{
45+
{
46+
std::unique_lock<std::mutex> lk{m_mutex};
47+
m_idMap.clear();
48+
m_timeMap.clear();
49+
}
50+
51+
m_shutdown = true;
52+
m_condVariable.notify_one();
53+
m_thread.join();
54+
}
55+
56+
TimeoutDispatcher::TimeoutId TimeoutDispatcher::Dispatch(std::shared_ptr<Napi::FunctionReference> function, std::chrono::milliseconds delay)
57+
{
58+
if (delay.count() < 0)
59+
{
60+
delay = std::chrono::milliseconds{0};
61+
}
62+
63+
std::unique_lock<std::mutex> lk{m_mutex};
64+
65+
const auto id = NextTimeoutId();
66+
const auto earliestTime = m_timeMap.empty() ? TimePoint::max()
67+
: m_timeMap.cbegin()->second->time;
68+
const auto time = Now() + delay;
69+
const auto result = m_idMap.insert({id, std::make_unique<Timeout>(id, std::move(function), time)});
70+
m_timeMap.insert({time, result.first->second.get()});
71+
72+
if (time <= earliestTime)
73+
{
74+
m_runtime.Dispatch([this](Napi::Env) {
75+
m_condVariable.notify_one();
76+
});
77+
}
78+
79+
return id;
80+
}
81+
82+
void TimeoutDispatcher::Clear(TimeoutId id)
83+
{
84+
std::unique_lock<std::mutex> lk{m_mutex};
85+
const auto itId = m_idMap.find(id);
86+
if (itId != m_idMap.end())
87+
{
88+
const auto& timeout = itId->second;
89+
const auto timeRange = m_timeMap.equal_range(timeout->time);
90+
91+
assert(timeRange.first != m_timeMap.end() && "m_idMap and m_timeMap are out of sync");
92+
93+
for (auto itTime = timeRange.first; itTime != timeRange.second; itTime++)
94+
{
95+
if (itTime->second->id == id)
96+
{
97+
m_timeMap.erase(itTime);
98+
break;
99+
}
100+
}
101+
102+
m_idMap.erase(itId);
103+
}
104+
}
105+
106+
TimeoutDispatcher::TimeoutId TimeoutDispatcher::NextTimeoutId()
107+
{
108+
while (true)
109+
{
110+
++m_lastTimeoutId;
111+
112+
if (m_lastTimeoutId <= 0)
113+
{
114+
m_lastTimeoutId = 1;
115+
}
116+
117+
if (m_idMap.find(m_lastTimeoutId) == m_idMap.end())
118+
{
119+
return m_lastTimeoutId;
120+
}
121+
}
122+
}
123+
124+
void TimeoutDispatcher::ThreadFunction()
125+
{
126+
while (!m_shutdown)
127+
{
128+
std::unique_lock<std::mutex> lk{m_mutex};
129+
TimePoint nextTimePoint{};
130+
131+
while (!m_timeMap.empty())
132+
{
133+
nextTimePoint = m_timeMap.begin()->second->time;
134+
if (nextTimePoint <= Now())
135+
{
136+
break;
137+
}
138+
139+
m_condVariable.wait_until(lk, nextTimePoint);
140+
}
141+
142+
while (!m_timeMap.empty() && m_timeMap.begin()->second->time == nextTimePoint)
143+
{
144+
const auto* timeout = m_timeMap.begin()->second;
145+
CallFunction(timeout->function);
146+
m_timeMap.erase(m_timeMap.begin());
147+
m_idMap.erase(timeout->id);
148+
}
149+
150+
while (!m_shutdown && m_timeMap.empty())
151+
{
152+
m_condVariable.wait(lk);
153+
}
154+
}
155+
}
156+
157+
void TimeoutDispatcher::CallFunction(std::shared_ptr<Napi::FunctionReference> function)
158+
{
159+
if (function)
160+
{
161+
m_runtime.Dispatch([function = std::move(function)](Napi::Env)
162+
{ function->Call({}); });
163+
}
164+
}
165+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#pragma once
2+
3+
#include <Babylon/JsRuntime.h>
4+
#include <napi/napi.h>
5+
6+
#include <atomic>
7+
#include <chrono>
8+
#include <condition_variable>
9+
#include <map>
10+
#include <unordered_map>
11+
#include <cstdint>
12+
#include <thread>
13+
14+
namespace Babylon::Polyfills::Internal
15+
{
16+
class TimeoutDispatcher
17+
{
18+
using TimeoutId = int32_t;
19+
struct Timeout;
20+
21+
public:
22+
TimeoutDispatcher(Babylon::JsRuntime& runtime);
23+
~TimeoutDispatcher();
24+
25+
TimeoutId Dispatch(std::shared_ptr<Napi::FunctionReference> function, std::chrono::milliseconds delay);
26+
void Clear(TimeoutId id);
27+
28+
private:
29+
using TimePoint = std::chrono::time_point<std::chrono::steady_clock, std::chrono::microseconds>;
30+
31+
TimeoutId NextTimeoutId();
32+
void ThreadFunction();
33+
void CallFunction(std::shared_ptr<Napi::FunctionReference> function);
34+
35+
Babylon::JsRuntime& m_runtime;
36+
std::mutex m_mutex{};
37+
std::condition_variable m_condVariable{};
38+
TimeoutId m_lastTimeoutId{0};
39+
std::unordered_map<TimeoutId, std::unique_ptr<Timeout>> m_idMap;
40+
std::multimap<TimePoint, Timeout*> m_timeMap;
41+
std::atomic<bool> m_shutdown{false};
42+
std::thread m_thread;
43+
};
44+
}

0 commit comments

Comments
 (0)