Skip to content

Commit 9aaa497

Browse files
committed
core: refactor timers
- Don't use JS objects for timer handles - Use a hash table in C rather an a Map in JS - Allocate memory without using the JS allocator (1) 1) This is important because the libuv loop outlives the JS runtime. Since handle closing is not sync and the callback where we need to free the structures will be called later it's not safe to use it. This is a small step in the direction of not using the JS allocator for structures that embed libuv handles.
1 parent 47d2712 commit 9aaa497

File tree

7 files changed

+55541
-54524
lines changed

7 files changed

+55541
-54524
lines changed

src/bundles/c/core/polyfills.c

+54,247-54,358
Large diffs are not rendered by default.

src/js/polyfills/timers.js

+4-58
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,7 @@
11
const core = globalThis[Symbol.for('tjs.internal.core')];
22

3-
const timers = new Map();
4-
let nextId = 1;
3+
globalThis.setTimeout = core.setTimeout;
4+
globalThis.clearTimeout = core.clearTimeout;
55

6-
function getNextId() {
7-
let id;
8-
9-
// eslint-disable-next-line no-constant-condition
10-
while (true) {
11-
id = nextId++;
12-
13-
if (!timers.has(id)) {
14-
break;
15-
}
16-
17-
if (nextId >= Number.MAX_SAFE_INTEGER) {
18-
nextId = 1;
19-
}
20-
}
21-
22-
return id;
23-
}
24-
25-
globalThis.setTimeout = (fn, ms, ...args) => {
26-
const timer = core.setTimeout(fn, ms, ...args);
27-
const id = getNextId();
28-
29-
timers.set(id, timer);
30-
31-
return id;
32-
};
33-
34-
globalThis.clearTimeout = id => {
35-
const timer = timers.get(id);
36-
37-
if (timer) {
38-
core.clearTimeout(timer);
39-
}
40-
41-
timers.delete(id);
42-
};
43-
44-
globalThis.setInterval = (fn, ms, ...args) => {
45-
const timer = core.setInterval(fn, ms, ...args);
46-
const id = getNextId();
47-
48-
timers.set(id, timer);
49-
50-
return id;
51-
};
52-
53-
globalThis.clearInterval = id => {
54-
const timer = timers.get(id);
55-
56-
if (timer) {
57-
core.clearInterval(timer);
58-
}
59-
60-
timers.delete(id);
61-
};
6+
globalThis.setInterval = core.setInterval;
7+
globalThis.clearInterval = core.setInterval;

src/mem.h

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* txiki.js
3+
*
4+
* Copyright (c) 2024-present Saúl Ibarra Corretgé <s@saghul.net>
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
19+
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
25+
#ifndef TJS_MEM_H
26+
#define TJS_MEM_H
27+
28+
#include "../deps/quickjs/cutils.h"
29+
30+
#include <stdlib.h>
31+
32+
#ifdef TJS__HAS_MIMALLOC
33+
#include <mimalloc.h>
34+
#endif
35+
36+
static inline size_t tjs__malloc_usable_size(const void *ptr) {
37+
#if defined(TJS__HAS_MIMALLOC)
38+
return mi_malloc_usable_size(ptr);
39+
#else
40+
return js__malloc_usable_size(ptr):
41+
#endif
42+
}
43+
44+
static inline void *tjs__malloc(size_t size) {
45+
#ifdef TJS__HAS_MIMALLOC
46+
return mi_malloc(size);
47+
#else
48+
return malloc(size);
49+
#endif
50+
}
51+
52+
static inline void *tjs__calloc(size_t count, size_t size) {
53+
#ifdef TJS__HAS_MIMALLOC
54+
return mi_calloc(count, size);
55+
#else
56+
return calloc(count, size);
57+
#endif
58+
}
59+
60+
static inline void tjs__free(void *ptr) {
61+
#ifdef TJS__HAS_MIMALLOC
62+
mi_free(ptr);
63+
#else
64+
free(ptr);
65+
#endif
66+
}
67+
68+
static inline void *tjs__realloc(void *ptr, size_t size) {
69+
#ifdef TJS__HAS_MIMALLOC
70+
return mi_realloc(ptr, size);
71+
#else
72+
return realloc(ptr, size);
73+
#endif
74+
}
75+
76+
#endif

src/private.h

+7
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
#include <stdbool.h>
3535
#include <uv.h>
3636

37+
typedef struct TJSTimer TJSTimer;
3738

3839
struct TJSRuntime {
3940
TJSRunOptions options;
@@ -54,6 +55,10 @@ struct TJSRuntime {
5455
struct {
5556
IM3Environment env;
5657
} wasm_ctx;
58+
struct {
59+
TJSTimer *timers;
60+
int64_t next_timer;
61+
} timers;
5762
};
5863

5964
void tjs__mod_dns_init(JSContext *ctx, JSValue ns);
@@ -96,6 +101,8 @@ JSValue tjs__get_args(JSContext *ctx);
96101

97102
int tjs__eval_bytecode(JSContext *ctx, const uint8_t *buf, size_t buf_len);
98103

104+
void tjs__destroy_timers(TJSRuntime *qrt);
105+
99106
uv_loop_t *TJS_GetLoop(TJSRuntime *qrt);
100107
TJSRuntime *TJS_NewRuntimeWorker(void);
101108
TJSRuntime *TJS_NewRuntimeInternal(bool is_worker, TJSRunOptions *options);

src/timers.c

+59-64
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,34 @@
2222
* THE SOFTWARE.
2323
*/
2424

25+
#include "hash.h"
26+
#include "mem.h"
2527
#include "private.h"
2628
#include "utils.h"
2729

30+
#define MAX_SAFE_INTEGER (((int64_t)1 << 53) - 1)
2831

29-
typedef struct {
32+
struct TJSTimer {
3033
JSContext *ctx;
31-
JSRuntime *rt;
34+
int64_t id;
3235
uv_timer_t handle;
36+
UT_hash_handle hh;
3337
int interval;
3438
JSValue func;
3539
int argc;
3640
JSValue argv[];
37-
} TJSTimer;
41+
};
42+
43+
static void uv__timer_close(uv_handle_t *handle) {
44+
TJSTimer *th = handle->data;
45+
CHECK_NOT_NULL(th);
46+
tjs__free(th);
47+
}
3848

39-
static void clear_timer(TJSTimer *th) {
49+
static void destroy_timer(TJSTimer *th) {
4050
JSContext *ctx = th->ctx;
51+
TJSRuntime *qrt = JS_GetContextOpaque(ctx);
52+
CHECK_NOT_NULL(qrt);
4153

4254
JS_FreeValue(ctx, th->func);
4355
th->func = JS_UNDEFINED;
@@ -47,6 +59,18 @@ static void clear_timer(TJSTimer *th) {
4759
th->argv[i] = JS_UNDEFINED;
4860
}
4961
th->argc = 0;
62+
63+
HASH_DEL(qrt->timers.timers, th);
64+
65+
uv_close((uv_handle_t *) &th->handle, uv__timer_close);
66+
}
67+
68+
void tjs__destroy_timers(TJSRuntime *qrt) {
69+
TJSTimer *th, *tmp;
70+
71+
HASH_ITER(hh, qrt->timers.timers, th, tmp) {
72+
destroy_timer(th);
73+
}
5074
}
5175

5276
static void call_timer(TJSTimer *th) {
@@ -61,12 +85,6 @@ static void call_timer(TJSTimer *th) {
6185
JS_FreeValue(ctx, ret);
6286
}
6387

64-
static void uv__timer_close(uv_handle_t *handle) {
65-
TJSTimer *th = handle->data;
66-
CHECK_NOT_NULL(th);
67-
js_free_rt(th->rt, th);
68-
}
69-
7088
static void uv__timer_cb(uv_timer_t *handle) {
7189
TJSTimer *th = handle->data;
7290
CHECK_NOT_NULL(th);
@@ -75,71 +93,43 @@ static void uv__timer_cb(uv_timer_t *handle) {
7593
tjs__execute_jobs(th->ctx);
7694

7795
call_timer(th);
78-
if (!th->interval)
79-
clear_timer(th);
80-
}
81-
82-
static JSClassID tjs_timer_class_id;
8396

84-
static void tjs_timer_finalizer(JSRuntime *rt, JSValue val) {
85-
TJSTimer *th = JS_GetOpaque(val, tjs_timer_class_id);
86-
if (th) {
87-
clear_timer(th);
88-
/* The handle might have been closed by the loop destruction in TJS_FreeRuntime. */
89-
if (!uv_is_closing((uv_handle_t *) &th->handle))
90-
uv_close((uv_handle_t *) &th->handle, uv__timer_close);
91-
}
92-
}
93-
94-
static void tjs_timer_mark(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func) {
95-
TJSTimer *th = JS_GetOpaque(val, tjs_timer_class_id);
96-
if (th) {
97-
JS_MarkValue(rt, th->func, mark_func);
98-
for (int i = 0; i < th->argc; i++)
99-
JS_MarkValue(rt, th->argv[i], mark_func);
100-
}
97+
if (!th->interval)
98+
destroy_timer(th);
10199
}
102100

103-
static JSClassDef tjs_timer_class = {
104-
"Timer",
105-
.finalizer = tjs_timer_finalizer,
106-
.gc_mark = tjs_timer_mark,
107-
};
101+
static JSValue tjs_setTimeout(JSContext *ctx, JSValue this_val, int argc, JSValue *argv, int magic) {
102+
TJSRuntime *qrt = JS_GetContextOpaque(ctx);
103+
CHECK_NOT_NULL(qrt);
108104

109-
static JSValue tjs_setTimeout(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic) {
110105
int64_t delay;
111-
JSValueConst func;
106+
JSValue func;
112107
TJSTimer *th;
113-
JSValue obj;
114108

115109
func = argv[0];
116110
if (!JS_IsFunction(ctx, func))
117111
return JS_ThrowTypeError(ctx, "not a function");
118112

119113
if (argc <= 1) {
120114
delay = 0;
121-
} else {
122-
if (JS_ToInt64(ctx, &delay, argv[1]))
123-
return JS_EXCEPTION;
115+
} else if (JS_ToInt64(ctx, &delay, argv[1])) {
116+
return JS_EXCEPTION;
124117
}
125118

126-
obj = JS_NewObjectClass(ctx, tjs_timer_class_id);
127-
if (JS_IsException(obj))
128-
return obj;
129-
130119
int nargs = argc - 2;
131120
if (nargs < 0) {
132121
nargs = 0;
133122
}
134123

135-
th = js_mallocz(ctx, sizeof(*th) + nargs * sizeof(JSValue));
136-
if (!th) {
137-
JS_FreeValue(ctx, obj);
138-
return JS_EXCEPTION;
139-
}
124+
th = tjs__malloc(sizeof(*th) + nargs * sizeof(JSValue));
125+
if (!th)
126+
return JS_ThrowOutOfMemory(ctx);
127+
128+
th->id = qrt->timers.next_timer++;
129+
if (qrt->timers.next_timer > MAX_SAFE_INTEGER)
130+
qrt->timers.next_timer = 1;
140131

141132
th->ctx = ctx;
142-
th->rt = JS_GetRuntime(ctx);
143133
CHECK_EQ(uv_timer_init(tjs_get_loop(ctx), &th->handle), 0);
144134
th->handle.data = th;
145135
th->interval = magic;
@@ -150,17 +140,26 @@ static JSValue tjs_setTimeout(JSContext *ctx, JSValueConst this_val, int argc, J
150140

151141
CHECK_EQ(uv_timer_start(&th->handle, uv__timer_cb, delay, magic ? delay : 0 /* repeat */), 0);
152142

153-
JS_SetOpaque(obj, th);
154-
return obj;
143+
HASH_ADD_INT64(qrt->timers.timers, id, th);
144+
145+
return JS_NewInt64(ctx, th->id);
155146
}
156147

157-
static JSValue tjs_clearTimeout(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
158-
TJSTimer *th = JS_GetOpaque2(ctx, argv[0], tjs_timer_class_id);
159-
if (!th)
148+
static JSValue tjs_clearTimeout(JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
149+
TJSRuntime *qrt = JS_GetContextOpaque(ctx);
150+
CHECK_NOT_NULL(qrt);
151+
int64_t timer_id;
152+
TJSTimer *th = NULL;
153+
154+
if (JS_ToInt64(ctx, &timer_id, argv[0]))
160155
return JS_EXCEPTION;
161156

162-
CHECK_EQ(uv_timer_stop(&th->handle), 0);
163-
clear_timer(th);
157+
HASH_FIND_INT64(qrt->timers.timers, &timer_id, th);
158+
159+
if (th != NULL) {
160+
CHECK_EQ(uv_timer_stop(&th->handle), 0);
161+
destroy_timer(th);
162+
}
164163

165164
return JS_UNDEFINED;
166165
}
@@ -171,9 +170,5 @@ static const JSCFunctionListEntry tjs_timer_funcs[] = { JS_CFUNC_MAGIC_DEF("setT
171170
TJS_CFUNC_DEF("clearInterval", 1, tjs_clearTimeout) };
172171

173172
void tjs__mod_timers_init(JSContext *ctx, JSValue ns) {
174-
JSRuntime *rt = JS_GetRuntime(ctx);
175-
176-
JS_NewClassID(rt, &tjs_timer_class_id);
177-
JS_NewClass(rt, tjs_timer_class_id, &tjs_timer_class);
178173
JS_SetPropertyFunctionList(ctx, ns, tjs_timer_funcs, countof(tjs_timer_funcs));
179174
}

0 commit comments

Comments
 (0)