Skip to content

Commit

Permalink
Make file uploads use less memory on Linux (#7938)
Browse files Browse the repository at this point in the history
* Add Linux memfd file upload memory optimization

* various build fixes

* cleanup

* Update blob.zig

* Update base.zig

* Add COW test

* Update blob.zig

---------

Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
  • Loading branch information
Jarred-Sumner and Jarred-Sumner authored Jan 1, 2024
1 parent 1f9ce68 commit 693a00d
Show file tree
Hide file tree
Showing 7 changed files with 362 additions and 7 deletions.
3 changes: 3 additions & 0 deletions src/bun.js/base.zig
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,9 @@ pub const ArrayBuffer = extern struct {
return buffer_value;
}

extern fn ArrayBuffer__fromSharedMemfd(fd: i64, globalObject: *JSC.JSGlobalObject, byte_offset: usize, byte_length: usize, total_size: usize) JSC.JSValue;
pub const toArrayBufferFromSharedMemfd = ArrayBuffer__fromSharedMemfd;

pub fn toJSBufferFromMemfd(fd: bun.FileDescriptor, globalObject: *JSC.JSGlobalObject) JSC.JSValue {
const stat = switch (bun.sys.fstat(fd)) {
.err => |err| {
Expand Down
28 changes: 28 additions & 0 deletions src/bun.js/bindings/ZigGlobalObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1465,6 +1465,34 @@ JSC_DEFINE_HOST_FUNCTION(functionReportError,
return JSC::JSValue::encode(JSC::jsUndefined());
}

extern "C" JSC__JSValue ArrayBuffer__fromSharedMemfd(int64_t fd, JSC::JSGlobalObject* globalObject, size_t byteOffset, size_t byteLength, size_t totalLength)
{

// Windows doesn't have mmap
// This code should pretty much only be called on Linux.
#if !OS(WINDOWS)
auto ptr = mmap(nullptr, totalLength, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);

if (ptr == MAP_FAILED) {
return JSC::JSValue::encode(JSC::JSValue {});
}

auto buffer = ArrayBuffer::createFromBytes(reinterpret_cast<char*>(ptr) + byteOffset, byteLength, createSharedTask<void(void*)>([ptr, totalLength](void* p) {
munmap(ptr, totalLength);
}));

Structure* structure = globalObject->arrayBufferStructure(JSC::ArrayBufferSharingMode::Default);

if (UNLIKELY(!structure)) {
return JSC::JSValue::encode(JSC::JSValue {});
}

return JSValue::encode(JSC::JSArrayBuffer::create(globalObject->vm(), structure, WTFMove(buffer)));
#else
return JSC::JSValue::encode(JSC::JSValue {});
#endif
}

extern "C" JSC__JSValue Bun__createArrayBufferForCopy(JSC::JSGlobalObject* globalObject, const void* ptr, size_t len)
{
auto scope = DECLARE_THROW_SCOPE(globalObject->vm());
Expand Down
5 changes: 5 additions & 0 deletions src/bun.js/javascript.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1255,6 +1255,8 @@ pub const VirtualMachine = struct {
debugger: bun.CLI.Command.Debugger = .{ .unspecified = {} },
};

pub var is_smol_mode = false;

pub fn init(opts: Options) !*VirtualMachine {
JSC.markBinding(@src());
const allocator = opts.allocator;
Expand Down Expand Up @@ -1343,6 +1345,9 @@ pub const VirtualMachine = struct {
vm.jsc = vm.global.vm();
vm.smol = opts.smol;

if (opts.smol)
is_smol_mode = opts.smol;

if (source_code_printer == null) {
const writer = try js_printer.BufferWriter.init(allocator);
source_code_printer = allocator.create(js_printer.BufferPrinter) catch unreachable;
Expand Down
89 changes: 82 additions & 7 deletions src/bun.js/webcore/blob.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3484,6 +3484,10 @@ pub const Blob = struct {
return this.ptr[0..this.len];
}

pub fn allocatedSlice(this: ByteStore) []u8 {
return this.ptr[0..this.cap];
}

pub fn deinit(this: *ByteStore) void {
bun.default_allocator.free(this.stored_name.slice());
this.allocator.free(this.ptr[0..this.cap]);
Expand Down Expand Up @@ -4131,15 +4135,14 @@ pub const Blob = struct {
};
}

pub fn create(
bytes_: []const u8,
pub fn createWithBytesAndAllocator(
bytes: []u8,
allocator: std.mem.Allocator,
globalThis: *JSGlobalObject,
was_string: bool,
) Blob {
const bytes = allocator.dupe(u8, bytes_) catch @panic("Out of memory");
return Blob{
.size = @as(SizeType, @truncate(bytes_.len)),
.size = @as(SizeType, @truncate(bytes.len)),
.store = if (bytes.len > 0)
Blob.Store.init(bytes, allocator) catch unreachable
else
Expand All @@ -4150,6 +4153,50 @@ pub const Blob = struct {
};
}

pub fn tryCreate(
bytes_: []const u8,
allocator_: std.mem.Allocator,
globalThis: *JSGlobalObject,
was_string: bool,
) !Blob {
if (comptime Environment.isLinux) {
if (bun.linux.memfd_allocator.shouldUse(bytes_)) {
switch (bun.linux.memfd_allocator.create(bytes_)) {
.err => {},
.result => |result| {
const store = bun.new(
Store,
Store{
.data = .{
.bytes = result,
},
.allocator = bun.default_allocator,
.ref_count = 1,
},
);
var blob = initWithStore(store, globalThis);
if (was_string and blob.content_type.len == 0) {
blob.content_type = MimeType.text.value;
}

return blob;
},
}
}
}

return createWithBytesAndAllocator(try allocator_.dupe(u8, bytes_), allocator_, globalThis, was_string);
}

pub fn create(
bytes_: []const u8,
allocator_: std.mem.Allocator,
globalThis: *JSGlobalObject,
was_string: bool,
) Blob {
return tryCreate(bytes_, allocator_, globalThis, was_string) catch bun.outOfMemory();
}

pub fn initWithStore(store: *Blob.Store, globalThis: *JSGlobalObject) Blob {
return Blob{
.size = store.size(),
Expand Down Expand Up @@ -4513,6 +4560,36 @@ pub const Blob = struct {
pub fn toArrayBufferWithBytes(this: *Blob, global: *JSGlobalObject, buf: []u8, comptime lifetime: Lifetime) JSValue {
switch (comptime lifetime) {
.clone => {
if (comptime Environment.isLinux) {
// If we can use a copy-on-write clone of the buffer, do so.
if (this.store) |store| {
if (store.data == .bytes) {
const allocated_slice = store.data.bytes.allocatedSlice();
if (bun.isSliceInBuffer(buf, allocated_slice)) {
if (bun.linux.memfd_allocator.from(store.data.bytes.allocator)) |allocator| {
allocator.ref();
defer allocator.deref();

const byteOffset = @as(usize, @intFromPtr(buf.ptr)) -| @as(usize, @intFromPtr(allocated_slice.ptr));
const byteLength = buf.len;

const result = JSC.ArrayBuffer.toArrayBufferFromSharedMemfd(
@intCast(allocator.fd),
global,
byteOffset,
byteLength,
allocated_slice.len,
);
bloblog("toArrayBuffer COW clone({d}, {d}) = {d}", .{ byteOffset, byteLength, @intFromBool(result != .zero) });

if (result != .zero) {
return result;
}
}
}
}
}
}
return JSC.ArrayBuffer.create(global, buf, .ArrayBuffer);
},
.share => {
Expand Down Expand Up @@ -4674,9 +4751,7 @@ pub const Blob = struct {
JSC.JSValue.JSType.BigUint64Array,
JSC.JSValue.JSType.DataView,
=> {
const buf = try bun.default_allocator.dupe(u8, top_value.asArrayBuffer(global).?.byteSlice());

return Blob.init(buf, bun.default_allocator, global);
return try Blob.tryCreate(top_value.asArrayBuffer(global).?.byteSlice(), bun.default_allocator, global, false);
},

.DOMWrapper => {
Expand Down
3 changes: 3 additions & 0 deletions src/bun.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,9 @@ pub fn assertDefined(val: anytype) void {
}

pub const LinearFifo = @import("./linear_fifo.zig").LinearFifo;
pub const linux = struct {
pub const memfd_allocator = @import("./linux_memfd_allocator.zig").LinuxMemFdAllocator;
};

/// hash a string
pub fn hash(content: []const u8) u64 {
Expand Down
Loading

0 comments on commit 693a00d

Please sign in to comment.