Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
6 changes: 0 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 49 additions & 0 deletions bench/snippets/filesystem-router.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { bench, run } from "../runner.mjs";

const root = mkdtempSync(join(tmpdir(), "bun-bench-fsr-"));
process.on("exit", () => rmSync(root, { recursive: true, force: true }));

// 500 routes in 10 directories, one in ten of them dynamic.
const dense = join(root, "dense");
for (let d = 0; d < 10; d++) {
const section = d === 0 ? dense : join(dense, `section-${d}`);
mkdirSync(section, { recursive: true });
for (let i = 0; i < 50; i++) {
writeFileSync(join(section, i % 10 === 0 ? `[param${i}].tsx` : `page-${i}.tsx`), "");
}
}

// 50 routes in 50 directories: the shape in which a scan costs the most for each route.
const sparse = join(root, "sparse");
// The same, with each route file a link to a file outside the tree.
const linked = join(root, "linked");
mkdirSync(join(root, "targets"));
for (let d = 0; d < 50; d++) {
mkdirSync(join(sparse, `section-${d}`), { recursive: true });
writeFileSync(join(sparse, `section-${d}`, "page.tsx"), "");
mkdirSync(join(linked, `section-${d}`), { recursive: true });
writeFileSync(join(root, "targets", `page-${d}.tsx`), "");
symlinkSync(join(root, "targets", `page-${d}.tsx`), join(linked, `section-${d}`, "page.tsx"));
}

// The first scan of a process is not a repeat of anything: time it once, on its own.
const start = Bun.nanoseconds();
const router = new Bun.FileSystemRouter({ style: "nextjs", dir: dense });
console.log(`first new Bun.FileSystemRouter(): ${((Bun.nanoseconds() - start) / 1e6).toFixed(2)} ms`);

for (const [shape, dir] of [
["500 routes, 10 directories", dense],
["50 routes, 50 directories", sparse],
["50 linked routes, 50 directories", linked],
]) {
const reloaded = new Bun.FileSystemRouter({ style: "nextjs", dir });
bench(`new Bun.FileSystemRouter() (${shape})`, () => new Bun.FileSystemRouter({ style: "nextjs", dir }));
bench(`router.reload() (${shape})`, () => reloaded.reload());
}
bench("router.match() a static route", () => router.match("/section-3/page-7"));
bench("router.match() a dynamic route", () => router.match("/section-3/hello"));

await run();
1 change: 1 addition & 0 deletions scripts/rust-miri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const MIRI_CRATES = [
"bun_pod",
"bun_ptr",
"bun_resolve_builtins",
"bun_router",
"bun_semver",
"bun_shell_parser",
"bun_threading",
Expand Down
48 changes: 0 additions & 48 deletions src/bun_alloc/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,6 @@ pub fn trim<'a>(s: &'a [u8], chars: &[u8]) -> &'a [u8] {
}

// ─── ascii-lowercase helpers ──────────────────────────────────────────────
// Sunk from bun_core::strings so bun_alloc::BSSList::append_lower_case can call
// them without a dep cycle (bun_core → bun_alloc, not the reverse).
// `bun_core::strings` re-exports `copy_lowercase` and `ascii_lowercase_buf`.

/// ASCII-lowercase
Expand Down Expand Up @@ -2033,52 +2031,6 @@ impl<const COUNT: usize, const ITEM_LENGTH: usize> BSSStringList<COUNT, ITEM_LEN
Ok(unsafe { core::slice::from_raw_parts(ptr, len) })
}

/// Append `value` lowercased ASCII-wise.
///
/// The previous port routed the lowercase scratch through a
/// `thread_local! { RefCell<Box<[u8; 4096]>> }`, which (a) heap-allocs 4 KiB
/// on first use per thread and (b) pays a `RefCell` flag check per call.
/// Filenames are overwhelmingly <256 bytes, so a stack scratch suffices for
/// the hot path; longer inputs (rare — full paths) fall through to a
/// one-shot heap temp. No TLS, no Box-on-first-use, no `RefCell`.
///
/// SAFETY: see [`append`].
pub unsafe fn append_lower_case<'a>(
this: *mut Self,
value: &[u8],
) -> core::result::Result<&'a [u8], AllocError> {
// SAFETY: see `append`.
let _guard = unsafe { (*this).mutex.lock() };

// `do_append` only reads `slice` via `BSSAppendable::copy_into` (copies
// into `self.backing_buf` / a fresh heap alloc) and returns raw parts
// pointing at that owned storage, not at `slice` — so the scratch
// buffer's borrow does not escape.
let (ptr, len) = if value.len() <= 256 {
let mut scratch = [0u8; 256];
// SAFETY: inner mutex held ⇒ this thread has exclusive access.
unsafe {
(*this).do_append(&crate::copy_lowercase(value, &mut scratch[..value.len()]))?
}
} else {
// Slow path: input >256 bytes (rare). Use a one-shot heap temp via
// mimalloc directly (PORTING.md forbids `Vec` in hot allocators).
let p = mimalloc::mi_malloc(value.len()).cast::<u8>();
if p.is_null() {
return Err(AllocError);
}
// SAFETY: `p` is a fresh allocation of `value.len()` bytes; sole owner.
let tmp = unsafe { core::slice::from_raw_parts_mut(p, value.len()) };
// SAFETY: inner mutex held ⇒ this thread has exclusive access.
let r = unsafe { (*this).do_append(&crate::copy_lowercase(value, tmp)) };
// SAFETY: `p` was allocated by `mi_malloc` above.
unsafe { mimalloc::mi_free(p.cast()) };
r?
};
// SAFETY: see `append`.
Ok(unsafe { core::slice::from_raw_parts(ptr, len) })
}

/// Returns `(ptr, len)` of the freshly-appended payload (excluding the trailing NUL),
/// where `ptr` carries write provenance (`out.as_mut_ptr()`). Callers reconstruct a
/// `&[u8]` (`append`) or `&mut [u8]` (`append_mutable`) from it; returning raw parts
Expand Down
2 changes: 1 addition & 1 deletion src/install/NetworkTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use crate::{ExtractTarball, PackageManager, TarballStream, Task};

// Adapter so `StringOrTinyString::init_append_if_needed` can intern overflow
// names into the resolver's filename arena. The bun_sys-level `FilenameStore` exposes `append` /
// `append_lower_case` but doesn't itself implement `strings::Appender` (that
// `append_slice` but doesn't itself implement `strings::Appender` (that
// impl lives in `bun_resolver`, which this crate can't reach without a cycle).
pub struct FilenameStoreAppender<'a>(pub(crate) &'a FilenameStore);
impl strings::Appender for FilenameStoreAppender<'_> {
Expand Down
20 changes: 20 additions & 0 deletions src/jsc/hot_reloader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,26 @@ impl ImportWatcher {
.items::<"package_json", Option<&'static bun_watcher::PackageJSON>>()[index as usize]
}

/// The paths of the watched files whose directory `in_dir` accepts, copied under the watcher's mutex.
pub fn watched_files(&self, mut in_dir: impl FnMut(&[u8]) -> bool) -> Vec<Box<[u8]>> {
let w = match self {
ImportWatcher::Hot(w) | ImportWatcher::Watch(w) => w,
ImportWatcher::None => return Vec::new(),
};
let _guard = w.mutex.lock_guard();
let paths = w.watchlist.items_file_path();
let kinds = w.watchlist.items_kind();
paths
.iter()
.zip(kinds)
.filter(|(path, kind)| {
**kind == bun_watcher::Kind::File
&& in_dir(bun_paths::dirname(path).unwrap_or_default())
})
.map(|(path, _)| Box::from(&**path))
.collect()
}

#[inline]
pub fn add_file_by_path_slow(&mut self, file_path: &[u8]) -> bool {
match self {
Expand Down
6 changes: 0 additions & 6 deletions src/resolver/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,12 +180,6 @@ impl Entry {
self.base_lowercase_.slice()
}

/// Interned in DirnameStore.
#[inline]
pub fn dir(&self) -> &'static [u8] {
self.dir
}

/// Returns `Interned::EMPTY` until a fill has been published. Lock-free.
#[inline]
pub fn abs_path(&self) -> Interned {
Expand Down
11 changes: 0 additions & 11 deletions src/resolver/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,17 +156,6 @@ pub mod fs {
unsafe { bun_alloc::BSSStringList::append($backing(), &value) }
.map_err(|_| bun_alloc::AllocError)
}
/// Like `append`, but ASCII-lowercases `value` while copying
/// it into the store.
#[inline]
pub fn append_lower_case(
&self,
value: &[u8],
) -> core::result::Result<&'static [u8], bun_alloc::AllocError> {
// SAFETY: see `append`.
unsafe { bun_alloc::BSSStringList::append_lower_case($backing(), value) }
.map_err(|_| bun_alloc::AllocError)
}
}
};
}
Expand Down
9 changes: 0 additions & 9 deletions src/router/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,13 @@ path = "lib.rs"
workspace = true

[dependencies]
thiserror.workspace = true
bun_errno.workspace = true
bun_alloc.workspace = true
strum.workspace = true
bstr.workspace = true
scopeguard.workspace = true
bun_core.workspace = true
bun_collections.workspace = true
bun_ast.workspace = true
bun_http_types.workspace = true
bun_paths.workspace = true
bun_ptr.workspace = true
# `bun_resolver` is acyclic from this crate (resolver does not depend on
# router); router consumes the real `bun_resolver::fs::{FileSystem,DirEntry}`
# now that the `bun_sys::fs::DirEntry` opaque-seam workaround is gone.
bun_resolver.workspace = true
bun_sys.workspace = true
bun_url.workspace = true
bun_wyhash.workspace = true
25 changes: 0 additions & 25 deletions src/router/error.rs

This file was deleted.

Loading
Loading