aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <jarred@jarredsumner.com> 2022-03-01 00:47:31 -0800
committerGravatar Jarred Sumner <jarred@jarredsumner.com> 2022-03-01 00:47:31 -0800
commit114c0e8ed2a0eea835139ece3677efce09a8b702 (patch)
treebb1c4f6d3e3514585728b29ddf3d4d052e223b15
parent9ccb520e9e93cf85227eb37febec6acdf45bd9cf (diff)
downloadbun-114c0e8ed2a0eea835139ece3677efce09a8b702.tar.gz
bun-114c0e8ed2a0eea835139ece3677efce09a8b702.tar.zst
bun-114c0e8ed2a0eea835139ece3677efce09a8b702.zip
[bun.js] Implement `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval`
-rw-r--r--README.md31
-rw-r--r--integration/bunjs-only-snippets/setInterval.test.js35
-rw-r--r--integration/bunjs-only-snippets/setTimeout.test.js39
-rw-r--r--src/bun_js.zig25
-rw-r--r--src/io/io_darwin.zig4
-rw-r--r--src/io/io_linux.zig4
-rw-r--r--src/javascript/jsc/base.zig2
-rw-r--r--src/javascript/jsc/bindings/ZigGlobalObject.cpp8
-rw-r--r--src/javascript/jsc/javascript.zig183
-rw-r--r--src/network_thread.zig3
-rw-r--r--src/thread_pool.zig2
11 files changed, 295 insertions, 41 deletions
diff --git a/README.md b/README.md
index 61af6e349..a331dc35f 100644
--- a/README.md
+++ b/README.md
@@ -256,22 +256,21 @@ From there, make sure to import the `dist/tailwind.css` file (or what you chose
bun is a project with incredibly large scope, and it’s early days.
-| Feature | In |
-| --------------------------------------------------------------------------------------- | --------------- |
-| [Hash components for Fast Refresh](https://github.com/Jarred-Sumner/bun/issues/18) | JSX Transpiler |
-| Source Maps | JavaScript |
-| Source Maps | CSS |
-| [`extends`](https://www.typescriptlang.org/tsconfig#extends) in tsconfig.json | TS Transpiler |
-| [TypeScript Decorators](https://www.typescriptlang.org/docs/handbook/decorators.html) | TS Transpiler |
-| `@jsxPragma` comments | JS Transpiler |
-| JSX source file name | JS Transpiler |
-| Sharing `.bun` files | bun |
-| [setTimeout](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout) & setInterval | bun.js |
-| [workspace: dependencies](https://github.com/Jarred-Sumner/bun/issues/83) | Package manager |
-| [git: dependencies](https://github.com/Jarred-Sumner/bun/issues/82) | Package manager |
-| [github: dependencies](https://github.com/Jarred-Sumner/bun/issues/81) | Package manager |
-| [link: dependencies](https://github.com/Jarred-Sumner/bun/issues/81) | Package manager |
-| Dates & timestamps | TOML parser |
+| Feature | In |
+| ------------------------------------------------------------------------------------- | --------------- |
+| [Hash components for Fast Refresh](https://github.com/Jarred-Sumner/bun/issues/18) | JSX Transpiler |
+| Source Maps | JavaScript |
+| Source Maps | CSS |
+| [`extends`](https://www.typescriptlang.org/tsconfig#extends) in tsconfig.json | TS Transpiler |
+| [TypeScript Decorators](https://www.typescriptlang.org/docs/handbook/decorators.html) | TS Transpiler |
+| `@jsxPragma` comments | JS Transpiler |
+| JSX source file name | JS Transpiler |
+| Sharing `.bun` files | bun |
+| [workspace: dependencies](https://github.com/Jarred-Sumner/bun/issues/83) | Package manager |
+| [git: dependencies](https://github.com/Jarred-Sumner/bun/issues/82) | Package manager |
+| [github: dependencies](https://github.com/Jarred-Sumner/bun/issues/81) | Package manager |
+| [link: dependencies](https://github.com/Jarred-Sumner/bun/issues/81) | Package manager |
+| Dates & timestamps | TOML parser |
<small>
JS Transpiler == JavaScript Transpiler
diff --git a/integration/bunjs-only-snippets/setInterval.test.js b/integration/bunjs-only-snippets/setInterval.test.js
new file mode 100644
index 000000000..f633998cd
--- /dev/null
+++ b/integration/bunjs-only-snippets/setInterval.test.js
@@ -0,0 +1,35 @@
+import { it, expect } from "bun:test";
+
+it("setInterval", async () => {
+ var counter = 0;
+ var start;
+ const result = await new Promise((resolve, reject) => {
+ start = performance.now();
+
+ var id = setInterval(() => {
+ counter++;
+ if (counter === 10) {
+ resolve(counter);
+ clearInterval(id);
+ }
+ }, 1);
+ });
+
+ expect(result).toBe(10);
+ expect(performance.now() - start >= 10).toBe(true);
+});
+
+it("clearInterval", async () => {
+ var called = false;
+ const id = setInterval(() => {
+ called = true;
+ expect(false).toBe(true);
+ }, 1);
+ clearInterval(id);
+ await new Promise((resolve, reject) => {
+ setInterval(() => {
+ resolve();
+ }, 10);
+ });
+ expect(called).toBe(false);
+});
diff --git a/integration/bunjs-only-snippets/setTimeout.test.js b/integration/bunjs-only-snippets/setTimeout.test.js
new file mode 100644
index 000000000..55f71712c
--- /dev/null
+++ b/integration/bunjs-only-snippets/setTimeout.test.js
@@ -0,0 +1,39 @@
+import { it, expect } from "bun:test";
+
+it("setTimeout", async () => {
+ var lastID = -1;
+ const result = await new Promise((resolve, reject) => {
+ var numbers = [];
+
+ for (let i = 1; i < 100; i++) {
+ const id = setTimeout(() => {
+ numbers.push(i);
+ if (i === 99) {
+ resolve(numbers);
+ }
+ }, i);
+ expect(id > lastID).toBe(true);
+ lastID = id;
+ }
+ });
+
+ for (let j = 0; j < result.length; j++) {
+ expect(result[j]).toBe(j + 1);
+ }
+ expect(result.length).toBe(99);
+});
+
+it("clearTimeout", async () => {
+ var called = false;
+ const id = setTimeout(() => {
+ called = true;
+ expect(false).toBe(true);
+ }, 1);
+ clearTimeout(id);
+ await new Promise((resolve, reject) => {
+ setTimeout(() => {
+ resolve();
+ }, 10);
+ });
+ expect(called).toBe(false);
+});
diff --git a/src/bun_js.zig b/src/bun_js.zig
index 5f673fb06..d304cbb7e 100644
--- a/src/bun_js.zig
+++ b/src/bun_js.zig
@@ -105,9 +105,32 @@ pub const Run = struct {
this.vm.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), false) catch {};
}
Output.prettyErrorln("\n", .{});
+ Output.flush();
}
- Output.flush();
+ {
+ var i: usize = 0;
+ while (this.vm.*.event_loop.pending_tasks_count.loadUnchecked() > 0 or this.vm.timer.active > 0) {
+ this.vm.tick();
+ i +%= 1;
+
+ if (i > 0 and i % 100 == 0) {
+ std.time.sleep(std.time.ns_per_us);
+ }
+ }
+
+ if (i > 0) {
+ if (this.vm.log.msgs.items.len > 0) {
+ if (Output.enable_ansi_colors) {
+ this.vm.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), true) catch {};
+ } else {
+ this.vm.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), false) catch {};
+ }
+ Output.prettyErrorln("\n", .{});
+ Output.flush();
+ }
+ }
+ }
Global.exit(0);
}
diff --git a/src/io/io_darwin.zig b/src/io/io_darwin.zig
index e57b6710e..086a8a116 100644
--- a/src/io/io_darwin.zig
+++ b/src/io/io_darwin.zig
@@ -406,6 +406,10 @@ completed: FIFO(Completion) = .{},
io_pending: FIFO(Completion) = .{},
last_event_fd: std.atomic.Atomic(u32) = std.atomic.Atomic(u32).init(32),
+pub fn hasNoWork(this: *IO) bool {
+ return this.io_inflight == 0 and this.io_pending.peek() == null and this.completed.peek() == null and this.timeouts.peek() == null;
+}
+
pub fn init(_: u12, _: u32) !IO {
const kq = try os.kqueue();
assert(kq > -1);
diff --git a/src/io/io_linux.zig b/src/io/io_linux.zig
index 2ba2b3e36..4e4d9137f 100644
--- a/src/io/io_linux.zig
+++ b/src/io/io_linux.zig
@@ -453,6 +453,10 @@ completed: FIFO(Completion) = .{},
next_tick: FIFO(Completion) = .{},
+pub fn hasNoWork(this: *IO) bool {
+ return this.completed.peek() == null and this.unqueued.peek() == null and this.next_tick.peek() == null;
+}
+
pub fn init(entries_: u12, flags: u32) !IO {
var ring: IO_Uring = undefined;
var entries = entries_;
diff --git a/src/javascript/jsc/base.zig b/src/javascript/jsc/base.zig
index 664dc0486..7c3fda1c0 100644
--- a/src/javascript/jsc/base.zig
+++ b/src/javascript/jsc/base.zig
@@ -1854,6 +1854,7 @@ const BigIntStats = JSC.Node.BigIntStats;
const Transpiler = @import("./api/transpiler.zig");
const TextEncoder = WebCore.TextEncoder;
const TextDecoder = WebCore.TextDecoder;
+const TimeoutTask = JSC.BunTimer.Timeout.TimeoutTask;
pub const JSPrivateDataPtr = TaggedPointerUnion(.{
ResolveError,
BuildError,
@@ -1877,6 +1878,7 @@ pub const JSPrivateDataPtr = TaggedPointerUnion(.{
Transpiler,
TextEncoder,
TextDecoder,
+ TimeoutTask,
});
pub inline fn GetJSPrivateData(comptime Type: type, ref: js.JSObjectRef) ?*Type {
diff --git a/src/javascript/jsc/bindings/ZigGlobalObject.cpp b/src/javascript/jsc/bindings/ZigGlobalObject.cpp
index dd9982737..a3ec63516 100644
--- a/src/javascript/jsc/bindings/ZigGlobalObject.cpp
+++ b/src/javascript/jsc/bindings/ZigGlobalObject.cpp
@@ -507,28 +507,28 @@ void GlobalObject::installAPIGlobals(JSClassRef* globals, int count)
extraStaticGlobals.uncheckedAppend(
GlobalPropertyInfo { setTimeoutIdentifier,
JSC::JSFunction::create(vm(), JSC::jsCast<JSC::JSGlobalObject*>(this), 0,
- "setTimeout", functionQueueMicrotask),
+ "setTimeout", functionSetTimeout),
JSC::PropertyAttribute::DontDelete | 0 });
JSC::Identifier clearTimeoutIdentifier = JSC::Identifier::fromString(vm(), "clearTimeout"_s);
extraStaticGlobals.uncheckedAppend(
GlobalPropertyInfo { clearTimeoutIdentifier,
JSC::JSFunction::create(vm(), JSC::jsCast<JSC::JSGlobalObject*>(this), 0,
- "clearTimeout", functionQueueMicrotask),
+ "clearTimeout", functionClearTimeout),
JSC::PropertyAttribute::DontDelete | 0 });
JSC::Identifier setIntervalIdentifier = JSC::Identifier::fromString(vm(), "setInterval"_s);
extraStaticGlobals.uncheckedAppend(
GlobalPropertyInfo { setIntervalIdentifier,
JSC::JSFunction::create(vm(), JSC::jsCast<JSC::JSGlobalObject*>(this), 0,
- "setInterval", functionQueueMicrotask),
+ "setInterval", functionSetInterval),
JSC::PropertyAttribute::DontDelete | 0 });
JSC::Identifier clearIntervalIdentifier = JSC::Identifier::fromString(vm(), "clearInterval"_s);
extraStaticGlobals.uncheckedAppend(
GlobalPropertyInfo { clearIntervalIdentifier,
JSC::JSFunction::create(vm(), JSC::jsCast<JSC::JSGlobalObject*>(this), 0,
- "clearInterval", functionQueueMicrotask),
+ "clearInterval", functionClearInterval),
JSC::PropertyAttribute::DontDelete | 0 });
auto clientData = Bun::clientData(vm());
diff --git a/src/javascript/jsc/javascript.zig b/src/javascript/jsc/javascript.zig
index 727ac9654..879e944e3 100644
--- a/src/javascript/jsc/javascript.zig
+++ b/src/javascript/jsc/javascript.zig
@@ -14,6 +14,7 @@ const default_allocator = _global.default_allocator;
const StoredFileDescriptorType = _global.StoredFileDescriptorType;
const Arena = @import("../../mimalloc_arena.zig").Arena;
const C = _global.C;
+const NetworkThread = @import("http").NetworkThread;
pub fn zigCast(comptime Destination: type, value: anytype) *Destination {
return @ptrCast(*Destination, @alignCast(@alignOf(*Destination), value));
@@ -52,6 +53,7 @@ const Headers = WebCore.Headers;
const Fetch = WebCore.Fetch;
const FetchEvent = WebCore.FetchEvent;
const js = @import("../../jsc.zig").C;
+const JSC = @import("../../jsc.zig");
const JSError = @import("./base.zig").JSError;
const d = @import("./base.zig").d;
const MarkedArrayBuffer = @import("./base.zig").MarkedArrayBuffer;
@@ -1116,46 +1118,138 @@ pub const Bun = struct {
pub const Timer = struct {
last_id: i32 = 0,
warned: bool = false,
+ active: u32 = 0,
+ timeouts: TimeoutMap = TimeoutMap{},
+
+ const TimeoutMap = std.AutoArrayHashMapUnmanaged(i32, *Timeout);
pub fn getNextID() callconv(.C) i32 {
VirtualMachine.vm.timer.last_id += 1;
return VirtualMachine.vm.timer.last_id;
}
+ pub const Timeout = struct {
+ id: i32 = 0,
+ callback: JSValue,
+ interval: i32 = 0,
+ completion: NetworkThread.Completion = undefined,
+ repeat: bool = false,
+ io_task: ?*TimeoutTask = null,
+ cancelled: bool = false,
+
+ pub const TimeoutTask = IOTask(Timeout);
+
+ pub fn run(this: *Timeout, _task: *TimeoutTask) void {
+ this.io_task = _task;
+ NetworkThread.global.pool.io.?.timeout(
+ *Timeout,
+ this,
+ onCallback,
+ &this.completion,
+ std.time.ns_per_ms * @intCast(
+ u63,
+ @maximum(
+ this.interval,
+ 1,
+ ),
+ ),
+ );
+ }
+
+ pub fn onCallback(this: *Timeout, _: *NetworkThread.Completion, _: NetworkThread.AsyncIO.TimeoutError!void) void {
+ this.io_task.?.onFinish();
+ }
+
+ pub fn then(this: *Timeout, global: *JSGlobalObject) void {
+ if (!this.cancelled) {
+ if (this.repeat) {
+ this.io_task.?.deinit();
+ var task = Timeout.TimeoutTask.createOnJSThread(VirtualMachine.vm.allocator, global, this) catch unreachable;
+ this.io_task = task;
+ task.schedule();
+ }
+
+ _ = JSC.C.JSObjectCallAsFunction(global.ref(), this.callback.asObjectRef(), null, 0, null, null);
+
+ if (this.repeat)
+ return;
+ }
+
+ this.clear(global);
+ }
+
+ pub fn clear(this: *Timeout, global: *JSGlobalObject) void {
+ this.cancelled = true;
+ JSC.C.JSValueUnprotect(global.ref(), this.callback.asObjectRef());
+ _ = VirtualMachine.vm.timer.timeouts.swapRemove(this.id);
+ if (this.io_task) |task| {
+ task.deinit();
+ }
+ VirtualMachine.vm.allocator.destroy(this);
+ VirtualMachine.vm.timer.active -|= 1;
+ }
+ };
+
+ fn set(
+ id: i32,
+ globalThis: *JSGlobalObject,
+ callback: JSValue,
+ countdown: JSValue,
+ repeat: bool,
+ ) !void {
+ var timeout = try VirtualMachine.vm.allocator.create(Timeout);
+ js.JSValueProtect(globalThis.ref(), callback.asObjectRef());
+ timeout.* = Timeout{ .id = id, .callback = callback, .interval = countdown.toInt32(), .repeat = repeat };
+ var task = try Timeout.TimeoutTask.createOnJSThread(VirtualMachine.vm.allocator, globalThis, timeout);
+ VirtualMachine.vm.timer.timeouts.put(VirtualMachine.vm.allocator, id, timeout) catch unreachable;
+ VirtualMachine.vm.timer.active +|= 1;
+ task.schedule();
+ }
+
pub fn setTimeout(
- _: *JSGlobalObject,
- _: JSValue,
- _: JSValue,
+ globalThis: *JSGlobalObject,
+ callback: JSValue,
+ countdown: JSValue,
) callconv(.C) JSValue {
- VirtualMachine.vm.timer.last_id += 1;
+ const id = VirtualMachine.vm.timer.last_id;
+ VirtualMachine.vm.timer.last_id +%= 1;
- Output.prettyWarnln("setTimeout is not implemented yet", .{});
+ Timer.set(id, globalThis, callback, countdown, false) catch
+ return JSValue.jsUndefined();
- // For now, we are going to straight up lie
- return JSValue.jsNumber(@intCast(i32, VirtualMachine.vm.timer.last_id));
+ return JSValue.jsNumber(@intCast(i32, id));
}
pub fn setInterval(
- _: *JSGlobalObject,
- _: JSValue,
- _: JSValue,
+ globalThis: *JSGlobalObject,
+ callback: JSValue,
+ countdown: JSValue,
) callconv(.C) JSValue {
- VirtualMachine.vm.timer.last_id += 1;
+ const id = VirtualMachine.vm.timer.last_id;
+ VirtualMachine.vm.timer.last_id +%= 1;
+
+ Timer.set(id, globalThis, callback, countdown, true) catch
+ return JSValue.jsUndefined();
- Output.prettyWarnln("setInterval is not implemented yet", .{});
+ return JSValue.jsNumber(@intCast(i32, id));
+ }
- // For now, we are going to straight up lie
- return JSValue.jsNumber(@intCast(i32, VirtualMachine.vm.timer.last_id));
+ pub fn clearTimer(id: JSValue, _: *JSGlobalObject) void {
+ var timer: *Timeout = VirtualMachine.vm.timer.timeouts.get(id.toInt32()) orelse return;
+ timer.cancelled = true;
}
+
pub fn clearTimeout(
- _: *JSGlobalObject,
- _: JSValue,
+ globalThis: *JSGlobalObject,
+ id: JSValue,
) callconv(.C) JSValue {
+ Timer.clearTimer(id, globalThis);
return JSValue.jsUndefined();
}
pub fn clearInterval(
- _: *JSGlobalObject,
- _: JSValue,
+ globalThis: *JSGlobalObject,
+ id: JSValue,
) callconv(.C) JSValue {
+ Timer.clearTimer(id, globalThis);
return JSValue.jsUndefined();
}
@@ -1466,12 +1560,60 @@ pub fn ConcurrentPromiseTask(comptime Context: type) type {
}
};
}
+
+pub fn IOTask(comptime Context: type) type {
+ return struct {
+ const This = @This();
+ ctx: *Context,
+ task: NetworkThread.Task = .{ .callback = runFromThreadPool },
+ event_loop: *VirtualMachine.EventLoop,
+ allocator: std.mem.Allocator,
+ globalThis: *JSGlobalObject,
+
+ pub fn createOnJSThread(allocator: std.mem.Allocator, globalThis: *JSGlobalObject, value: *Context) !*This {
+ var this = try allocator.create(This);
+ this.* = .{
+ .event_loop = VirtualMachine.vm.event_loop,
+ .ctx = value,
+ .allocator = allocator,
+ .globalThis = globalThis,
+ };
+ return this;
+ }
+
+ pub fn runFromThreadPool(task: *NetworkThread.Task) void {
+ var this = @fieldParentPtr(This, "task", task);
+ Context.run(this.ctx, this);
+ }
+
+ pub fn runFromJS(this: This) void {
+ var ctx = this.ctx;
+ ctx.then(this.globalThis);
+ }
+
+ pub fn schedule(this: *This) void {
+ NetworkThread.init() catch return;
+ NetworkThread.global.pool.schedule(NetworkThread.Batch.from(&this.task));
+ }
+
+ pub fn onFinish(this: *This) void {
+ this.event_loop.enqueueTaskConcurrent(Task.init(this));
+ }
+
+ pub fn deinit(this: *This) void {
+ this.allocator.destroy(this);
+ }
+ };
+}
+
const AsyncTransformTask = @import("./api/transpiler.zig").TransformTask.AsyncTransformTask;
+const BunTimerTimeoutTask = Bun.Timer.Timeout.TimeoutTask;
// const PromiseTask = JSInternalPromise.Completion.PromiseTask;
pub const Task = TaggedPointerUnion(.{
FetchTasklet,
Microtask,
AsyncTransformTask,
+ BunTimerTimeoutTask,
// PromiseTask,
// TimeoutTasklet,
});
@@ -1558,6 +1700,11 @@ pub const VirtualMachine = struct {
transform_task.deinit();
finished += 1;
},
+ @field(Task.Tag, @typeName(BunTimerTimeoutTask)) => {
+ var transform_task: *BunTimerTimeoutTask = task.get(BunTimerTimeoutTask).?;
+ transform_task.*.runFromJS();
+ finished += 1;
+ },
else => unreachable,
}
}
diff --git a/src/network_thread.zig b/src/network_thread.zig
index ba1db6524..90c283522 100644
--- a/src/network_thread.zig
+++ b/src/network_thread.zig
@@ -1,8 +1,9 @@
const ThreadPool = @import("thread_pool");
pub const Batch = ThreadPool.Batch;
pub const Task = ThreadPool.Task;
+pub const Completion = AsyncIO.Completion;
const std = @import("std");
-const AsyncIO = @import("io");
+pub const AsyncIO = @import("io");
const Output = @import("./global.zig").Output;
const IdentityContext = @import("./identity_context.zig").IdentityContext;
const HTTP = @import("./http_client_async.zig");
diff --git a/src/thread_pool.zig b/src/thread_pool.zig
index 0a9551bd7..014519680 100644
--- a/src/thread_pool.zig
+++ b/src/thread_pool.zig
@@ -285,7 +285,7 @@ fn _wait(self: *ThreadPool, _is_waking: bool, comptime sleep_on_idle: bool) erro
const idle = HTTP.AsyncHTTP.active_requests_count.loadUnchecked() == 0;
- if (sleep_on_idle) {
+ if (sleep_on_idle and io.hasNoWork()) {
idle_network_ticks += @as(u32, @boolToInt(idle));
// If it's been roughly 2ms since the last network request, go to sleep!