Skip to content

Commit

Permalink
perf(wasm-api-dom): add FreeList zig impl
Browse files Browse the repository at this point in the history
- add generic FreeList impl & tests
- store Zig listeners in FreeList to avoid linear searches for free slots
- update addListener() & requestAnimationFrame() args to
  accept pointers only
- remove obsolete reuseOrAddSlot()
  • Loading branch information
postspectacular committed Oct 10, 2022
1 parent abd9f09 commit e2a63c2
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 30 deletions.
44 changes: 14 additions & 30 deletions packages/wasm-api-dom/include/dom.zig
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const std = @import("std");
const dom = @import("api.zig");
const FreeList = @import("free-list.zig").FreeList;

pub usingnamespace dom;

Expand All @@ -24,12 +25,12 @@ pub const DOMError = error{
InvalidID,
};

var eventListeners: std.ArrayList(?EventListener) = undefined;
var rafListeners: std.ArrayList(?RAFListener) = undefined;
var eventListeners: FreeList(*const EventListener) = undefined;
var rafListeners: FreeList(*const RAFListener) = undefined;

pub fn init(allocator: std.mem.Allocator) anyerror!void {
eventListeners = std.ArrayList(?EventListener).init(allocator);
rafListeners = std.ArrayList(?RAFListener).init(allocator);
eventListeners = FreeList(*const EventListener).init(allocator);
rafListeners = FreeList(*const RAFListener).init(allocator);
}

pub extern "dom" fn getWindowInfo(desc: *dom.WindowInfo) void;
Expand Down Expand Up @@ -79,24 +80,22 @@ pub fn setInnerText(elementID: i32, tag: []const u8) void {
}

export fn dom_callListener(listenerID: usize, event: *const dom.Event) void {
if (eventListeners.items[listenerID]) |listener| listener.callback(event, listener.arg);
if (eventListeners.get(listenerID)) |listener| listener.callback(event, listener.arg);
}

pub extern "dom" fn _addListener(elementID: i32, name: [*]const u8, listenerID: usize) void;

pub fn addListener(elementID: i32, name: []const u8, listener: EventListener) anyerror!usize {
const listenerID = try reuseOrAddSlot(EventListener, &eventListeners, listener);
pub fn addListener(elementID: i32, name: []const u8, listener: *const EventListener) anyerror!usize {
const listenerID = try eventListeners.add(listener);
_addListener(elementID, name.ptr, listenerID);
return listenerID;
}

pub extern "dom" fn _removeListener(listenerID: usize) void;

pub fn removeListener(listenerID: usize) void {
if (eventListeners.items[listenerID] != null) {
_removeListener(listenerID);
eventListeners.items[listenerID] = null;
}
eventListeners.remove(listenerID);
_removeListener(listenerID);
}

/// calls .preventDefault() on currently processed event
Expand All @@ -109,30 +108,15 @@ pub extern "dom" fn stopImmediatePropagation() void;

pub extern "dom" fn _requestAnimationFrame(listenerID: usize) void;

pub fn requestAnimationFrame(listener: RAFListener) anyerror!usize {
const id = try reuseOrAddSlot(RAFListener, &rafListeners, listener);
pub fn requestAnimationFrame(listener: *const RAFListener) anyerror!usize {
const id = try rafListeners.add(listener);
_requestAnimationFrame(id);
return id;
}

export fn dom_callRAF(listenerID: usize, time: f64) void {
if (rafListeners.items[listenerID]) |raf| {
rafListeners.items[listenerID] = null;
if (rafListeners.get(listenerID)) |raf| {
rafListeners.remove(listenerID);
raf.callback(time, raf.arg);
}
}

/// Finds first available null slot in given arraylist and writes `item` there,
/// or appends item if no free slots are available
fn reuseOrAddSlot(comptime T: type, list: *std.ArrayList(?T), item: T) anyerror!usize {
var i: usize = 0;
while (i < list.items.len) : (i += 1) {
if (list.items[i] == null) {
list.items[i] = item;
return i;
}
}
const id = list.items.len;
try list.append(item);
return id;
}
122 changes: 122 additions & 0 deletions packages/wasm-api-dom/include/free-list.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
const std = @import("std");
const builtin = @import("builtin");

/// Wraps `std.Arraylist` with a simple add()/remove()/get()/has() API and
/// adds free slot tracking & re-use (depending on value type, potentially
/// without any memory overhead). Free slots are stored as implicit singly
/// linked list within the same array as the actual values/items.
pub fn FreeList(comptime T: type) type {
const Item = union {
value: T,
id: ?usize,
};
return struct {
list: std.ArrayList(Item),
freeId: ?usize = null,

const Self = @This();

pub fn init(allocator: std.mem.Allocator) Self {
return .{
.list = std.ArrayList(Item).init(allocator),
};
}

pub fn deinit(self: *Self) void {
self.list.deinit();
self.freeId = null;
}

/// Adds given `item` to the list and returns slot ID. If possible
/// re-uses existing free slots, else attempts to allocate a new slot.
pub fn add(self: *Self, item: T) std.mem.Allocator.Error!usize {
if (self.freeId) |id| {
self.freeId = self.list.items[id].id;
self.list.items[id] = .{ .value = item };
return id;
} else {
const id = self.list.items.len;
try self.list.append(.{ .value = item });
return id;
}
}

/// Adds given `id` to the internal free slot list and removes related
/// value/item
pub fn remove(self: *Self, id: usize) void {
if (!(builtin.mode == .ReleaseFast or builtin.mode == .ReleaseSmall)) {
if (!self.has(id)) return;
}
self.list.items[id] = .{ .id = self.freeId };
self.freeId = id;
}

/// Checks if given `id` is valid (skipped in unsafe release modes) and
/// returns item value or null if ID is invalid.
pub fn get(self: *const Self, id: usize) ?T {
if (builtin.mode == .ReleaseFast or builtin.mode == .ReleaseSmall) {
return self.list.items[id].value;
}
return if (self.has(id)) self.list.items[id].value else null;
}

/// Returns true if given `id` is valid, i.e. is referring to a
/// currently used item (and isn't part of the list of free slots).
pub fn has(self: *const Self, id: usize) bool {
if (id >= self.list.items.len) return false;
var freeId = self.freeId;
while (true) {
if (freeId) |fid| {
if (fid == id) return false;
freeId = self.list.items[fid].id;
} else {
return true;
}
}
}

/// Writes all current free slot IDs to given slice & returns sub-slice
pub fn allFreeIDs(self: *const Self, ids: []usize) []usize {
var i: usize = 0;
var freeId = self.freeId;
while (true) {
if (freeId) |fid| {
ids[i] = fid;
i += 1;
freeId = self.list.items[fid].id;
} else {
return ids[0..i];
}
}
}
};
}

test "FreeList" {
var x: f64 = 123;
var foo = FreeList(f64).init(std.testing.allocator);
defer foo.deinit();
try std.testing.expect(try foo.add(x) == 0);
try std.testing.expect(try foo.add(x) == 1);

try std.testing.expect(foo.has(0));
try std.testing.expect(foo.has(1));

foo.remove(0);
try std.testing.expect(!foo.has(0));
try std.testing.expect(!foo.has(2));

try std.testing.expect(try foo.add(x) == 0);
try std.testing.expect(try foo.add(x) == 2);
try std.testing.expect(foo.has(2));

foo.remove(1);
foo.remove(2);
try std.testing.expect(try foo.add(x) == 2);

foo.remove(0);
try std.testing.expect(try foo.add(x) == 0);
try std.testing.expect(try foo.add(x) == 1);

try std.testing.expect(foo.freeId == null);
}

0 comments on commit e2a63c2

Please sign in to comment.