mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-15 15:58:57 +00:00
Higher performance.now() precision (closer to FFs behavior)
Much better v8 object debugging/printing in debug mode Window.requestIdleCallback and cancelIdleCallback Don't prematurely close stream on empty read - queue promises.
This commit is contained in:
@@ -475,23 +475,15 @@ fn logFunctionCallError(self: *Caller, type_name: []const u8, func: []const u8,
|
||||
}
|
||||
|
||||
fn serializeFunctionArgs(self: *Caller, info: v8.FunctionCallbackInfo) ![]const u8 {
|
||||
const separator = log.separator();
|
||||
const js_parameter_count = info.length();
|
||||
|
||||
const context = self.context;
|
||||
var arr: std.ArrayListUnmanaged(u8) = .{};
|
||||
for (0..js_parameter_count) |i| {
|
||||
const js_value = info.getArg(@intCast(i));
|
||||
const value_string = try context.valueToDetailString(js_value);
|
||||
const value_type = try context.jsStringToZig(try js_value.typeOf(self.isolate), .{});
|
||||
try std.fmt.format(arr.writer(context.call_arena), "{s}{d}: {s} ({s})", .{
|
||||
separator,
|
||||
i + 1,
|
||||
value_string,
|
||||
value_type,
|
||||
});
|
||||
var buf = std.Io.Writer.Allocating.init(context.call_arena);
|
||||
|
||||
const separator = log.separator();
|
||||
for (0..info.length()) |i| {
|
||||
try buf.writer.print("{s}{d} - ", .{ separator, i + 1 });
|
||||
try context.debugValue(info.getArg(@intCast(i)), &buf.writer);
|
||||
}
|
||||
return arr.items;
|
||||
return buf.written();
|
||||
}
|
||||
|
||||
// Takes a function, and returns a tuple for its argument. Used when we
|
||||
|
||||
@@ -1062,67 +1062,84 @@ pub fn jsStringToZigZ(self: *const Context, str: v8.String, opts: JsStringToZigO
|
||||
return buf;
|
||||
}
|
||||
|
||||
pub fn valueToDetailString(self: *const Context, value: v8.Value) ![]u8 {
|
||||
var str: ?v8.String = null;
|
||||
const v8_context = self.v8_context;
|
||||
|
||||
if (value.isObject() and !value.isFunction()) blk: {
|
||||
str = v8.Json.stringify(v8_context, value, null) catch break :blk;
|
||||
|
||||
if (str.?.lenUtf8(self.isolate) == 2) {
|
||||
// {} isn't useful, null this so that we can get the toDetailString
|
||||
// (which might also be useless, but maybe not)
|
||||
str = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (str == null) {
|
||||
str = try value.toDetailString(v8_context);
|
||||
}
|
||||
|
||||
const s = try self.jsStringToZig(str.?, .{});
|
||||
if (comptime builtin.mode == .Debug) {
|
||||
if (std.mem.eql(u8, s, "[object Object]")) {
|
||||
if (self.debugValueToString(value.castTo(v8.Object))) |ds| {
|
||||
return ds;
|
||||
} else |err| {
|
||||
log.err(.js, "debug serialize value", .{ .err = err });
|
||||
}
|
||||
}
|
||||
}
|
||||
return s;
|
||||
pub fn debugValue(self: *const Context, js_val: v8.Value, writer: *std.Io.Writer) !void {
|
||||
var seen: std.AutoHashMapUnmanaged(u32, void) = .empty;
|
||||
return _debugValue(self, js_val, &seen, 0, writer) catch error.WriteFailed;
|
||||
}
|
||||
|
||||
fn debugValueToString(self: *const Context, js_obj: v8.Object) ![]u8 {
|
||||
if (comptime builtin.mode != .Debug) {
|
||||
@compileError("debugValue can only be called in debug mode");
|
||||
fn _debugValue(self: *const Context, js_val: v8.Value, seen: *std.AutoHashMapUnmanaged(u32, void), depth: usize, writer: *std.Io.Writer) !void {
|
||||
if (js_val.isNull()) {
|
||||
// I think null can sometimes appear as an object, so check this and
|
||||
// handle it first.
|
||||
return writer.writeAll("null");
|
||||
}
|
||||
const v8_context = self.v8_context;
|
||||
|
||||
if (!js_val.isObject()) {
|
||||
// handle these explicitly, so we don't include the type (we only want to include
|
||||
// it when there's some ambiguity, e.g. the string "true")
|
||||
if (js_val.isUndefined()) {
|
||||
return writer.writeAll("undefined");
|
||||
}
|
||||
if (js_val.isTrue()) {
|
||||
return writer.writeAll("true");
|
||||
}
|
||||
if (js_val.isFalse()) {
|
||||
return writer.writeAll("false");
|
||||
}
|
||||
// TODO: KARL wait for v8 build to work again, this works with
|
||||
// the latest version of zig-v8-fork, I just can't build it right now
|
||||
// APPLY THIS change to valueToString and valueToStringz
|
||||
// if (js_val.isSymbol()) {
|
||||
// const js_sym = v8.Symbol{.handle = js_val.handle};
|
||||
// const js_sym_desc = js_sym.getDescription(self.isolate);
|
||||
// const js_sym_str = try self.valueToString(js_sym_desc, .{});
|
||||
// return writer.print("{s} (symbol)", .{js_sym_str});
|
||||
// }
|
||||
const js_type = try self.jsStringToZig(try js_val.typeOf(self.isolate), .{});
|
||||
const js_val_str = try self.valueToString(js_val, .{});
|
||||
if (js_val_str.len > 2000) {
|
||||
try writer.writeAll(js_val_str[0..2000]);
|
||||
try writer.writeAll(" ... (truncated)");
|
||||
} else {
|
||||
try writer.writeAll(js_val_str);
|
||||
}
|
||||
return writer.print(" ({s})", .{js_type});
|
||||
}
|
||||
|
||||
const js_obj = js_val.castTo(v8.Object);
|
||||
{
|
||||
// explicit scope because gop will become invalid in recursive call
|
||||
const gop = try seen.getOrPut(self.call_arena, js_obj.getIdentityHash());
|
||||
if (gop.found_existing) {
|
||||
return writer.writeAll("<circular>\n");
|
||||
}
|
||||
gop.value_ptr.* = {};
|
||||
}
|
||||
|
||||
const v8_context = self.v8_context;
|
||||
const names_arr = js_obj.getOwnPropertyNames(v8_context);
|
||||
const names_obj = names_arr.castTo(v8.Object);
|
||||
const len = names_arr.length();
|
||||
|
||||
var arr: std.ArrayListUnmanaged(u8) = .empty;
|
||||
var writer = arr.writer(self.call_arena);
|
||||
try writer.writeAll("(JSON.stringify failed, dumping top-level fields)\n");
|
||||
if (depth > 20) {
|
||||
return writer.writeAll("...deeply nested object...");
|
||||
}
|
||||
|
||||
try writer.print("({d}/{d})", .{ js_obj.getOwnPropertyNames(v8_context).length(), js_obj.getPropertyNames(v8_context).length() });
|
||||
for (0..len) |i| {
|
||||
if (i == 0) {
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
const field_name = try names_obj.getAtIndex(v8_context, @intCast(i));
|
||||
const field_value = try js_obj.getValue(v8_context, field_name);
|
||||
const name = try self.valueToString(field_name, .{});
|
||||
const value = try self.valueToString(field_value, .{});
|
||||
try writer.splatByteAll(' ', depth);
|
||||
try writer.writeAll(name);
|
||||
try writer.writeAll(": ");
|
||||
if (std.mem.indexOfAny(u8, value, &std.ascii.whitespace) == null) {
|
||||
try writer.writeAll(value);
|
||||
} else {
|
||||
try writer.writeByte('"');
|
||||
try writer.writeAll(value);
|
||||
try writer.writeByte('"');
|
||||
try self._debugValue(try js_obj.getValue(v8_context, field_name), seen, depth + 1, writer);
|
||||
if (i != len - 1) {
|
||||
try writer.writeByte('\n');
|
||||
}
|
||||
try writer.writeByte(' ');
|
||||
}
|
||||
return arr.items;
|
||||
}
|
||||
|
||||
pub fn stackTrace(self: *const Context) !?[]const u8 {
|
||||
|
||||
@@ -20,6 +20,8 @@ const std = @import("std");
|
||||
const js = @import("js.zig");
|
||||
const v8 = js.v8;
|
||||
|
||||
const IS_DEBUG = @import("builtin").mode == .Debug;
|
||||
|
||||
const Caller = @import("Caller.zig");
|
||||
const Context = @import("Context.zig");
|
||||
const PersistentObject = v8.Persistent(v8.Object);
|
||||
@@ -74,12 +76,10 @@ pub fn toString(self: Object) ![]const u8 {
|
||||
return self.context.valueToString(js_value, .{});
|
||||
}
|
||||
|
||||
pub fn toDetailString(self: Object) ![]const u8 {
|
||||
const js_value = self.js_obj.toValue();
|
||||
return self.context.valueToDetailString(js_value);
|
||||
}
|
||||
|
||||
pub fn format(self: Object, writer: *std.Io.Writer) !void {
|
||||
if (comptime IS_DEBUG) {
|
||||
return self.context.debugValue(self.js_obj.toValue(), writer);
|
||||
}
|
||||
const str = self.toString() catch return error.WriteFailed;
|
||||
return writer.writeAll(str);
|
||||
}
|
||||
|
||||
@@ -71,7 +71,12 @@ pub const PromiseResolver = struct {
|
||||
return self.resolver.getPromise();
|
||||
}
|
||||
|
||||
pub fn resolve(self: PromiseResolver, value: anytype) !void {
|
||||
pub fn resolve(self: PromiseResolver, comptime source: []const u8, value: anytype) void {
|
||||
self._resolve(value) catch |err| {
|
||||
log.err(.bug, "resolve", .{ .source = source, .err = err, .persistent = false });
|
||||
};
|
||||
}
|
||||
fn _resolve(self: PromiseResolver, value: anytype) !void {
|
||||
const context = self.context;
|
||||
const js_value = try context.zigValueToJs(value);
|
||||
|
||||
@@ -81,7 +86,12 @@ pub const PromiseResolver = struct {
|
||||
self.runMicrotasks();
|
||||
}
|
||||
|
||||
pub fn reject(self: PromiseResolver, value: anytype) !void {
|
||||
pub fn reject(self: PromiseResolver, comptime source: []const u8, value: anytype) void {
|
||||
self._reject(value) catch |err| {
|
||||
log.err(.bug, "reject", .{ .source = source, .err = err, .persistent = false });
|
||||
};
|
||||
}
|
||||
fn _reject(self: PromiseResolver, value: anytype) !void {
|
||||
const context = self.context;
|
||||
const js_value = try context.zigValueToJs(value);
|
||||
|
||||
@@ -104,7 +114,12 @@ pub const PersistentPromiseResolver = struct {
|
||||
return self.resolver.castToPromiseResolver().getPromise();
|
||||
}
|
||||
|
||||
pub fn resolve(self: PersistentPromiseResolver, value: anytype) !void {
|
||||
pub fn resolve(self: PersistentPromiseResolver, comptime source: []const u8, value: anytype) void {
|
||||
self._resolve(value) catch |err| {
|
||||
log.err(.bug, "resolve", .{ .source = source, .err = err, .persistent = true });
|
||||
};
|
||||
}
|
||||
fn _resolve(self: PersistentPromiseResolver, value: anytype) !void {
|
||||
const context = self.context;
|
||||
const js_value = try context.zigValueToJs(value, .{});
|
||||
defer context.runMicrotasks();
|
||||
@@ -114,7 +129,13 @@ pub const PersistentPromiseResolver = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reject(self: PersistentPromiseResolver, value: anytype) !void {
|
||||
pub fn reject(self: PersistentPromiseResolver, comptime source: []const u8, value: anytype) void {
|
||||
self._reject(value) catch |err| {
|
||||
log.err(.bug, "reject", .{ .source = source, .err = err, .persistent = true });
|
||||
};
|
||||
}
|
||||
|
||||
fn _reject(self: PersistentPromiseResolver, value: anytype) !void {
|
||||
const context = self.context;
|
||||
const js_value = try context.zigValueToJs(value, .{});
|
||||
defer context.runMicrotasks();
|
||||
|
||||
@@ -21,18 +21,6 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=readable_stream_read_empty>
|
||||
(async function() {
|
||||
const stream = new ReadableStream();
|
||||
const reader = stream.getReader();
|
||||
|
||||
// Reading from empty stream should return done:true
|
||||
const result = await reader.read();
|
||||
testing.expectEqual('object', typeof result);
|
||||
testing.expectEqual(true, result.done);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script id=response_body>
|
||||
(async function() {
|
||||
const response = new Response('hello world');
|
||||
|
||||
@@ -106,7 +106,7 @@ pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Fu
|
||||
}
|
||||
|
||||
if (self._when_defined.fetchRemove(name)) |entry| {
|
||||
try entry.value.resolve(constructor);
|
||||
entry.value.resolve("whenDefined", constructor);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,21 +13,33 @@ const Performance = @This();
|
||||
_time_origin: u64,
|
||||
_entries: std.ArrayListUnmanaged(*Entry) = .{},
|
||||
|
||||
/// Get high-resolution timestamp in microseconds, rounded to 5μs increments
|
||||
/// to match browser behavior (prevents fingerprinting)
|
||||
fn highResTimestamp() u64 {
|
||||
const ts = datetime.timespec();
|
||||
const micros = @as(u64, @intCast(ts.sec)) * 1_000_000 + @as(u64, @intCast(@divTrunc(ts.nsec, 1_000)));
|
||||
// Round to nearest 5 microseconds (like Firefox default)
|
||||
const rounded = @divTrunc(micros + 2, 5) * 5;
|
||||
return rounded;
|
||||
}
|
||||
|
||||
pub fn init() Performance {
|
||||
return .{
|
||||
._time_origin = datetime.milliTimestamp(.monotonic),
|
||||
._time_origin = highResTimestamp(),
|
||||
._entries = .{},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn now(self: *const Performance) f64 {
|
||||
const current = datetime.milliTimestamp(.monotonic);
|
||||
const current = highResTimestamp();
|
||||
const elapsed = current - self._time_origin;
|
||||
return @floatFromInt(elapsed);
|
||||
// Return as milliseconds with microsecond precision
|
||||
return @as(f64, @floatFromInt(elapsed)) / 1000.0;
|
||||
}
|
||||
|
||||
pub fn getTimeOrigin(self: *const Performance) f64 {
|
||||
return @floatFromInt(self._time_origin);
|
||||
// Return as milliseconds
|
||||
return @as(f64, @floatFromInt(self._time_origin)) / 1000.0;
|
||||
}
|
||||
|
||||
pub fn mark(self: *Performance, name: []const u8, _options: ?Mark.Options, page: *Page) !*Mark {
|
||||
|
||||
@@ -222,6 +222,24 @@ pub fn cancelAnimationFrame(self: *Window, id: u32) void {
|
||||
sc.removed = true;
|
||||
}
|
||||
|
||||
const RequestIdleCallbackOpts = struct {
|
||||
timeout: ?u32 = null,
|
||||
};
|
||||
pub fn requestIdleCallback(self: *Window, cb: js.Function, opts_: ?RequestIdleCallbackOpts, page: *Page) !u32 {
|
||||
const opts = opts_ orelse RequestIdleCallbackOpts{};
|
||||
return self.scheduleCallback(cb, opts.timeout orelse 50, .{
|
||||
.repeat = false,
|
||||
.params = &.{},
|
||||
.low_priority = true,
|
||||
.name = "window.requestIdleCallback",
|
||||
}, page);
|
||||
}
|
||||
|
||||
pub fn cancelIdleCallback(self: *Window, id: u32) void {
|
||||
var sc = self._timers.get(id) orelse return;
|
||||
sc.removed = true;
|
||||
}
|
||||
|
||||
pub fn reportError(self: *Window, err: js.Object, page: *Page) !void {
|
||||
const error_event = try ErrorEvent.init("error", .{
|
||||
.@"error" = err,
|
||||
@@ -485,6 +503,8 @@ pub const JsApi = struct {
|
||||
pub const clearImmediate = bridge.function(Window.clearImmediate, .{});
|
||||
pub const requestAnimationFrame = bridge.function(Window.requestAnimationFrame, .{});
|
||||
pub const cancelAnimationFrame = bridge.function(Window.cancelAnimationFrame, .{});
|
||||
pub const requestIdleCallback = bridge.function(Window.requestIdleCallback, .{});
|
||||
pub const cancelIdleCallback = bridge.function(Window.cancelIdleCallback, .{});
|
||||
pub const matchMedia = bridge.function(Window.matchMedia, .{});
|
||||
pub const postMessage = bridge.function(Window.postMessage, .{});
|
||||
pub const btoa = bridge.function(Window.btoa, .{});
|
||||
|
||||
@@ -112,12 +112,10 @@ fn httpDataCallback(transfer: *Http.Transfer, data: []const u8) !void {
|
||||
fn httpDoneCallback(ctx: *anyopaque) !void {
|
||||
const self: *Fetch = @ptrCast(@alignCast(ctx));
|
||||
self._response._body = self._buf.items;
|
||||
return self._resolver.resolve(self._response);
|
||||
return self._resolver.resolve("fetch done", self._response);
|
||||
}
|
||||
|
||||
fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void {
|
||||
const self: *Fetch = @ptrCast(@alignCast(ctx));
|
||||
self._resolver.reject(@errorName(err)) catch |inner| {
|
||||
log.err(.bug, "failed to reject", .{ .source = "fetch", .err = inner, .reject = err });
|
||||
};
|
||||
self._resolver.reject("fetch error", @errorName(err));
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ const js = @import("../../js/js.zig");
|
||||
|
||||
const Page = @import("../../Page.zig");
|
||||
const ReadableStream = @import("ReadableStream.zig");
|
||||
const ReadableStreamDefaultReader = @import("ReadableStreamDefaultReader.zig");
|
||||
|
||||
const ReadableStreamDefaultController = @This();
|
||||
|
||||
@@ -28,24 +29,42 @@ _page: *Page,
|
||||
_stream: *ReadableStream,
|
||||
_arena: std.mem.Allocator,
|
||||
_queue: std.ArrayList([]const u8),
|
||||
_pending_reads: std.ArrayList(js.PersistentPromiseResolver),
|
||||
|
||||
pub fn init(stream: *ReadableStream, page: *Page) !*ReadableStreamDefaultController {
|
||||
return page._factory.create(ReadableStreamDefaultController{
|
||||
._page = page,
|
||||
._queue = .empty,
|
||||
._stream = stream,
|
||||
._arena = page.arena,
|
||||
._queue = std.ArrayList([]const u8){},
|
||||
._pending_reads = .empty,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn addPendingRead(self: *ReadableStreamDefaultController, page: *Page) !js.Promise {
|
||||
const resolver = try page.js.createPromiseResolver(.page);
|
||||
try self._pending_reads.append(self._arena, resolver);
|
||||
return resolver.promise();
|
||||
}
|
||||
|
||||
pub fn enqueue(self: *ReadableStreamDefaultController, chunk: []const u8) !void {
|
||||
if (self._stream._state != .readable) {
|
||||
return error.StreamNotReadable;
|
||||
}
|
||||
|
||||
// Store a copy of the chunk in the page arena
|
||||
const chunk_copy = try self._page.arena.dupe(u8, chunk);
|
||||
try self._queue.append(self._arena, chunk_copy);
|
||||
if (self._pending_reads.items.len == 0) {
|
||||
const chunk_copy = try self._page.arena.dupe(u8, chunk);
|
||||
return self._queue.append(self._arena, chunk_copy);
|
||||
}
|
||||
|
||||
// I know, this is ouch! But we expect to have very few (if any)
|
||||
// pending reads.
|
||||
const resolver = self._pending_reads.orderedRemove(0);
|
||||
const result = ReadableStreamDefaultReader.ReadResult{
|
||||
.done = false,
|
||||
.value = .{ .values = chunk },
|
||||
};
|
||||
resolver.resolve("stream enqueue", result);
|
||||
}
|
||||
|
||||
pub fn close(self: *ReadableStreamDefaultController) !void {
|
||||
@@ -54,6 +73,16 @@ pub fn close(self: *ReadableStreamDefaultController) !void {
|
||||
}
|
||||
|
||||
self._stream._state = .closed;
|
||||
|
||||
// Resolve all pending reads with done=true
|
||||
const result = ReadableStreamDefaultReader.ReadResult{
|
||||
.done = true,
|
||||
.value = null,
|
||||
};
|
||||
for (self._pending_reads.items) |resolver| {
|
||||
resolver.resolve("stream close", result);
|
||||
}
|
||||
self._pending_reads.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
pub fn doError(self: *ReadableStreamDefaultController, err: []const u8) !void {
|
||||
@@ -63,6 +92,12 @@ pub fn doError(self: *ReadableStreamDefaultController, err: []const u8) !void {
|
||||
|
||||
self._stream._state = .errored;
|
||||
self._stream._stored_error = try self._page.arena.dupe(u8, err);
|
||||
|
||||
// Reject all pending reads
|
||||
for (self._pending_reads.items) |resolver| {
|
||||
resolver.reject("stream errror", err);
|
||||
}
|
||||
self._pending_reads.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
pub fn dequeue(self: *ReadableStreamDefaultController) ?[]const u8 {
|
||||
|
||||
@@ -59,17 +59,14 @@ pub fn read(self: *ReadableStreamDefaultReader, page: *Page) !js.Promise {
|
||||
|
||||
if (stream._state == .closed) {
|
||||
const result = ReadResult{
|
||||
.value = null,
|
||||
.done = true,
|
||||
.value = null,
|
||||
};
|
||||
return page.js.resolvePromise(result);
|
||||
}
|
||||
|
||||
const result = ReadResult{
|
||||
.done = true,
|
||||
.value = null,
|
||||
};
|
||||
return page.js.resolvePromise(result);
|
||||
// No data, but not closed. We need to queue the read for any future data
|
||||
return stream._controller.addPendingRead(page);
|
||||
}
|
||||
|
||||
pub fn releaseLock(self: *ReadableStreamDefaultReader) void {
|
||||
|
||||
@@ -548,7 +548,7 @@ pub fn milliTimestamp(comptime mode: TimestampMode) u64 {
|
||||
return @as(u64, @intCast(ts.sec)) * 1000 + @as(u64, @intCast(@divTrunc(ts.nsec, 1_000_000)));
|
||||
}
|
||||
|
||||
fn timespec() posix.timespec {
|
||||
pub fn timespec() posix.timespec {
|
||||
if (comptime is_posix == false) {
|
||||
@compileError("`timespec` should not be called when `is_posix` is false");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user