Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions bench/snippets/blob-append.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { bench, run } from "../runner.mjs";

// Accumulating into a Blob by re-wrapping it. Each case builds the whole chain,
// so the per-iteration cost should grow linearly with the number of chunks.
const chunk64KiB = new Uint8Array(64 * 1024);
const chunk64B = new Uint8Array(64);

function accumulate(chunk, count) {
let blob = new Blob([chunk]);
for (let i = 1; i < count; i++) blob = new Blob([blob, chunk]);
return blob;
}

bench("b = new Blob([b, 64 KiB chunk]) x 64", () => accumulate(chunk64KiB, 64));
bench("b = new Blob([b, 64 KiB chunk]) x 256", () => accumulate(chunk64KiB, 256));
bench("b = new Blob([b, 64 B chunk]) x 1024", () => accumulate(chunk64B, 1024));
bench("b = new Blob([b, 64 B chunk]) x 4096", () => accumulate(chunk64B, 4096));

const oneMiB = new Blob([new Uint8Array(1024 * 1024)]);
bench("new Blob([1 MiB blob, 64 B chunk]) once", () => new Blob([oneMiB, chunk64B]));

await run();
24 changes: 20 additions & 4 deletions src/jsc/webcore_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,14 @@ pub mod store {
/// rather than `Vec<u8>` so the memfd-backed path
/// (`LinuxMemFdAllocator::create` → `mmap`'d region freed via `munmap`)
/// can carry its allocator vtable with the buffer.
///
/// `ptr[..len]` is immutable for as long as this `Bytes` exists. The
/// allocation behind it is not necessarily this value's alone: the stores
/// that `bun_runtime`'s `AppendBuffer` builds are several `Bytes` viewing
/// prefixes of one allocation (their `allocator` keeps it alive), so only
/// `allocator.free` may be assumed about the memory past what `slice()`
/// returns, and writing through `ptr` additionally needs the allocation to
/// be exclusively this value's (see `as_array_list_leak`).
pub struct Bytes {
pub ptr: Option<NonNull<u8>>,
pub len: SizeType,
Expand All @@ -616,11 +624,12 @@ pub mod store {
pub stored_name: Box<[u8]>,
}

// SAFETY: `Bytes` is morally `Vec<u8>`-with-custom-free. The raw
// `NonNull<u8>` is uniquely owned (`ptr` is the sole alias) and
// SAFETY: `Bytes` is morally `Vec<u8>`-with-custom-free. It never writes
// through `ptr` itself, other `Bytes` sharing the allocation (see the type
// doc) only read it too, the allocator's `free` is thread-safe, and
// `StdAllocator` is `Send + Sync`.
unsafe impl Send for Bytes {}
// SAFETY: `&Bytes` only reads the uniquely-owned slice via `slice()`; no
// SAFETY: `&Bytes` only reads the immutable `ptr[..len]` via `slice()`; no
// interior mutability, so sharing references across threads is sound.
unsafe impl Sync for Bytes {}

Expand Down Expand Up @@ -753,9 +762,16 @@ pub mod store {
self.as_array_list_leak()
}

/// Writing through the result (or handing it out writable, as the
/// `Lifetime::Transfer` ArrayBuffer path does) is only sound when no
/// other `Bytes` shares the allocation: a freshly created store, or a
/// store that `AppendBuffer::shares_allocation` clears. Everything
/// else only reads through it.
pub fn as_array_list_leak(&mut self) -> &mut [u8] {
match self.ptr {
// SAFETY: `ptr[..len]` is live and uniquely owned by `*self`.
// SAFETY: `ptr[..len]` is live for as long as `*self` is, and
// `&mut self` rules out other users of this value; see the doc
// comment for what writers must additionally ensure.
Some(p) => unsafe {
core::slice::from_raw_parts_mut(p.as_ptr(), self.len as usize)
},
Expand Down
34 changes: 33 additions & 1 deletion src/runtime/webcore/Blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ use crate::node::types::{PathLikeExt as _, PathOrFdExt as _};
use store::{BytesExt as _, FileExt as _, S3Ext as _, StoreExt as _};
pub use store::{Store, StoreRef};

#[path = "blob/AppendBuffer.rs"]
pub(crate) mod append_buffer;
use append_buffer::AppendBuffer;
#[path = "blob/copy_file.rs"]
pub mod copy_file;
#[cfg(not(windows))]
Expand Down Expand Up @@ -3035,7 +3038,12 @@ impl BlobExt for Blob {
}
}
Lifetime::Transfer => {
if self.store().is_some_and(|s| !s.has_one_ref()) {
// The bytes become a writable ArrayBuffer, so besides being the
// store's only holder nothing else may be able to read them.
if self
.store()
.is_some_and(|s| !s.has_one_ref() || AppendBuffer::shares_allocation(s))
{
// SAFETY: same `buf` contract as the caller; the `Clone` arm only reads it.
let copied = unsafe {
self.to_array_buffer_view_with_bytes::<{ Lifetime::Clone }, TYPED_ARRAY_VIEW>(
Expand Down Expand Up @@ -3344,6 +3352,11 @@ impl BlobExt for Blob {
let mut stack: Vec<JSValue> = Vec::new();
let mut joiner = bun_core::string_joiner::StringJoiner::default();
let mut could_have_non_ascii = false;
// Store of a Blob part that opens the result (`new Blob([blob, ...])`).
// Its bytes stay out of `joiner` and are appended onto instead of
// copied; see `AppendBuffer`. Held as a ref so user JS run by a later
// part cannot release it.
let mut append_prefix: Option<StoreRef> = None;

loop {
match current.js_type_loose() {
Expand Down Expand Up @@ -3436,6 +3449,12 @@ impl BlobExt for Blob {
if let Some(blob) = item.as_class_ref::<Blob>() {
could_have_non_ascii = could_have_non_ascii
|| blob.charset.get() != strings::AsciiStatus::AllAscii;
if append_prefix.is_none() && joiner.len == 0 {
if let Some(store) = AppendBuffer::prefix_store(blob) {
append_prefix = Some(store.clone());
continue;
}
}
// A later part may run user JS that drops the
// last ref to this Blob's Store before `done()`.
if parts_can_run_js {
Expand Down Expand Up @@ -3510,6 +3529,19 @@ impl BlobExt for Blob {
};
}

if let Some(prefix) = append_prefix {
// As below, only a positive ASCII answer is recorded.
let is_all_ascii = (!could_have_non_ascii).then_some(true);
// `joiner` still holds the remaining parts (borrowed ones are kept
// alive by `_keep`/`arg` exactly as for `done()`); `concat` reads
// them in place and dropping the joiner frees the owned ones.
let store = AppendBuffer::concat(&prefix, &joiner, is_all_ascii);
let blob = Blob::init_with_store(store, global);
blob.charset
.set(strings::AsciiStatus::from_bool(is_all_ascii));
return Ok(blob);
}

let joined: Vec<u8> = joiner.done().expect("oom").into_vec();

if !could_have_non_ascii {
Expand Down
Loading
Loading