mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-08-03 03:13:05 -04:00
mem: improve ArenaPool usage
1 - Add a metric to track the number of inlight arenas from the pool
2 - Script now use 2 arenas:
- An initial (small) one for the script
- A sized one for the body
Should result in less pressure on our limited .large arenas
3 - DOMPoint and DOMPointRO are now arena free (they live on the slab only)
4 - TextDecoder no longer accumulate garbage in its arena
5 - Response object is much better at picking its arena size, rather than just
using a .large
This commit is contained in:
@@ -34,6 +34,7 @@ const SAFETY = Arena.SAFETY;
|
||||
pub const BucketSize = enum { tiny, small, medium, large };
|
||||
|
||||
pub const Bucket = struct {
|
||||
size: BucketSize,
|
||||
free_list: ?*Arena = null,
|
||||
free_list_len: u16 = 0,
|
||||
free_list_max: u16,
|
||||
@@ -66,10 +67,10 @@ pub fn init(allocator: Allocator, config: Config) ArenaPool {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.entry_pool = .empty,
|
||||
.tiny = .{ .free_list_max = config.tiny.max, .retain_bytes = config.tiny.retain },
|
||||
.small = .{ .free_list_max = config.small.max, .retain_bytes = config.small.retain },
|
||||
.medium = .{ .free_list_max = config.medium.max, .retain_bytes = config.medium.retain },
|
||||
.large = .{ .free_list_max = config.large.max, .retain_bytes = config.large.retain },
|
||||
.tiny = .{ .size = .tiny, .free_list_max = config.tiny.max, .retain_bytes = config.tiny.retain },
|
||||
.small = .{ .size = .small, .free_list_max = config.small.max, .retain_bytes = config.small.retain },
|
||||
.medium = .{ .size = .medium, .free_list_max = config.medium.max, .retain_bytes = config.medium.retain },
|
||||
.large = .{ .size = .large, .free_list_max = config.large.max, .retain_bytes = config.large.retain },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -100,6 +101,13 @@ pub fn deinit(self: *ArenaPool) void {
|
||||
self.entry_pool.deinit(self.allocator);
|
||||
}
|
||||
|
||||
pub fn bucketFor(self: *const ArenaPool, size: usize) BucketSize {
|
||||
if (size <= self.tiny.retain_bytes) return .tiny;
|
||||
if (size <= self.small.retain_bytes) return .small;
|
||||
if (size <= self.medium.retain_bytes) return .medium;
|
||||
return .large;
|
||||
}
|
||||
|
||||
// Acquire an arena from the pool.
|
||||
// - Pass a BucketSize (.tiny, .small, .medium, .large) for explicit bucket selection
|
||||
// - Pass a usize for automatic bucket selection based on expected size
|
||||
@@ -120,10 +128,7 @@ fn _acquire(self: *ArenaPool, account: ?*Arena.Account, size_or_bucket: anytype,
|
||||
break :blk @as(BucketSize, size_or_bucket);
|
||||
}
|
||||
if (T == usize or T == comptime_int) {
|
||||
if (size_or_bucket <= self.tiny.retain_bytes) break :blk .tiny;
|
||||
if (size_or_bucket <= self.small.retain_bytes) break :blk .small;
|
||||
if (size_or_bucket <= self.medium.retain_bytes) break :blk .medium;
|
||||
break :blk .large;
|
||||
break :blk self.bucketFor(size_or_bucket);
|
||||
}
|
||||
@compileError("acquire expects BucketSize or usize, got " ++ @typeName(T));
|
||||
};
|
||||
@@ -135,6 +140,8 @@ fn _acquire(self: *ArenaPool, account: ?*Arena.Account, size_or_bucket: anytype,
|
||||
.large => &self.large,
|
||||
};
|
||||
|
||||
lp.metrics.arena_inflight.incr(bucket_size);
|
||||
|
||||
self.mutex.lockUncancelable(lp.io);
|
||||
defer self.mutex.unlock(lp.io);
|
||||
|
||||
@@ -186,6 +193,8 @@ pub fn release(self: *ArenaPool, entry: *Arena) void {
|
||||
const arena = &entry._arena;
|
||||
const bucket = entry.bucket;
|
||||
|
||||
lp.metrics.arena_inflight.decr(bucket.size);
|
||||
|
||||
if (IS_DEBUG) {
|
||||
self.mutex.lockUncancelable(lp.io);
|
||||
defer self.mutex.unlock(lp.io);
|
||||
|
||||
@@ -31,6 +31,7 @@ script_errors: Counter = .{},
|
||||
js_errors: CounterEnum("kind", enum { js_exception, other }) = .{},
|
||||
arena_hit: CounterEnum("size", @import("ArenaPool.zig").BucketSize) = .{},
|
||||
arena_miss: CounterEnum("size", @import("ArenaPool.zig").BucketSize) = .{},
|
||||
arena_inflight: GaugeEnum("size", @import("ArenaPool.zig").BucketSize) = .{},
|
||||
arena_memory_bytes: Gauge = .{},
|
||||
navigate: CounterEnum("type", @import("telemetry/telemetry.zig").Event.Navigate.Context) = .{},
|
||||
js_heap_size_bytes: Histogram(&.{
|
||||
@@ -87,6 +88,7 @@ const help = .{
|
||||
.js_errors = "Uncaught JS errors (script exceptions, listener/callback throws, unhandled promise rejections); kind=js_exception is a thrown JS value, other is an internal failure (e.g. compilation error, terminated execution)",
|
||||
.arena_hit = "Arena pool acquisitions served from the free list",
|
||||
.arena_miss = "Arena pool acquisitions that had to allocate a new arena",
|
||||
.arena_inflight = "Arenas currently checked out of the pool. Above the bucket's max, every acquisition is a miss and every release is discarded",
|
||||
.arena_memory_bytes = "Backing memory held by pooled arenas, including capacity retained on the free list",
|
||||
.navigate = "Navigations by initiating frame type",
|
||||
.js_heap_size_bytes = "V8 heap physical size, sampled when a page is closed",
|
||||
@@ -164,6 +166,33 @@ const Gauge = struct {
|
||||
}
|
||||
};
|
||||
|
||||
fn GaugeEnum(comptime label: []const u8, comptime T: type) type {
|
||||
return struct {
|
||||
values: std.enums.EnumArray(T, Gauge) = .initFill(.{}),
|
||||
|
||||
pub const Tag = T;
|
||||
pub const label_name = label;
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn incr(self: *Self, tag: T) void {
|
||||
self.values.getPtr(tag).incr();
|
||||
}
|
||||
|
||||
pub fn decr(self: *Self, tag: T) void {
|
||||
self.values.getPtr(tag).decr();
|
||||
}
|
||||
|
||||
fn write(self: *const Self, comptime name: []const u8, comptime help_text: []const u8, writer: *std.Io.Writer) !void {
|
||||
try writer.writeAll("# HELP " ++ name ++ " " ++ help_text ++ "\n" ++ "# TYPE " ++ name ++ " gauge\n");
|
||||
inline for (comptime std.enums.values(Tag)) |tag| {
|
||||
const value = @atomicLoad(isize, &self.values.getPtrConst(tag).value, .monotonic);
|
||||
try writer.print(name ++ "{{" ++ label ++ "=\"" ++ @tagName(tag) ++ "\"}} {d}\n", .{value});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn CounterEnum(comptime label: []const u8, comptime T: type) type {
|
||||
return struct {
|
||||
counts: std.enums.EnumArray(T, Counter) = .initFill(.{}),
|
||||
|
||||
@@ -526,12 +526,10 @@ pub fn destroy(self: *Factory, value: anytype) void {
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
// We should always destroy from the leaf down.
|
||||
if (@hasDecl(S, "_prototype_root")) {
|
||||
// A Event{._type == .generic} (or any other similar types)
|
||||
// _should_ be destroyed directly. The _type = .generic is a pseudo
|
||||
// child
|
||||
if (S != Event or value._type != .generic) {
|
||||
log.fatal(.bug, "factory.destroy.event", .{ .type = @typeName(S) });
|
||||
if (comptime @hasDecl(S, "_prototype_root")) {
|
||||
const is_leaf = if (comptime @hasField(S, "_type")) value._type == .generic else false;
|
||||
if (!is_leaf) {
|
||||
log.fatal(.bug, "factory.destroy.root", .{ .type = @typeName(S) });
|
||||
unreachable;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2180,7 +2180,8 @@ pub fn loadExternalStylesheet(self: *Frame, link: *Element.Html.Link, href: []co
|
||||
}
|
||||
const element = link.asElement();
|
||||
|
||||
const arena = try session.getArena(.medium, "Frame.loadExternalStylesheet");
|
||||
// HttpClient will take out a larger arena for the body, if necessary
|
||||
const arena = try session.getArena(.small, "Frame.loadExternalStylesheet");
|
||||
defer arena.release();
|
||||
|
||||
const resolved = URL.resolve(arena.allocator(), self.base(), href, .{ .encoding = self.charset }) catch |err| {
|
||||
@@ -2212,7 +2213,7 @@ pub fn loadExternalStylesheet(self: *Frame, link: *Element.Html.Link, href: []co
|
||||
sm.is_evaluating = true;
|
||||
defer sm.endEvaluationWindow(was_evaluating);
|
||||
|
||||
var response = http_client.syncRequest(arena.allocator(), .{
|
||||
var response = http_client.syncRequest(.{
|
||||
.url = resolved,
|
||||
.method = .GET,
|
||||
.frame_id = self._frame_id,
|
||||
@@ -2227,7 +2228,7 @@ pub fn loadExternalStylesheet(self: *Frame, link: *Element.Html.Link, href: []co
|
||||
log.warn(.http, "external stylesheet fetch", .{ .err = err, .url = resolved });
|
||||
return self.fireElementEvent(element, comptime .wrap("error"));
|
||||
};
|
||||
defer response.deinit(arena.allocator());
|
||||
defer response.deinit();
|
||||
|
||||
if (response.status < 200 or response.status >= 300) {
|
||||
log.info(.http, "external stylesheet status", .{ .status = response.status, .url = resolved });
|
||||
|
||||
@@ -112,7 +112,7 @@ pub fn preloadScript(self: *ScriptManager, element: ?*Element.Html, url: []const
|
||||
}
|
||||
|
||||
const frame = self.frame;
|
||||
const arena = try frame.getArena(.large, "SM.preloadScript");
|
||||
const arena = try frame.getArena(.small, "SM.preloadScript");
|
||||
errdefer arena.release();
|
||||
|
||||
const owned_url = try arena.dupeZ(u8, url);
|
||||
@@ -240,7 +240,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
||||
// released early on the adoption path — so the errdefer can't double-free.
|
||||
var handover = false;
|
||||
|
||||
const arena = try frame.getArena(.large, "SM.addFromElement");
|
||||
const arena = try frame.getArena(.small, "SM.addFromElement");
|
||||
errdefer if (handover == false) {
|
||||
arena.release();
|
||||
};
|
||||
@@ -354,7 +354,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
||||
script.status = pre.status;
|
||||
script.complete = true;
|
||||
} else {
|
||||
const response = try self.base.client.syncRequest(arena.allocator(), .{
|
||||
const response = try self.base.client.syncRequest(.{
|
||||
.url = remote_url,
|
||||
.method = .GET,
|
||||
.frame_id = frame._frame_id,
|
||||
@@ -367,6 +367,9 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
||||
.shutdown_callback = HttpClient.noopShutdown, // syncRequest installs its own
|
||||
}, &frame._http_owner);
|
||||
|
||||
// Take the body's arena rather than releasing it: `source`
|
||||
// has to outlive this call, up to script.deinit().
|
||||
script.source_arena = response.arena;
|
||||
script.source = .{ .remote = response.body };
|
||||
script.status = response.status;
|
||||
script.complete = true;
|
||||
|
||||
@@ -234,7 +234,7 @@ pub fn preloadImport(self: *ScriptManagerBase, url: [:0]const u8, referrer: []co
|
||||
}
|
||||
errdefer _ = self.imported_modules.remove(url);
|
||||
|
||||
const arena = try self.acquireArena(.large, "SM.preloadImport");
|
||||
const arena = try self.acquireArena(.small, "SM.preloadImport");
|
||||
errdefer arena.release();
|
||||
|
||||
const script = try arena.create(Script);
|
||||
@@ -426,7 +426,7 @@ pub fn getAsyncImport(self: *ScriptManagerBase, url: [:0]const u8, cb: ImportAsy
|
||||
}
|
||||
}
|
||||
|
||||
const arena = try self.acquireArena(.large, "SM.getAsyncImport");
|
||||
const arena = try self.acquireArena(.small, "SM.getAsyncImport");
|
||||
errdefer arena.release();
|
||||
|
||||
const script = try arena.create(Script);
|
||||
@@ -611,6 +611,13 @@ pub const Script = struct {
|
||||
source: Source,
|
||||
url: []const u8,
|
||||
arena: *lp.Arena,
|
||||
|
||||
// Where `source` lives, when it isn't `arena`. The double-arena lets us
|
||||
// use a .small arena for the Script itself, and then a properly sized one
|
||||
// for the body, when we know its size. This avoids eager-usage of our
|
||||
// limited .large arena pool
|
||||
source_arena: ?*lp.Arena = null,
|
||||
|
||||
extra: Extra,
|
||||
node: std.DoublyLinkedList.Node,
|
||||
manager: *ScriptManagerBase,
|
||||
@@ -687,9 +694,18 @@ pub const Script = struct {
|
||||
};
|
||||
|
||||
pub fn deinit(self: *Script) void {
|
||||
if (self.source_arena) |source_arena| {
|
||||
source_arena.release();
|
||||
}
|
||||
self.arena.release();
|
||||
}
|
||||
|
||||
// The allocator `source` grows from. Falls back to the control arena when
|
||||
// no header callback ran to size a dedicated one.
|
||||
fn sourceAllocator(self: *Script) Allocator {
|
||||
return (self.source_arena orelse self.arena).allocator();
|
||||
}
|
||||
|
||||
pub fn startCallback(transfer: *HttpClient.Transfer) !void {
|
||||
log.debug(.http, "script fetch start", .{ .req = transfer });
|
||||
}
|
||||
@@ -748,9 +764,19 @@ pub const Script = struct {
|
||||
}
|
||||
|
||||
lp.assert(self.source.remote.capacity == 0, "ScriptManagerBase.Header buffer", .{ .capacity = self.source.remote.capacity });
|
||||
|
||||
const content_length = transfer.getContentLength();
|
||||
if (self.source_arena == null) {
|
||||
// A redirect re-runs this callback; keep the arena we already have.
|
||||
self.source_arena = if (content_length) |cl|
|
||||
try self.manager.acquireArena(cl, "SM.source")
|
||||
else
|
||||
try self.manager.acquireArena(.large, "SM.source");
|
||||
}
|
||||
|
||||
var buffer: std.ArrayList(u8) = .empty;
|
||||
if (transfer.getContentLength()) |cl| {
|
||||
try buffer.ensureTotalCapacity(self.arena.allocator(), cl);
|
||||
if (content_length) |cl| {
|
||||
try buffer.ensureTotalCapacity(self.sourceAllocator(), cl);
|
||||
}
|
||||
self.source = .{ .remote = buffer };
|
||||
return .proceed;
|
||||
@@ -765,7 +791,7 @@ pub const Script = struct {
|
||||
}
|
||||
|
||||
fn _dataCallback(self: *Script, _: *HttpClient.Transfer, data: []const u8) !void {
|
||||
try self.source.remote.appendSlice(self.arena.allocator(), data);
|
||||
try self.source.remote.appendSlice(self.sourceAllocator(), data);
|
||||
}
|
||||
|
||||
pub fn doneCallback(ctx: *anyopaque) !void {
|
||||
|
||||
@@ -49,6 +49,20 @@
|
||||
testing.expectEqual('♥', d3.decode(new Uint8Array([165]), { stream: true }));
|
||||
</script>
|
||||
|
||||
<script id=stream_many>
|
||||
// Each decode's output lives on the caller's call_arena, which resets under
|
||||
// the decoder between calls. Reusing one decoder across many chunks must
|
||||
// still return only the chunk just decoded.
|
||||
let d5 = new TextDecoder();
|
||||
let out = '';
|
||||
for (let i = 0; i < 500; i++) {
|
||||
out += d5.decode(new Uint8Array([104, 105]), { stream: true });
|
||||
}
|
||||
testing.expectEqual(1000, out.length);
|
||||
testing.expectEqual('hihi', out.slice(0, 4));
|
||||
testing.expectEqual('', d5.decode());
|
||||
</script>
|
||||
|
||||
<script id=slice>
|
||||
const buf1 = new ArrayBuffer(7);
|
||||
const arr1 = new Uint8Array(buf1)
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
const Factory = @import("../Factory.zig");
|
||||
const RO = @import("DOMPointReadOnly.zig");
|
||||
|
||||
const DOMPoint = @This();
|
||||
@@ -32,11 +31,8 @@ pub fn init(x_: ?f64, y_: ?f64, z_: ?f64, w_: ?f64, exec: *const js.Execution) !
|
||||
}
|
||||
|
||||
pub fn create(x: f64, y: f64, z: f64, w: f64, page: *Page) !*DOMPoint {
|
||||
const arena = try page.getArena(.tiny, "DOMPoint");
|
||||
errdefer arena.release();
|
||||
|
||||
const self = try Factory.chainedWithAllocator(arena.allocator(), .{
|
||||
RO.buildValue(arena, x, y, z, w),
|
||||
const self = try page.factory.chained(.{
|
||||
RO.buildValue(x, y, z, w),
|
||||
DOMPoint{ ._proto = undefined },
|
||||
});
|
||||
self._proto._type = .{ .mutable = self };
|
||||
|
||||
@@ -30,7 +30,6 @@ pub const _prototype_root = true;
|
||||
|
||||
_type: Type,
|
||||
_rc: lp.RC,
|
||||
_arena: *lp.Arena,
|
||||
|
||||
_x: f64,
|
||||
_y: f64,
|
||||
@@ -70,8 +69,11 @@ pub fn init(x_: ?f64, y_: ?f64, z_: ?f64, w_: ?f64, exec: *const js.Execution) !
|
||||
return createBare(x_ orelse 0, y_ orelse 0, z_ orelse 0, w_ orelse 1, exec.page);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *DOMPointReadOnly, _: *Page) void {
|
||||
self._arena.release();
|
||||
pub fn deinit(self: *DOMPointReadOnly, page: *Page) void {
|
||||
switch (self._type) {
|
||||
.generic => page.factory.destroy(self),
|
||||
.mutable => |point| page.factory.destroy(point),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *DOMPointReadOnly) void {
|
||||
@@ -83,18 +85,12 @@ pub fn releaseRef(self: *DOMPointReadOnly, page: *Page) void {
|
||||
}
|
||||
|
||||
pub fn createBare(x: f64, y: f64, z: f64, w: f64, page: *Page) !*DOMPointReadOnly {
|
||||
const arena = try page.getArena(.tiny, "DOMPoint");
|
||||
errdefer arena.release();
|
||||
|
||||
const self = try arena.create(DOMPointReadOnly);
|
||||
self.* = buildValue(arena, x, y, z, w);
|
||||
return self;
|
||||
return page.factory.create(buildValue(x, y, z, w));
|
||||
}
|
||||
|
||||
pub fn buildValue(arena: *lp.Arena, x: f64, y: f64, z: f64, w: f64) DOMPointReadOnly {
|
||||
pub fn buildValue(x: f64, y: f64, z: f64, w: f64) DOMPointReadOnly {
|
||||
return .{
|
||||
._rc = .{},
|
||||
._arena = arena,
|
||||
._type = .generic,
|
||||
._x = x,
|
||||
._y = y,
|
||||
|
||||
@@ -396,7 +396,8 @@ pub fn importScripts(self: *WorkerGlobalScope, urls: []const [:0]const u8) !void
|
||||
}
|
||||
|
||||
const session = self._session;
|
||||
const arena = try session.getArena(.large, "importScript");
|
||||
// HttpClient will take out a larger arena for the body, if necessary
|
||||
const arena = try session.getArena(.small, "importScript");
|
||||
defer arena.release();
|
||||
|
||||
for (urls) |url| {
|
||||
@@ -415,7 +416,7 @@ fn importScript(self: *WorkerGlobalScope, arena: Allocator, url: [:0]const u8) !
|
||||
var headers = try http_client.newHeaders();
|
||||
try self.headersForRequest(&headers);
|
||||
|
||||
const response = http_client.syncRequest(arena, .{
|
||||
var response = http_client.syncRequest(.{
|
||||
.url = resolved_url,
|
||||
.method = .GET,
|
||||
.frame_id = self._frame_id,
|
||||
@@ -431,6 +432,7 @@ fn importScript(self: *WorkerGlobalScope, arena: Allocator, url: [:0]const u8) !
|
||||
log.warn(.http, "importScript", .{ .url = resolved_url, .err = err });
|
||||
return error.NetworkError;
|
||||
};
|
||||
defer response.deinit();
|
||||
|
||||
if (response.status != 200) {
|
||||
log.warn(.http, "importScript", .{ .url = resolved_url, .status = response.status });
|
||||
|
||||
@@ -55,7 +55,9 @@ pub fn init(label_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*TextDecoder {
|
||||
return error.RangeError;
|
||||
}
|
||||
|
||||
const arena = try page.getArena(.large, "TextDecoder");
|
||||
// Only ever holds the decoder itself and the lazily-lowercased encoding
|
||||
// name; decode output comes from the caller's call_arena.
|
||||
const arena = try page.getArena(.tiny, "TextDecoder");
|
||||
errdefer arena.release();
|
||||
|
||||
const opts = opts_ orelse InitOpts{};
|
||||
@@ -110,7 +112,7 @@ const DecodeOpts = struct {
|
||||
stream: bool = false,
|
||||
};
|
||||
|
||||
pub fn decode(self: *TextDecoder, input_: ?[]const u8, opts_: ?DecodeOpts) ![]const u8 {
|
||||
pub fn decode(self: *TextDecoder, input_: ?[]const u8, opts_: ?DecodeOpts, exec: *const js.Execution) ![]const u8 {
|
||||
const opts: DecodeOpts = opts_ orelse .{};
|
||||
const input = input_ orelse "";
|
||||
|
||||
@@ -122,12 +124,12 @@ pub fn decode(self: *TextDecoder, input_: ?[]const u8, opts_: ?DecodeOpts) ![]co
|
||||
return error.OutOfMemory;
|
||||
}
|
||||
}
|
||||
return self._decode(input, self._decoder, false);
|
||||
return self._decode(exec.call_arena, input, self._decoder, false);
|
||||
}
|
||||
|
||||
if (self._decoder) |decoder| {
|
||||
// Non-streaming with existing decoder: flush with is_last=true, then free
|
||||
const result = try self._decode(input, decoder, true);
|
||||
const result = try self._decode(exec.call_arena, input, decoder, true);
|
||||
|
||||
// on error, _decode will free the decoder. So we only free it on non-error
|
||||
html5ever.encoding_decoder_free(decoder);
|
||||
@@ -136,10 +138,10 @@ pub fn decode(self: *TextDecoder, input_: ?[]const u8, opts_: ?DecodeOpts) ![]co
|
||||
}
|
||||
|
||||
// non-streaming, no existing decoder
|
||||
return self._decode(input, null, true);
|
||||
return self._decode(exec.call_arena, input, null, true);
|
||||
}
|
||||
|
||||
fn _decode(self: *TextDecoder, input: []const u8, streaming_decoder: ?*anyopaque, is_last: bool) ![]const u8 {
|
||||
fn _decode(self: *TextDecoder, arena: std.mem.Allocator, input: []const u8, streaming_decoder: ?*anyopaque, is_last: bool) ![]const u8 {
|
||||
if (input.len == 0 and !is_last) {
|
||||
return "";
|
||||
}
|
||||
@@ -155,7 +157,7 @@ fn _decode(self: *TextDecoder, input: []const u8, streaming_decoder: ?*anyopaque
|
||||
}
|
||||
|
||||
// Allocate output buffer
|
||||
const output = try self._arena.alloc(u8, max_out);
|
||||
const output = try arena.alloc(u8, max_out);
|
||||
|
||||
// Decode using either streaming or one-shot decoder
|
||||
const result = if (streaming_decoder) |decoder|
|
||||
|
||||
@@ -69,7 +69,7 @@ pub fn init(input: Input, options: ?InitOpts, exec: *const Execution) !js.Promis
|
||||
}
|
||||
}
|
||||
|
||||
const response = try Response.init(null, .{ .status = 0 }, exec);
|
||||
const response = try Response.initPending(exec);
|
||||
errdefer response.deinit(exec.page);
|
||||
|
||||
const fetch = try response._arena.create(Fetch);
|
||||
|
||||
@@ -75,9 +75,30 @@ pub const BodyInit = body_init.BodyInit;
|
||||
|
||||
pub fn init(body_: ?BodyInit, opts_: ?InitOpts, exec: *const Execution) !*Response {
|
||||
const session = exec.session;
|
||||
const arena = try session.getPinnedArena(.large, "Response");
|
||||
errdefer arena.release();
|
||||
|
||||
const bucket: lp.ArenaPool.BucketSize = blk: {
|
||||
const body = body_ orelse break :blk .small;
|
||||
if (body == .stream) {
|
||||
// A stream body is referenced below, never copied into the arena.
|
||||
break :blk .small;
|
||||
}
|
||||
const hint = body.sizeHint() orelse break :blk .large;
|
||||
break :blk session.arena_pool.bucketFor(hint + 512);
|
||||
};
|
||||
|
||||
const arena = try session.getPinnedArena(bucket, "Response");
|
||||
errdefer arena.release();
|
||||
return initWithArena(arena, body_, opts_, exec);
|
||||
}
|
||||
|
||||
// fetch()'s response shell.
|
||||
pub fn initPending(exec: *const Execution) !*Response {
|
||||
const arena = try exec.session.getPinnedArena(.large, "Response.pending");
|
||||
errdefer arena.release();
|
||||
return initWithArena(arena, null, .{ .status = 0 }, exec);
|
||||
}
|
||||
|
||||
fn initWithArena(arena: *lp.Arena, body_: ?BodyInit, opts_: ?InitOpts, exec: *const Execution) !*Response {
|
||||
const opts = opts_ orelse InitOpts{};
|
||||
const status_text = if (opts.statusText) |st| try arena.dupe(u8, st) else "";
|
||||
|
||||
@@ -118,7 +139,7 @@ pub fn init(body_: ?BodyInit, opts_: ?InitOpts, exec: *const Execution) !*Respon
|
||||
|
||||
pub fn createError(exec: *const Execution) !*Response {
|
||||
const session = exec.session;
|
||||
const arena = try session.getPinnedArena(.large, "Response.error");
|
||||
const arena = try session.getPinnedArena(.tiny, "Response.error");
|
||||
errdefer arena.release();
|
||||
|
||||
const self = try arena.create(Response);
|
||||
@@ -144,7 +165,7 @@ pub fn createRedirect(url_: []const u8, status_: ?u16, exec: *const Execution) !
|
||||
}
|
||||
|
||||
const session = exec.session;
|
||||
const arena = try session.getPinnedArena(.large, "Response.redirect");
|
||||
const arena = try session.getPinnedArena(.small, "Response.redirect");
|
||||
errdefer arena.release();
|
||||
|
||||
const location = try URL.resolve(arena.allocator(), exec.base(), url_, .{ .encoding = exec.charset.* });
|
||||
|
||||
@@ -49,6 +49,16 @@ pub const BodyInit = union(enum) {
|
||||
buffer: js.TypedArray(u8),
|
||||
bytes: []const u8, // must be last, js.Bridge will map anything to a string
|
||||
|
||||
// How much a call to `extract` will dupe. Used for ArenaPool size selection.
|
||||
pub fn sizeHint(self: BodyInit) ?usize {
|
||||
return switch (self) {
|
||||
.bytes => |b| b.len,
|
||||
.buffer => |b| b.values.len,
|
||||
.blob => |b| b._slice.len + b._mime.len,
|
||||
.form_data, .url_search_params, .stream => null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn extract(self: BodyInit, arena: Allocator) !Extracted {
|
||||
switch (self) {
|
||||
.bytes => |b| {
|
||||
|
||||
@@ -1489,7 +1489,7 @@ test "cdp: syncRequest short-circuits after disconnect" {
|
||||
// per-test leak check, so it's verified by review. The latch check returns
|
||||
// before any other req field is read, so the rest are placeholders.
|
||||
const headers = try client.newHeaders();
|
||||
try testing.expectError(error.ClientDisconnected, client.syncRequest(testing.allocator, .{
|
||||
try testing.expectError(error.ClientDisconnected, client.syncRequest(.{
|
||||
.frame_id = 0,
|
||||
.loader_id = 0,
|
||||
.method = .GET,
|
||||
|
||||
@@ -22,6 +22,7 @@ pub const log = @import("log.zig");
|
||||
pub const datetime = @import("datetime.zig");
|
||||
pub const App = @import("App.zig");
|
||||
pub const Arena = @import("Arena.zig");
|
||||
pub const ArenaPool = @import("ArenaPool.zig");
|
||||
pub const Network = @import("network/Network.zig");
|
||||
pub const Server = @import("Server.zig");
|
||||
pub const Config = @import("Config.zig");
|
||||
|
||||
@@ -1069,7 +1069,7 @@ fn cacheStore(self: *Client, transfer: *Transfer) void {
|
||||
}
|
||||
|
||||
const SyncContext = struct {
|
||||
allocator: Allocator,
|
||||
client: *Client,
|
||||
completion: union(enum) {
|
||||
in_progress: void,
|
||||
done: void,
|
||||
@@ -1080,19 +1080,35 @@ const SyncContext = struct {
|
||||
status: u16 = 0,
|
||||
body: std.ArrayList(u8),
|
||||
|
||||
// Acquired on the first byte we have to buffer, so a bodyless response
|
||||
// never takes one. Ownership moves to the SyncResponse.
|
||||
arena: ?*lp.Arena = null,
|
||||
|
||||
fn headerCallback(transfer: *Transfer) anyerror!Transfer.HeaderResult {
|
||||
const self: *SyncContext = @ptrCast(@alignCast(transfer.req.ctx));
|
||||
lp.assert(transfer.responseStatus() != null, "HttpClient.SyncRequest.headerCallback", .{ .value = transfer.responseStatus() });
|
||||
self.status = transfer.responseStatus().?;
|
||||
if (transfer.getContentLength()) |cl| {
|
||||
try self.body.ensureTotalCapacity(self.allocator, cl);
|
||||
try self.body.ensureTotalCapacity(try self.bodyAllocator(cl), cl);
|
||||
}
|
||||
return .proceed;
|
||||
}
|
||||
|
||||
fn bodyAllocator(self: *SyncContext, content_length: ?usize) !Allocator {
|
||||
if (self.arena) |arena| {
|
||||
return arena.allocator();
|
||||
}
|
||||
const arena = if (content_length) |cl|
|
||||
try self.client.arena_pool.acquire(cl, "syncRequest.body")
|
||||
else
|
||||
try self.client.arena_pool.acquire(.large, "syncRequest.body");
|
||||
self.arena = arena;
|
||||
return arena.allocator();
|
||||
}
|
||||
|
||||
fn dataCallback(transfer: *Transfer, data: []const u8) anyerror!void {
|
||||
const self: *SyncContext = @ptrCast(@alignCast(transfer.req.ctx));
|
||||
try self.body.appendSlice(self.allocator, data);
|
||||
try self.body.appendSlice(try self.bodyAllocator(null), data);
|
||||
}
|
||||
|
||||
fn doneCallback(ctx: *anyopaque) anyerror!void {
|
||||
@@ -1111,7 +1127,8 @@ const SyncContext = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub fn syncRequest(self: *Client, allocator: Allocator, req: Request, owner: *Owner) !SyncResponse {
|
||||
// Caller must deinit SyncResponse or otherwise take ownership of its optional arena
|
||||
pub fn syncRequest(self: *Client, req: Request, owner: *Owner) !SyncResponse {
|
||||
if (self.inbox.terminated) {
|
||||
// request() takes ownership of req.headers on every path; we return
|
||||
// before calling it, so free the curl_slist here to avoid leaking it.
|
||||
@@ -1126,8 +1143,8 @@ pub fn syncRequest(self: *Client, allocator: Allocator, req: Request, owner: *Ow
|
||||
return error.SyncWaitInterrupted;
|
||||
}
|
||||
|
||||
var sync_ctx = SyncContext{ .allocator = allocator, .body = .empty };
|
||||
errdefer sync_ctx.body.deinit(allocator);
|
||||
var sync_ctx = SyncContext{ .client = self, .body = .empty };
|
||||
errdefer if (sync_ctx.arena) |arena| arena.release();
|
||||
|
||||
var r = req;
|
||||
r.sync = true;
|
||||
@@ -1168,6 +1185,7 @@ pub fn syncRequest(self: *Client, allocator: Allocator, req: Request, owner: *Ow
|
||||
.done, .shutdown => return .{
|
||||
.status = sync_ctx.status,
|
||||
.body = sync_ctx.body,
|
||||
.arena = sync_ctx.arena,
|
||||
},
|
||||
.err => |e| return e,
|
||||
}
|
||||
@@ -1705,8 +1723,14 @@ pub const SyncResponse = struct {
|
||||
status: u16,
|
||||
body: std.ArrayList(u8),
|
||||
|
||||
pub fn deinit(self: *SyncResponse, allocator: Allocator) void {
|
||||
self.body.deinit(allocator);
|
||||
// Owns `body`. Null when the response had nothing to buffer. Callers that
|
||||
// keep `body` past this call take the arena instead of releasing it.
|
||||
arena: ?*lp.Arena,
|
||||
|
||||
pub fn deinit(self: *SyncResponse) void {
|
||||
if (self.arena) |arena| {
|
||||
arena.release();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user