Skip to content

Commit

Permalink
More reliable macOS event loop (#1166)
Browse files Browse the repository at this point in the history
* More reliable macOS event loop

* Reduce CPU usage of idling

* Add another implementation

* Add benchmark

Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
  • Loading branch information
Jarred-Sumner and Jarred-Sumner authored Aug 29, 2022
1 parent b2141a2 commit c1734c6
Show file tree
Hide file tree
Showing 13 changed files with 607 additions and 144 deletions.
61 changes: 49 additions & 12 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -243,22 +243,26 @@ SRC_FILES := $(wildcard $(SRC_DIR)/*.cpp)
SRC_WEBCORE_FILES := $(wildcard $(SRC_DIR)/webcore/*.cpp)
SRC_SQLITE_FILES := $(wildcard $(SRC_DIR)/sqlite/*.cpp)
SRC_NODE_OS_FILES := $(wildcard $(SRC_DIR)/node_os/*.cpp)
SRC_IO_FILES := $(wildcard src/io/*.cpp)
SRC_BUILTINS_FILES := $(wildcard src/bun.js/builtins/*.cpp)

OBJ_FILES := $(patsubst $(SRC_DIR)/%.cpp,$(OBJ_DIR)/%.o,$(SRC_FILES))
WEBCORE_OBJ_FILES := $(patsubst $(SRC_DIR)/webcore/%.cpp,$(OBJ_DIR)/%.o,$(SRC_WEBCORE_FILES))
SQLITE_OBJ_FILES := $(patsubst $(SRC_DIR)/sqlite/%.cpp,$(OBJ_DIR)/%.o,$(SRC_SQLITE_FILES))
NODE_OS_OBJ_FILES := $(patsubst $(SRC_DIR)/node_os/%.cpp,$(OBJ_DIR)/%.o,$(SRC_NODE_OS_FILES))
BUILTINS_OBJ_FILES := $(patsubst src/bun.js/builtins/%.cpp,$(OBJ_DIR)/%.o,$(SRC_BUILTINS_FILES))
IO_FILES := $(patsubst src/io/%.cpp,$(OBJ_DIR)/%.o,$(SRC_IO_FILES))


DEBUG_OBJ_FILES := $(patsubst $(SRC_DIR)/%.cpp,$(DEBUG_OBJ_DIR)/%.o,$(SRC_FILES))
DEBUG_WEBCORE_OBJ_FILES := $(patsubst $(SRC_DIR)/webcore/%.cpp,$(DEBUG_OBJ_DIR)/%.o,$(SRC_WEBCORE_FILES))
DEBUG_SQLITE_OBJ_FILES := $(patsubst $(SRC_DIR)/sqlite/%.cpp,$(DEBUG_OBJ_DIR)/%.o,$(SRC_SQLITE_FILES))
DEBUG_NODE_OS_OBJ_FILES := $(patsubst $(SRC_DIR)/node_os/%.cpp,$(DEBUG_OBJ_DIR)/%.o,$(SRC_NODE_OS_FILES))
DEBUG_BUILTINS_OBJ_FILES := $(patsubst src/bun.js/builtins/%.cpp,$(DEBUG_OBJ_DIR)/%.o,$(SRC_BUILTINS_FILES))
DEBUG_IO_FILES := $(patsubst src/io/%.cpp,$(DEBUG_OBJ_DIR)/%.o,$(SRC_IO_FILES))

BINDINGS_OBJ := $(OBJ_FILES) $(WEBCORE_OBJ_FILES) $(SQLITE_OBJ_FILES) $(NODE_OS_OBJ_FILES) $(BUILTINS_OBJ_FILES)
DEBUG_BINDINGS_OBJ := $(DEBUG_OBJ_FILES) $(DEBUG_WEBCORE_OBJ_FILES) $(DEBUG_SQLITE_OBJ_FILES) $(DEBUG_NODE_OS_OBJ_FILES) $(DEBUG_BUILTINS_OBJ_FILES)
BINDINGS_OBJ := $(OBJ_FILES) $(WEBCORE_OBJ_FILES) $(SQLITE_OBJ_FILES) $(NODE_OS_OBJ_FILES) $(BUILTINS_OBJ_FILES) $(IO_FILES)
DEBUG_BINDINGS_OBJ := $(DEBUG_OBJ_FILES) $(DEBUG_WEBCORE_OBJ_FILES) $(DEBUG_SQLITE_OBJ_FILES) $(DEBUG_NODE_OS_OBJ_FILES) $(DEBUG_BUILTINS_OBJ_FILES) $(DEBUG_IO_FILES)

MAC_INCLUDE_DIRS := -I$(WEBKIT_RELEASE_DIR)/JavaScriptCore/PrivateHeaders \
-I$(WEBKIT_RELEASE_DIR)/WTF/Headers \
Expand Down Expand Up @@ -751,9 +755,9 @@ generate-install-script:
@esbuild --log-level=error --define:BUN_VERSION="\"$(PACKAGE_JSON_VERSION)\"" --define:process.env.NODE_ENV="\"production\"" --platform=node --format=cjs $(PACKAGES_REALPATH)/bun/install.ts > $(PACKAGES_REALPATH)/bun/install.js

.PHONY: fetch
fetch:
fetch: $(IO_FILES)
$(ZIG) build -Drelease-fast fetch-obj
$(CXX) $(PACKAGE_DIR)/fetch.o -g $(OPTIMIZATION_LEVEL) -o ./misctools/fetch $(DEFAULT_LINKER_FLAGS) -lc $(MINIMUM_ARCHIVE_FILES)
$(CXX) $(PACKAGE_DIR)/fetch.o -g $(OPTIMIZATION_LEVEL) -o ./misctools/fetch $(IO_FILES) $(DEFAULT_LINKER_FLAGS) -lc $(MINIMUM_ARCHIVE_FILES)
rm -rf $(PACKAGE_DIR)/fetch.o

.PHONY: sha
Expand All @@ -763,19 +767,31 @@ sha:
rm -rf $(PACKAGE_DIR)/sha.o

.PHONY: fetch-debug
fetch-debug:
fetch-debug: $(IO_FILES)
$(ZIG) build fetch-obj
$(CXX) $(DEBUG_PACKAGE_DIR)/fetch.o -g $(OPTIMIZATION_LEVEL) -o ./misctools/fetch $(DEFAULT_LINKER_FLAGS) -lc $(MINIMUM_ARCHIVE_FILES)
$(CXX) $(DEBUG_PACKAGE_DIR)/fetch.o -g $(OPTIMIZATION_LEVEL) -o ./misctools/fetch $(DEBUG_IO_FILES) $(DEFAULT_LINKER_FLAGS) -lc $(MINIMUM_ARCHIVE_FILES)

.PHONY: machbench-debug
machbench-debug: $(IO_FILES)
$(ZIG) build machbench-obj
$(CXX) $(DEBUG_PACKAGE_DIR)/machbench.o -g $(OPTIMIZATION_LEVEL) -o ./misctools/machbench $(DEBUG_IO_FILES) $(DEFAULT_LINKER_FLAGS) -lc $(MINIMUM_ARCHIVE_FILES)

.PHONY: machbench
machbench: $(IO_FILES)
$(ZIG) build -Drelease-fast machbench-obj
$(CXX) $(PACKAGE_DIR)/machbench.o -g $(OPTIMIZATION_LEVEL) -o ./misctools/machbench $(IO_FILES) $(DEFAULT_LINKER_FLAGS) -lc $(MINIMUM_ARCHIVE_FILES)
rm -rf $(PACKAGE_DIR)/machbench.o


.PHONY: httpbench-debug
httpbench-debug:
httpbench-debug: $(IO_FILES)
$(ZIG) build httpbench-obj
$(CXX) $(DEBUG_PACKAGE_DIR)/httpbench.o -fuse-ld=lld -g -o ./misctools/http_bench $(DEFAULT_LINKER_FLAGS) -lc $(MINIMUM_ARCHIVE_FILES)
$(CXX) $(IO_FILES) $(DEBUG_PACKAGE_DIR)/httpbench.o -g -o ./misctools/http_bench $(DEFAULT_LINKER_FLAGS) -lc $(MINIMUM_ARCHIVE_FILES)

.PHONY: httpbench-release
httpbench-release:
httpbench-release: $(IO_FILES)
$(ZIG) build -Drelease-fast httpbench-obj
$(CXX) $(PACKAGE_DIR)/httpbench.o -march=native -mtune=native -fuse-ld=lld -g $(OPTIMIZATION_LEVEL) -o ./misctools/http_bench $(DEFAULT_LINKER_FLAGS) -lc $(MINIMUM_ARCHIVE_FILES)
$(CXX) $(PACKAGE_DIR)/httpbench.o -g $(OPTIMIZATION_LEVEL) -o ./misctools/httpbench $(IO_FILES) $(DEFAULT_LINKER_FLAGS) -lc $(MINIMUM_ARCHIVE_FILES)
rm -rf $(PACKAGE_DIR)/httpbench.o

.PHONY: check-glibc-version-dependency
Expand Down Expand Up @@ -1288,12 +1304,12 @@ clean: clean-bindings
(cd $(BUN_DEPS_DIR)/zlib && make clean) || echo "";

.PHONY: release-bindings
release-bindings: $(OBJ_DIR) $(OBJ_FILES) $(WEBCORE_OBJ_FILES) $(SQLITE_OBJ_FILES) $(NODE_OS_OBJ_FILES) $(BUILTINS_OBJ_FILES)
release-bindings: $(OBJ_DIR) $(OBJ_FILES) $(WEBCORE_OBJ_FILES) $(SQLITE_OBJ_FILES) $(NODE_OS_OBJ_FILES) $(BUILTINS_OBJ_FILES) $(IO_FILES)

# Do not add $(DEBUG_DIR) to this list
# It will break caching, causing you to have to wait for every .cpp file to rebuild.
.PHONY: bindings
bindings: $(DEBUG_OBJ_FILES) $(DEBUG_WEBCORE_OBJ_FILES) $(DEBUG_SQLITE_OBJ_FILES) $(DEBUG_NODE_OS_OBJ_FILES) $(DEBUG_BUILTINS_OBJ_FILES)
bindings: $(DEBUG_OBJ_FILES) $(DEBUG_WEBCORE_OBJ_FILES) $(DEBUG_SQLITE_OBJ_FILES) $(DEBUG_NODE_OS_OBJ_FILES) $(DEBUG_BUILTINS_OBJ_FILES) $(DEBUG_IO_FILES)

.PHONY: jsc-bindings-mac
jsc-bindings-mac: bindings
Expand Down Expand Up @@ -1474,6 +1490,16 @@ $(OBJ_DIR)/%.o: $(SRC_DIR)/sqlite/%.cpp
$(EMIT_LLVM) \
-c -o $@ $<

$(OBJ_DIR)/%.o: src/io/%.cpp
$(CXX) $(CLANG_FLAGS) \
$(MACOS_MIN_FLAG) \
$(OPTIMIZATION_LEVEL) \
-fno-exceptions \
-fno-rtti \
-ferror-limit=1000 \
$(EMIT_LLVM) \
-c -o $@ $<

$(OBJ_DIR)/%.o: $(SRC_DIR)/node_os/%.cpp
$(CXX) $(CLANG_FLAGS) \
$(MACOS_MIN_FLAG) \
Expand Down Expand Up @@ -1518,6 +1544,17 @@ $(DEBUG_OBJ_DIR)/%.o: $(SRC_DIR)/webcore/%.cpp
$(EMIT_LLVM_FOR_DEBUG) \
-g3 -c -o $@ $<

$(DEBUG_OBJ_DIR)/%.o: src/io/%.cpp
$(CXX) $(CLANG_FLAGS) \
$(MACOS_MIN_FLAG) \
$(DEBUG_OPTIMIZATION_LEVEL) \
-fno-exceptions \
-fno-rtti \
-ferror-limit=1000 \
$(EMIT_LLVM_FOR_DEBUG) \
-g3 -c -o $@ $<


# $(DEBUG_OBJ_DIR) is not included here because it breaks
# detecting if a file needs to be rebuilt
$(DEBUG_OBJ_DIR)/%.o: $(SRC_DIR)/sqlite/%.cpp
Expand Down
16 changes: 16 additions & 0 deletions bench/snippets/set-timeout.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { bench, run } from "../node_modules/mitata/src/cli.mjs";

bench("setTimeout(, 4) 100 times", async () => {
var i = 100;
while (--i >= 0) {
await new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 4);
});
}
});

setTimeout(() => {
run({}).then(() => {});
}, 1);
22 changes: 22 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,28 @@ pub fn build(b: *std.build.Builder) !void {
headers_obj.addOptions("build_options", opts);
}

{
const headers_step = b.step("machbench-obj", "Build Machbench tool (object files)");
var headers_obj: *std.build.LibExeObjStep = b.addObject("machbench", "misctools/machbench.zig");
defer headers_step.dependOn(&headers_obj.step);
try configureObjectStep(b, headers_obj, target, obj.main_pkg_path.?);
var opts = b.addOptions();
opts.addOption(
bool,
"bindgen",
false,
);

opts.addOption(
bool,
"baseline",
is_baseline,
);
opts.addOption([:0]const u8, "sha", git_sha);
opts.addOption(bool, "is_canary", is_canary);
headers_obj.addOptions("build_options", opts);
}

{
const headers_step = b.step("fetch-obj", "Build fetch (object files)");
var headers_obj: *std.build.LibExeObjStep = b.addObject("fetch", "misctools/fetch.zig");
Expand Down
137 changes: 137 additions & 0 deletions misctools/machbench.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// most of this file is copy pasted from other files in misctools
const std = @import("std");
const bun = @import("../src/global.zig");
const string = bun.string;
const Output = bun.Output;
const Global = bun.Global;
const Environment = bun.Environment;
const strings = bun.strings;
const MutableString = bun.MutableString;
const stringZ = bun.stringZ;
const default_allocator = bun.default_allocator;
const C = bun.C;
const clap = @import("../src/deps/zig-clap/clap.zig");
const AsyncIO = @import("io");

const URL = @import("../src/url.zig").URL;
const Headers = @import("http").Headers;
const Method = @import("../src/http/method.zig").Method;
const ColonListType = @import("../src/cli/colon_list_type.zig").ColonListType;
const HeadersTuple = ColonListType(string, noop_resolver);
const path_handler = @import("../src/resolver/resolve_path.zig");
const NetworkThread = @import("http").NetworkThread;
const HTTP = @import("http");
fn noop_resolver(in: string) !string {
return in;
}

var waker: AsyncIO.Waker = undefined;

fn spamMe(count: usize) void {
Output.Source.configureNamedThread("1");
defer Output.flush();
var timer = std.time.Timer.start() catch unreachable;

var i: usize = 0;
while (i < count) : (i += 1) {
waker.wake() catch unreachable;
}
Output.prettyErrorln("[EVFILT_MACHPORT] Sent {any}", .{bun.fmt.fmtDuration(timer.read())});
}
const thread_count = 1;
pub fn machMain(runs: usize) anyerror!void {
defer Output.flush();
waker = try AsyncIO.Waker.init(bun.default_allocator);

var args = try std.process.argsAlloc(bun.default_allocator);
const count = std.fmt.parseInt(usize, args[args.len - 1], 10) catch 1024;
var elapsed: u64 = 0;

var remaining_runs: usize = runs;
while (remaining_runs > 0) : (remaining_runs -= 1) {
var threads: [thread_count]std.Thread = undefined;
var j: usize = 0;
while (j < thread_count) : (j += 1) {
threads[j] = try std.Thread.spawn(.{}, spamMe, .{count});
}

var timer = try std.time.Timer.start();
var i: usize = 0;
while (i < count * thread_count) : (i += 1) {
i += try waker.wait();
}

j = 0;
while (j < thread_count) : (j += 1) {
threads[j].join();
}
elapsed += timer.read();
}

Output.prettyErrorln("[EVFILT_MACHPORT] Recv {any}", .{bun.fmt.fmtDuration(elapsed)});
}
var user_waker: AsyncIO.UserFilterWaker = undefined;

fn spamMeUserFilter(count: usize) void {
Output.Source.configureNamedThread("2");
defer Output.flush();
var timer = std.time.Timer.start() catch unreachable;
var i: usize = 0;
while (i < count * thread_count) : (i += 1) {
user_waker.wake() catch unreachable;
}

Output.prettyErrorln("[EVFILT_USER] Sent {any}", .{bun.fmt.fmtDuration(timer.read())});
}
pub fn userMain(runs: usize) anyerror!void {
defer Output.flush();
user_waker = try AsyncIO.UserFilterWaker.init(bun.default_allocator);

var args = try std.process.argsAlloc(bun.default_allocator);
const count = std.fmt.parseInt(usize, args[args.len - 1], 10) catch 1024;
var remaining_runs = runs;
var elapsed: u64 = 0;

while (remaining_runs > 0) : (remaining_runs -= 1) {
var threads: [thread_count]std.Thread = undefined;
var j: usize = 0;
while (j < thread_count) : (j += 1) {
threads[j] = try std.Thread.spawn(.{}, spamMeUserFilter, .{count});
}

var timer = try std.time.Timer.start();
var i: usize = 0;
while (i < count) {
i += try user_waker.wait();
}

j = 0;
while (j < thread_count) : (j += 1) {
threads[j].join();
}
elapsed += timer.read();
}

Output.prettyErrorln("[EVFILT_USER] Recv {any}", .{bun.fmt.fmtDuration(elapsed)});
Output.flush();
}

pub fn main() anyerror!void {
var stdout_ = std.io.getStdOut();
var stderr_ = std.io.getStdErr();
var output_source = Output.Source.init(stdout_, stderr_);
Output.Source.set(&output_source);

var args = try std.process.argsAlloc(bun.default_allocator);
const count = std.fmt.parseInt(usize, args[args.len - 1], 10) catch 1024;
Output.prettyErrorln("For {d} messages and {d} threads:", .{ count, thread_count });
Output.flush();
defer Output.flush();
const runs = if (std.os.getenv("RUNS")) |run_count| try std.fmt.parseInt(usize, run_count, 10) else 1;

if (std.os.getenv("NO_MACH") == null)
try machMain(runs);

if (std.os.getenv("NO_USER") == null)
try userMain(runs);
}
17 changes: 16 additions & 1 deletion src/bun.js/event_loop.zig
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@ pub const Task = TaggedPointerUnion(.{
// TimeoutTasklet,
});

const AsyncIO = @import("io");

pub const EventLoop = struct {
ready_tasks_count: std.atomic.Atomic(u32) = std.atomic.Atomic(u32).init(0),
pending_tasks_count: std.atomic.Atomic(u32) = std.atomic.Atomic(u32).init(0),
Expand All @@ -300,6 +302,8 @@ pub const EventLoop = struct {
concurrent_lock: Lock = Lock.init(),
global: *JSGlobalObject = undefined,
virtual_machine: *VirtualMachine = undefined,
waker: ?AsyncIO.Waker = null,

pub const Queue = std.fifo.LinearFifo(Task, .Dynamic);

pub fn tickWithCount(this: *EventLoop) u32 {
Expand Down Expand Up @@ -465,14 +469,25 @@ pub const EventLoop = struct {
this.tasks.writeItem(task) catch unreachable;
}

pub fn ensureWaker(this: *EventLoop) void {
if (this.waker == null) {
this.waker = AsyncIO.Waker.init(this.virtual_machine.allocator) catch unreachable;
}
}

pub fn enqueueTaskConcurrent(this: *EventLoop, task: Task) void {
this.concurrent_lock.lock();
defer this.concurrent_lock.unlock();
this.concurrent_tasks.writeItem(task) catch unreachable;
if (this.virtual_machine.uws_event_loop) |loop| {
loop.nextTick(*EventLoop, this, EventLoop.tick);
}
_ = this.ready_tasks_count.fetchAdd(1, .Monotonic);

if (this.ready_tasks_count.fetchAdd(1, .Monotonic) == 0) {
if (this.waker) |waker| {
waker.wake() catch unreachable;
}
}
}
};

Expand Down
20 changes: 14 additions & 6 deletions src/bun_js.zig
Original file line number Diff line number Diff line change
Expand Up @@ -135,17 +135,25 @@ pub const Run = struct {
this.vm.tick();

{
var i: usize = 0;
var any = false;
while (this.vm.*.event_loop.pending_tasks_count.loadUnchecked() > 0 or this.vm.active_tasks > 0) {
this.vm.tick();
i +%= 1;

if (i > 0 and i % 100 == 0) {
std.time.sleep(std.time.ns_per_ms);
any = true;
if (this.vm.active_tasks > 0) {
if (this.vm.event_loop.ready_tasks_count.load(.Monotonic) == 0) {
_ = this.vm.global.vm().runGC(false);

if (this.vm.event_loop.ready_tasks_count.load(.Monotonic) == 0 and
this.vm.active_tasks > 0)
{
this.vm.event_loop.ensureWaker();
_ = this.vm.event_loop.waker.?.wait() catch 0;
}
}
}
}

if (i > 0) {
if (any) {
if (this.vm.log.msgs.items.len > 0) {
if (Output.enable_ansi_colors) {
this.vm.log.printForLogLevelWithEnableAnsiColors(Output.errorWriter(), true) catch {};
Expand Down
Loading

0 comments on commit c1734c6

Please sign in to comment.