diff options
author | 2022-03-01 00:47:31 -0800 | |
---|---|---|
committer | 2022-03-01 00:47:31 -0800 | |
commit | 114c0e8ed2a0eea835139ece3677efce09a8b702 (patch) | |
tree | bb1c4f6d3e3514585728b29ddf3d4d052e223b15 | |
parent | 9ccb520e9e93cf85227eb37febec6acdf45bd9cf (diff) | |
download | bun-114c0e8ed2a0eea835139ece3677efce09a8b702.tar.gz bun-114c0e8ed2a0eea835139ece3677efce09a8b702.tar.zst bun-114c0e8ed2a0eea835139ece3677efce09a8b702.zip |
[bun.js] Implement `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval`
-rw-r--r-- | README.md | 31 | ||||
-rw-r--r-- | integration/bunjs-only-snippets/setInterval.test.js | 35 | ||||
-rw-r--r-- | integration/bunjs-only-snippets/setTimeout.test.js | 39 | ||||
-rw-r--r-- | src/bun_js.zig | 25 | ||||
-rw-r--r-- | src/io/io_darwin.zig | 4 | ||||
-rw-r--r-- | src/io/io_linux.zig | 4 | ||||
-rw-r--r-- | src/javascript/jsc/base.zig | 2 | ||||
-rw-r--r-- | src/javascript/jsc/bindings/ZigGlobalObject.cpp | 8 | ||||
-rw-r--r-- | src/javascript/jsc/javascript.zig | 183 | ||||
-rw-r--r-- | src/network_thread.zig | 3 | ||||
-rw-r--r-- | src/thread_pool.zig | 2 |
11 files changed, 295 insertions, 41 deletions
@@ -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! |