Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
83 changes: 47 additions & 36 deletions src/bundler/ServerComponentParseTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ use std::fmt::Write as _;
use bun_alloc::{AllocError as OOM, Arena}; // bumpalo::Bump re-export
use bun_collections::VecExt;

use bun_ast::{Loc, Log, Source};
use bun_ast::{Index, Loc, Log, Source};
use bun_paths::fs::Path as FsPath;
use bun_threading::thread_pool::Task as ThreadPoolTask;

use bun_ast::ast_result::NamedExports;
use bun_ast::{B, Binding, E, G, S, Stmt, symbol};
use bun_ast::{ExprNodeList, LocRef, StmtOrExpr, UseDirective};
use bun_ast::{ImportKind, ImportRecordFlags};
Expand All @@ -24,19 +24,21 @@ use crate::cache::ExternalFreeFunction;
use crate::options::{Loader, Target};
use crate::parse_task::{self, ResultValue, Success, WatcherData, on_complete};

/// Boxed by `BundleV2::enqueue_server_component_generated_file`, which hands
/// it to the worker pool; [`task_callback_wrap`] takes the box back and frees
/// it once the file has been generated. Everything the generated AST may
/// point at (`Data`) lives in the bundle arena, never in the task itself.
Comment thread
robobun marked this conversation as resolved.
Outdated
pub(crate) struct ServerComponentParseTask {
pub task: ThreadPoolTask,
pub data: Data,
// BACKREF (LIFETIMES.tsv) — written through in `on_complete`.
// `ParentRef` (write-provenance via `NonNull::from(&mut self)` at construction)
// so deref sites are safe; `None` only for the FRU `Default` placeholder.
pub ctx: Option<bun_ptr::ParentRef<BundleV2<'static>, bun_ptr::Mut>>,
/// The generated file's own source record; moved into the result.
pub source: Source,
}

// `ServerComponentParseTask` is bump-arena-allocated; boxing the large arm
// would leak. The size diff is acceptable.
#[allow(clippy::large_enum_variant)]
pub enum Data {
/// Generate server-side code for a "use client" module. Given the
/// client ast, a "reference proxy" is created with identical exports.
Expand All @@ -46,30 +48,45 @@ pub enum Data {
}

pub struct ReferenceProxy {
pub(crate) other_source: Source,
pub(crate) named_exports: NamedExports,
/// Path of the "use client" module the proxy stands in for.
pub(crate) client_path: FsPath<'static>,
/// Source index of that module; production builds refer to its chunk
/// through a `UniqueKey` built from it.
Comment thread
robobun marked this conversation as resolved.
Outdated
pub(crate) client_source_index: Index,
/// That module's export names, in export order. Bundle-arena copies made
/// by `BundleV2::copy_export_names_for_reference_proxy`.
Comment thread
robobun marked this conversation as resolved.
Outdated
pub(crate) export_names: &'static [&'static [u8]],
}

pub struct ClientEntryWrapper {
// Owned copy.
pub(crate) path: Box<[u8]>,
/// Bundle-arena or `'static` bytes; stored as-is in the `ImportRecord`.
pub(crate) path: &'static [u8],
}

/// Raw thread-pool callback. Recovers `&mut ServerComponentParseTask` from the
/// intrusive `task` field and dispatches the parse, then posts the result back
/// to the owning event loop.
/// Raw thread-pool callback. Takes back the `Box<ServerComponentParseTask>`
/// that `BundleV2::enqueue_server_component_generated_file` handed to the
/// pool, generates the file, frees the task, then posts the result back to the
/// owning event loop.
Comment thread
robobun marked this conversation as resolved.
Outdated
// CONCURRENCY: thread-pool callback — runs on worker threads, one task per
// `ServerComponentParseTask` (heap-allocated, scheduled exactly once). Writes:
// own fields + `Log` (local) + result is posted via
// `ctx.loop_.enqueue_task_concurrent` (MPSC). Reads `ctx: &BundleV2` shared.
// `ServerComponentParseTask` is `Send` because `ctx: *mut BundleV2` is a
// backref to a `Send` type and `Source`/`Data` payloads are bundle-arena
// slices.
// `ServerComponentParseTask` is `Send` (built on the bundle thread, used and
// freed here) because `ctx: *mut BundleV2` is a backref to a `Send` type,
// `Data` is `Copy` data plus bundle-arena slices, and `Source` owns at most
// global-heap buffers.
Comment thread
robobun marked this conversation as resolved.
Outdated
fn task_callback_wrap(thread_pool_task: *mut ThreadPoolTask) {
// SAFETY: `thread_pool_task` points to the `task` field of a heap-allocated
// `ServerComponentParseTask` enqueued by BundleV2; offset_of recovers the parent.
let task: &mut ServerComponentParseTask = unsafe {
&mut *(bun_core::from_field_ptr!(ServerComponentParseTask, task, thread_pool_task))
// SAFETY: `thread_pool_task` is the `task` field of the
// `ServerComponentParseTask` that `enqueue_server_component_generated_file`
// leaked with `heap::into_raw` and scheduled; the pool runs a task exactly
// once and never touches it after this callback, so ownership of the box
// is ours to reclaim.
let mut task: Box<ServerComponentParseTask> = unsafe {
bun_core::heap::take(bun_core::from_field_ptr!(
ServerComponentParseTask,
task,
thread_pool_task
))
};

// `ctx` is a `ParentRef` BACKREF to the owning BundleV2 (set at enqueue).
Expand All @@ -84,11 +101,14 @@ fn task_callback_wrap(thread_pool_task: *mut ThreadPoolTask) {
// worker-owned bump arena; lives for the worker's lifetime.
let arena: &Arena = worker.arena();

let value = match task_callback(task, &mut log, arena) {
let value = match task_callback(&mut task, &mut log, arena) {
Ok(success) => ResultValue::Success(success),
// Only possible error is OOM; abort like `bun.outOfMemory()`.
Err(_oom) => bun_core::out_of_memory(),
};
// `task_callback` moved `source` into `value` and nothing else in the
// result refers to the task, so it is gone before the result is posted.
Comment thread
robobun marked this conversation as resolved.
Outdated
drop(task);

let result = Box::new(parse_task::Result {
// `ctx` already a `ParentRef<BundleV2>` with write provenance
Expand Down Expand Up @@ -215,21 +235,15 @@ impl Default for ServerComponentParseTask {
node: Default::default(),
callback: task_callback_wrap,
},
data: Data::ClientEntryWrapper(ClientEntryWrapper {
path: Box::default(),
}),
data: Data::ClientEntryWrapper(ClientEntryWrapper { path: b"" }),
ctx: None,
source: Source::default(),
}
}
}

fn generate_client_entry_wrapper(data: &ClientEntryWrapper, b: &mut AstBuilder) -> Result<(), OOM> {
// `add_import_record` stores the slice raw in the `ImportRecord`; `data.path`
// outlives the bundle pass (owned by the heap-allocated task). Route through
// `StoreStr` so the lifetime erasure goes through one audited unsafe.
let path = bun_ast::StoreStr::new(&data.path[..]);
let record = b.add_import_record(path.slice(), ImportKind::Stmt)?;
let record = b.add_import_record(data.path, ImportKind::Stmt)?;
let namespace_ref = b.new_symbol(symbol::Kind::Other, b"main")?;
b.append_stmt(S::Import {
namespace_ref,
Expand Down Expand Up @@ -257,8 +271,6 @@ fn generate_client_reference_proxy(
// config must be non-null to enter this function
.unwrap_or_else(|| unreachable!());

let client_named_exports = &data.named_exports;

// `add_import_stmt` stores the slices raw in `ImportRecord`/`ClauseItem`s;
// the framework config outlives the bundle pass. Route through `StoreStr`
// so the lifetime erasure goes through one audited unsafe.
Expand All @@ -275,7 +287,7 @@ fn generate_client_reference_proxy(
// that information is not yet available since chunks are not
// computed. The unique_key replacement system is used here.
if ctx.transpiler().options.has_dev_server() {
b.bump.alloc_slice_copy(data.other_source.path.pretty)
b.bump.alloc_slice_copy(data.client_path.pretty)
} else {
let mut buf = bun_alloc::ArenaString::new_in(b.bump);
write!(
Expand All @@ -284,16 +296,15 @@ fn generate_client_reference_proxy(
crate::chunk::UniqueKey {
prefix: ctx.unique_key,
kind: crate::chunk::QueryKind::Scb,
index: data.other_source.index.0,
index: data.client_source_index.0,
},
)
.map_err(|_| OOM)?;
buf.into_bump_str().as_bytes()
},
));

for key in client_named_exports.keys() {
let key: &[u8] = key.as_ref();
for &key in data.export_names {
let is_default = key == b"default";

// This error message is taken from
Expand All @@ -309,7 +320,7 @@ fn generate_client_reference_proxy(
"client function from the server, it can only be rendered as a ",
"Component or passed to props of a Client Component.",
),
module_path = bstr::BStr::new(data.other_source.path.pretty),
module_path = bstr::BStr::new(data.client_path.pretty),
)
} else {
write!(
Expand Down Expand Up @@ -359,7 +370,7 @@ fn generate_client_reference_proxy(
..Default::default()
}),
module_path,
b.new_expr(E::String::init(b.bump.alloc_slice_copy(key))),
b.new_expr(E::String::init(key)),
]),
..Default::default()
});
Expand Down
67 changes: 44 additions & 23 deletions src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3728,6 +3728,27 @@ pub mod bv2_impl {
Ok(source_index.get())
}

/// Copies a "use client" module's export names into the bundle arena
/// for the `ReferenceProxy` generated in its place. The proxy is built
/// on a worker thread, so it cannot read the module's `named_exports`
/// out of `graph.ast`, which this thread keeps appending to.
Comment thread
robobun marked this conversation as resolved.
Outdated
fn copy_export_names_for_reference_proxy(
&self,
named_exports: &crate::bundled_ast::NamedExports,
) -> &'static [&'static [u8]] {
// SAFETY: same contract as `interned_slice` — the arena outlives the
// bundle pass, and the proxy task (and the AST it produces, which
// stores these slices) is consumed within it.
let arena: &'static bun_alloc::Arena =
unsafe { bun_ptr::detach_lifetime_ref::<bun_alloc::Arena>(self.arena()) };
arena.alloc_slice_fill_iter(
named_exports
.keys()
.iter()
.map(|name| -> &'static [u8] { arena.alloc_slice_copy(&name[..]) }),
)
}

/// Enqueue a ServerComponentParseTask.
/// `source_without_index` is copied and assigned a new source index. That index is returned.
pub(crate) fn enqueue_server_component_generated_file(
Expand All @@ -3754,9 +3775,9 @@ pub mod bv2_impl {
})?;
let _ = self.graph.ast.append(JSAst::empty_in(self.graph.heap)); // OOM/capacity: fire-and-forget

// `bun.new(ServerComponentParseTask, …)` — heap-owned by the
// worker pool; freed via `bun.destroy` in `on_complete` after the
// result posts back to the bundle thread.
// Handed to the worker pool; `ServerComponentParseTask`'s pool
// callback takes the box back and frees it once the file has been
// generated.
Comment thread
robobun marked this conversation as resolved.
Outdated
let task = bun_core::heap::into_raw(Box::new(ServerComponentParseTask {
data,
// SAFETY: `from_mut(self)` is the live bundle (write provenance for
Expand All @@ -3774,7 +3795,9 @@ pub mod bv2_impl {

self.increment_scan_counter();

// SAFETY: `task` is the just-allocated arena box; sole reference here.
// SAFETY: `task` is the just-allocated box and this is its only
// pointer; projecting through it (not through a `&mut` to the field)
// keeps whole-allocation provenance for the callback's `heap::take`.
self.graph
.pool()
.worker_pool()
Expand Down Expand Up @@ -7176,11 +7199,8 @@ pub mod bv2_impl {
}
result.ast.import_records = import_records;

// `result.ast` is moved into `graph.ast` and `result.source` was
// swapped earlier, so snapshot the data the use-directive block
// needs *before* the move. Only paid for files that hit the SCB gate.
let named_exports_for_scb = if result.use_directive != crate::UseDirective::None
&& {
let is_server_component_boundary =
result.use_directive != crate::UseDirective::None && {
let separate = this
.framework
.as_ref()
Expand All @@ -7196,11 +7216,7 @@ pub mod bv2_impl {
} else {
is_client != is_browser
}
} {
Some(result.ast.named_exports.clone().expect("oom"))
} else {
None
};
};

let result_heap = *result.ast.parts.allocator();
this.graph.ast.set(
Expand All @@ -7219,7 +7235,8 @@ pub mod bv2_impl {
.expect("oom");
}

if let Some(named_exports) = named_exports_for_scb {
// Index the boundary and enqueue the files for its other side.
if is_server_component_boundary {
if result.use_directive == crate::UseDirective::Server {
bun_core::todo_panic!("\"use server\"");
}
Expand All @@ -7241,17 +7258,21 @@ pub mod bv2_impl {

let (reference_source_index, ssr_index) = if separate_ssr_graph {
// Enqueue two files, one in server graph, one in ssr graph.
let other_source =
this.graph.input_files.items_source()[result_source_index].clone();
let scb_source =
this.graph.input_files.items_source()[result_source_index].clone();
let export_names = this.copy_export_names_for_reference_proxy(
&this.graph.ast.items_named_exports()[result_source_index],
);
let client_source =
&this.graph.input_files.items_source()[result_source_index];
let proxy = crate::ServerComponentParseTask::ReferenceProxy {
client_path: client_source.path,
client_source_index: client_source.index,
export_names,
};
let scb_source = client_source.clone();
let reference_source_index = this
.enqueue_server_component_generated_file(
crate::ServerComponentParseTask::Data::ClientReferenceProxy(
crate::ServerComponentParseTask::ReferenceProxy {
other_source,
named_exports,
},
proxy,
),
scb_source,
)
Expand Down
Loading