Skip to content
Merged
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
1 change: 1 addition & 0 deletions changelog.d/6922-fs-thread-codec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**Binary size:** unpin the `fs.promises` FileHandle machinery from the always-linked cross-thread value codec via an armed `FsThreadCodec` hook (fs arms it at the single FileHandle mint point). Programs that never create a FileHandle no longer link its prototype/impl tree; cross-thread detach semantics (`fd === -1` on the far side) are unchanged. Default hello world: 4.6 → 4.5 MB.
17 changes: 17 additions & 0 deletions crates/perry-runtime/src/fs/filehandle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1451,7 +1451,24 @@ pub(crate) extern "C" fn filehandle_read_lines_impl(
interface
}

/// Cross-thread codec for FileHandle values (see `thread::FsThreadCodec`):
/// keeps the fs surface out of binaries that never create a FileHandle. The
/// probe unifies the two identity checks the thread serializer used to call
/// directly.
static FS_THREAD_CODEC_IMPL: crate::thread::FsThreadCodec = crate::thread::FsThreadCodec {
is_filehandle: fs_codec_is_filehandle,
build_detached: build_detached_filehandle_object,
};

fn fs_codec_is_filehandle(value: f64) -> bool {
crate::fs::is_fs_filehandle_value(value) || crate::fs::filehandle_object_fd(value).is_some()
}

fn build_filehandle_object(fd: i32) -> f64 {
// Arm BEFORE the object exists: every FileHandle (attached or detached)
// is minted here, so the thread codec is live from the first moment a
// probe could ever match.
crate::thread::arm_fs_thread_codec(&FS_THREAD_CODEC_IMPL);
crate::closure::js_register_closure_arity(filehandle_stat_impl as *const u8, 1);
crate::closure::js_register_closure_arity(filehandle_write_file_impl as *const u8, 2);
crate::closure::js_register_closure_arity(filehandle_append_file_impl as *const u8, 2);
Expand Down
60 changes: 53 additions & 7 deletions crates/perry-runtime/src/thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@
//! ```

use std::ptr;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::atomic::{AtomicPtr, AtomicUsize, Ordering};

use crate::bigint::{self, BigIntHeader, BIGINT_LIMBS};
use crate::closure::{self, real_capture_count, ClosureHeader};
Expand Down Expand Up @@ -269,6 +269,8 @@ pub enum SerializedValue {
/// An `fs.promises.FileHandle` crossing a `perry/thread` boundary.
/// Perry's fd registry is thread-local, so handles are not transferable;
/// deserialize as a FileHandle-shaped object with `fd === -1`.
/// Recognised/built via [`FsThreadCodec`] so the fs surface is linked
/// only when a FileHandle can actually exist.
DetachedFileHandle,

/// A `SharedArrayBuffer` crossing a `perry/thread` boundary (#4913).
Expand Down Expand Up @@ -312,6 +314,47 @@ unsafe impl Sync for SerializedValue {}
/// # Safety
/// The `bits` must be a valid NaN-boxed JSValue. Pointer-tagged values must
/// point to valid, live objects in the current thread's arena or malloc heap.
/// Cross-thread codec hook for `fs.promises` FileHandle values (binary
/// size). The serializer's FileHandle probe and the deserializer's
/// detached-handle builder live in `crate::fs`; referencing them statically
/// from this always-linked codec pinned the whole fs surface into every
/// binary (the microtask pump drains diagnostics publishes through the
/// codec, so it is reachable from `main` unconditionally). `crate::fs` arms
/// the hook at the top of `build_filehandle_object` — before the first
/// FileHandle object can exist — so a program that never creates one links
/// none of it and can never observe the difference: with no FileHandle in
/// the process, the probe cannot match and the variant is never produced.
pub(crate) struct FsThreadCodec {
/// `is_fs_filehandle_value(v) || filehandle_object_fd(v).is_some()`.
pub is_filehandle: fn(f64) -> bool,
/// `build_detached_filehandle_object` (FileHandle shape, `fd === -1`).
pub build_detached: fn() -> f64,
}

static FS_THREAD_CODEC: AtomicPtr<FsThreadCodec> = AtomicPtr::new(ptr::null_mut());

pub(crate) fn arm_fs_thread_codec(codec: &'static FsThreadCodec) {
// `black_box` for the same reason as `NM_INSTALL_ALL_HOOK`: a
// single-store AtomicPtr gets speculatively devirtualized by
// whole-program optimization (only one value is ever stored, so the
// compiler proves it and re-materializes the direct reference —
// re-pinning everything this hook exists to unpin).
FS_THREAD_CODEC.store(
std::hint::black_box(codec as *const FsThreadCodec as *mut FsThreadCodec),
Ordering::Release,
);
}

fn fs_thread_codec() -> Option<&'static FsThreadCodec> {
let p = FS_THREAD_CODEC.load(Ordering::Acquire);
if p.is_null() {
None
} else {
// SAFETY: only ever stores `&'static FsThreadCodec`.
Some(unsafe { &*p })
}
}

pub unsafe fn serialize_nanbox_for_thread(bits: u64) -> SerializedValue {
let tag = bits & TAG_MASK;

Expand Down Expand Up @@ -377,9 +420,7 @@ pub unsafe fn serialize_nanbox_for_thread(bits: u64) -> SerializedValue {
}
gc::GC_TYPE_OBJECT => {
let value = f64::from_bits(bits);
if crate::fs::is_fs_filehandle_value(value)
|| crate::fs::filehandle_object_fd(value).is_some()
{
if fs_thread_codec().is_some_and(|codec| (codec.is_filehandle)(value)) {
return SerializedValue::DetachedFileHandle;
}
return serialize_object(raw_ptr as *const crate::object::ObjectHeader);
Expand Down Expand Up @@ -804,9 +845,14 @@ pub unsafe fn deserialize_nanbox_on_current_thread(sv: &SerializedValue) -> u64
crate::date::alloc_date_cell(*ts).to_bits()
}

SerializedValue::DetachedFileHandle => {
crate::fs::build_detached_filehandle_object().to_bits()
}
SerializedValue::DetachedFileHandle => match fs_thread_codec() {
Some(codec) => (codec.build_detached)().to_bits(),
// Unreachable in practice: the variant is only produced by an
// armed serializer, and arming is process-global (all agents
// share one binary's statics). Defensive `undefined` mirrors the
// `Unsupported` fallback below rather than panicking.
None => TAG_UNDEFINED,
},

SerializedValue::SharedArrayBuffer { addr } => {
// Alias the same process-global backing store (#4913) — no copy.
Expand Down
Loading