mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-08-03 03:13:05 -04:00
Merge pull request #3091 from lightpanda-io/blob-mem-tweak
mem: improve memory efficiency of blob
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user