Merge pull request #3091 from lightpanda-io/blob-mem-tweak

mem: improve memory efficiency of blob
This commit is contained in:
Karl Seguin
2026-08-01 08:03:43 +08:00
committed by GitHub
7 changed files with 63 additions and 57 deletions

View File

@@ -78,37 +78,44 @@ const InitOptions = struct {
/// Creates a new Blob from JS values with optional MIME validation.
/// This is the JS Constructor
pub fn init(parts_: ?[]const js.Value, opts_: ?InitOptions, page: *Page) !*Blob {
const session = page.session;
const arena = try session.getPinnedArena(.large, "Blob");
errdefer arena.release();
pub fn init(parts_: ?[]const js.Value, opts_: ?InitOptions, exec: *const Execution) !*Blob {
const blob = try buildValue(parts_, opts_ orelse .{}, exec);
errdefer blob._arena.release();
const self = try arena.create(Blob);
self.* = try buildValue(arena, parts_, opts_ orelse .{});
arena.report();
const self = try blob._arena.create(Blob);
self.* = blob;
blob._arena.report();
return self;
}
// The Blob value for `parts`, with everything it references copied to
// `arena`. Callers either arena.create it (init) or embed it as the root of
// a {Blob, File} chain.
pub fn buildValue(arena: *lp.Arena, parts_: ?[]const js.Value, opts: InitOptions) !Blob {
const mime = try Mime.serialize(arena.allocator(), opts.type);
const data = blk: {
if (parts_) |blob_parts| {
const use_native_endings = std.mem.eql(u8, opts.endings, "native");
var w: Writer.Allocating = .init(arena.allocator());
for (blob_parts) |js_val| {
const part = try js_val.toStringSmart();
try writePartWithEndings(part, use_native_endings, &w.writer);
}
break :blk w.written();
pub fn buildValue(parts_: ?[]const js.Value, opts: InitOptions, exec: *const Execution) !Blob {
const data, const arena = blk: {
const parts = parts_ orelse {
break :blk .{ "", try exec.getPinnedArena(.tiny, "Blob") };
};
var len: usize = 0;
const slices = try exec.call_arena.alloc([]const u8, parts.len);
for (parts, slices) |js_val, *s| {
s.* = try js_val.toStringSmart();
len += s.len;
}
// +256, ~struct overhead, mime dupe, ...
const arena = try exec.getPinnedArena(len + 256, "blob");
break :blk "";
const buf = try arena.alloc(u8, len);
var w: Writer = .fixed(buf);
const use_native_endings = std.mem.eql(u8, opts.endings, "native");
for (slices) |part| {
try writePartWithEndings(part, use_native_endings, &w);
}
break :blk .{ w.buffered(), arena };
};
const mime = try Mime.serialize(arena.allocator(), opts.type);
return .{
._rc = .{},
._arena = arena,
@@ -129,8 +136,8 @@ pub fn buildValueFromBytes(arena: *lp.Arena, data: []const u8, content_type: []c
}
/// Creates a new Blob from raw byte slices (for internal Zig use).
pub fn initFromBytes(data: []const u8, content_type: []const u8, page: *Page) !*Blob {
const arena = try page.getPinnedArena(data.len + content_type.len + 256, "Blob");
pub fn initFromBytes(data: []const u8, content_type: []const u8, exec: *const Execution) !*Blob {
const arena = try exec.getPinnedArena(data.len + content_type.len + 256, "Blob");
errdefer arena.release();
const self = try arena.create(Blob);
@@ -318,7 +325,7 @@ pub fn slice(
start_: ?i32,
end_: ?i32,
content_type_: ?[]const u8,
page: *Page,
exec: *const Execution,
) !*Blob {
const data = self._slice;
@@ -339,7 +346,7 @@ pub fn slice(
break :blk @min(data.len, @max(start, @as(u31, @intCast(requested_end))));
};
return Blob.initFromBytes(data[start..end], content_type_ orelse "", page);
return Blob.initFromBytes(data[start..end], content_type_ orelse "", exec);
}
/// Returns the size of the Blob in bytes.
@@ -387,7 +394,7 @@ test "Blob: a pinned arena reaches the browser's account and is given back" {
try testing.expectEqual(0, browser.arena_account.pending);
const data = [_]u8{'x'} ** (64 * 1024);
const blob = try Blob.initFromBytes(&data, "text/plain", page);
const blob = try Blob.initFromBytes(&data, "text/plain", &frame.js.execution);
try testing.expect(browser.arena_account.pending >= data.len);
// The finalizer path hands every reported byte back.

View File

@@ -42,27 +42,25 @@ pub fn init(
parts_: ?[]const js.Value,
name: []const u8,
opts_: ?InitOptions,
page: *Page,
exec: *js.Execution,
) !*File {
const opts = opts_ orelse InitOptions{};
const session = page.session;
const arena = try session.getPinnedArena(.large, "Blob");
errdefer arena.release();
const blob = try Blob.buildValue(parts_, .{
.type = opts.type,
.endings = opts.endings,
}, exec);
const file = try Factory.chainedWithAllocator(arena.allocator(), .{
try Blob.buildValue(arena, parts_, .{
.type = opts.type,
.endings = opts.endings,
}),
const file = try Factory.chainedWithAllocator(blob._arena.allocator(), .{
blob,
File{
._proto = undefined,
._name = try arena.dupe(u8, name),
._name = try blob._arena.dupe(u8, name),
._last_modified = opts.lastModified orelse @intCast(lp.datetime.milliTimestamp(.real)),
},
});
file._proto._type = .{ .file = file };
arena.report();
blob._arena.report();
return file;
}

View File

@@ -73,7 +73,7 @@ pub fn getContext(_: *OffscreenCanvas, context_type: []const u8, exec: *Executio
/// Returns a Promise that resolves to a Blob containing the image.
/// Since we have no actual rendering, this returns an empty blob.
pub fn convertToBlob(_: *OffscreenCanvas, exec: *Execution) !js.Promise {
const blob = try Blob.init(null, null, exec.page);
const blob = try Blob.init(null, null, exec);
return exec.js.local.?.resolvePromise(blob);
}

View File

@@ -158,7 +158,7 @@ pub fn initFromMultipart(bytes: []const u8, boundary: []const u8, exec: *const E
else => {},
};
try form_data.parseMultipart(exec.page, bytes, boundary);
try form_data.parseMultipart(bytes, boundary, exec);
return form_data;
}
@@ -226,7 +226,7 @@ pub fn append(self: *FormData, name: []const u8, value: EntryValue, filename: ?[
if (filename) |n| {
// A supplied filename means a new File over the same bytes rather
// than a rename of the caller's object.
break :blk .{ .file = try fileFrom(blob, n, exec.page) };
break :blk .{ .file = try fileFrom(blob, n, exec) };
}
if (blob._type == .file) {
@@ -236,7 +236,7 @@ pub fn append(self: *FormData, name: []const u8, value: EntryValue, filename: ?[
}
// A Blob that is not a File becomes a File named "blob".
break :blk .{ .file = try fileFrom(blob, "blob", exec.page) };
break :blk .{ .file = try fileFrom(blob, "blob", exec) };
},
.bytes => |b| .{ .string = try String.init(self._arena.allocator(), b, .{}) },
};
@@ -250,8 +250,9 @@ pub fn append(self: *FormData, name: []const u8, value: EntryValue, filename: ?[
// Mirrors File.init — a Blob and File sharing one reference-counted arena —
// but over bytes we already hold rather than JS parts. Returned at refcount 1:
// the entry owns that reference and deleteByName releases it.
fn fileFrom(source: *Blob, name: []const u8, page: *Page) !*File {
const arena = try page.getArena(source._slice.len + source._mime.len + 256, "Blob");
fn fileFrom(source: *Blob, name: []const u8, exec: *Execution) !*File {
const arena = try exec.getArena(source._slice.len + source._mime.len + 256, "Blob");
errdefer arena.release();
const file = try Factory.chainedWithAllocator(arena.allocator(), .{
@@ -528,7 +529,7 @@ fn urlDecode(arena: Allocator, raw: []const u8) ![]const u8 {
// preamble, CRLF line breaks). Parts carrying a filename become File
// entries — the FormData holds a ref on each, released in deinit — and the
// rest become string entries.
fn parseMultipart(self: *FormData, page: *Page, bytes: []const u8, boundary: []const u8) !void {
fn parseMultipart(self: *FormData, bytes: []const u8, boundary: []const u8, exec: *const Execution) !void {
// The body must open with the dash-boundary: "--" boundary.
if (!std.mem.startsWith(u8, bytes, "--") or !std.mem.startsWith(u8, bytes[2..], boundary)) {
return error.InvalidFormData;
@@ -613,8 +614,8 @@ fn parseMultipart(self: *FormData, page: *Page, bytes: []const u8, boundary: []c
// Got a file.
if (parsed.filename) |filename| {
const blob = try Blob.initFromBytes(content, content_type, page);
errdefer blob.deinit(page);
const blob = try Blob.initFromBytes(content, content_type, exec);
errdefer blob.deinit(exec.page);
const file = try blob._arena.create(File);
file.* = .{
@@ -1251,7 +1252,7 @@ test "FormData: multipart parse" {
._arena = arena,
._entries = .empty,
};
try fd.parseMultipart(frame._page, "--BOUNDARY\r\n" ++
try fd.parseMultipart("--BOUNDARY\r\n" ++
"Content-Disposition: form-data; name=\"name\"\r\n\r\n" ++
"John\r\n" ++
"--BOUNDARY\r\n" ++
@@ -1260,7 +1261,7 @@ test "FormData: multipart parse" {
"--BOUNDARY\r\n" ++
"Content-Disposition: form-data; name=\"tricky\"\r\n\r\n" ++
"a\r\n--BOUNDARYx b\r\n" ++
"--BOUNDARY--\r\n", "BOUNDARY");
"--BOUNDARY--\r\n", "BOUNDARY", &frame.js.execution);
try testing.expectEqual(3, fd._entries.items.len);
try testing.expectString("John", fd.get(.wrap("name")).?);
@@ -1282,14 +1283,14 @@ test "FormData: multipart parse with file" {
._arena = arena,
._entries = .empty,
};
try fd.parseMultipart(frame._page, "--B\r\n" ++
try fd.parseMultipart("--B\r\n" ++
"Content-Disposition: form-data; name=\"upload\"; filename=\"hello.txt\"\r\n" ++
"Content-Type: text/plain\r\n\r\n" ++
"hello\r\n" ++
"--B\r\n" ++
"Content-Disposition: form-data; name=\"raw\"; filename=\"raw.bin\"\r\n\r\n" ++
"bytes\r\n" ++
"--B--\r\n", "B");
"--B--\r\n", "B", &frame.js.execution);
defer for (fd._entries.items) |entry| switch (entry.value) {
.file => |file| file.releaseRef(frame._page),
else => {},
@@ -1330,7 +1331,7 @@ test "FormData: multipart parse rejects malformed bodies" {
._arena = arena,
._entries = .empty,
};
try testing.expectError(error.InvalidFormData, fd.parseMultipart(frame._page, case, "B"));
try testing.expectError(error.InvalidFormData, fd.parseMultipart(case, "B", &frame.js.execution));
}
}
@@ -1364,7 +1365,7 @@ test "FormData: multipart round-trip" {
._arena = arena,
._entries = .empty,
};
try fd.parseMultipart(frame._page, buf.written(), "BOUNDARY");
try fd.parseMultipart(buf.written(), "BOUNDARY", &frame.js.execution);
try testing.expectEqual(4, fd._entries.items.len);
try testing.expectString("username", fd._entries.items[0].name.str());

View File

@@ -260,7 +260,7 @@ pub fn blob(self: *Request, exec: *const Execution) !js.Promise {
const headers = try self.getHeaders(exec);
const content_type = try headers.get("content-type", exec) orelse "";
const b = try Blob.initFromBytes(body, content_type, exec.page);
const b = try Blob.initFromBytes(body, content_type, exec);
return local.resolvePromise(b);
}

View File

@@ -448,7 +448,7 @@ pub fn blob(self: *Response, exec: *const Execution) !js.Promise {
.stream => return local.rejectPromise(.{ .type_error = "Cannot read blob from stream body" }),
};
const content_type = try self._headers.get("content-type", exec) orelse "";
const b = try Blob.initFromBytes(body, content_type, exec.page);
const b = try Blob.initFromBytes(body, content_type, exec);
return local.resolvePromise(b);
}

View File

@@ -756,7 +756,7 @@ fn dispatchMessageEvent(self: *WebSocket, data: []const u8, frame_type: http.WsF
switch (self._binary_type) {
.arraybuffer => .{ .arraybuffer = .{ .values = data } },
.blob => blk: {
const blob = try Blob.initFromBytes(data, "", exec.page);
const blob = try Blob.initFromBytes(data, "", exec);
blob.acquireRef();
break :blk .{ .blob = blob };
},