Merge pull request #3092 from lightpanda-io/arena-optimizations

mem: improve ArenaPool usage
This commit is contained in:
Karl Seguin
2026-08-01 08:03:58 +08:00
committed by GitHub
17 changed files with 197 additions and 65 deletions

View File

@@ -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);

View File

@@ -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(.{}),

View File

@@ -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;
}
}

View File

@@ -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 });

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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 };

View File

@@ -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,

View File

@@ -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 });

View File

@@ -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|

View File

@@ -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);

View File

@@ -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.* });

View File

@@ -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| {

View File

@@ -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,

View File

@@ -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");

View File

@@ -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();
}
}
};