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
103 changes: 103 additions & 0 deletions bench/snippets/write-string.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// A write of one string: Bun.write beside fs.promises.writeFile. It runs in Bun and in Node.
//
// in call until the call returns its promise
// stall the longest time that the JS thread is held, from the call to the end of the write
// total until the promise settles
//
// Each row is the median of ROUNDS writes. A "spaced" write comes 150 ms after the write
// before it and makes a new file. By then the allocator has given the memory of a freed copy
// of the string back to the OS. A "back-to-back" write follows the write before it at once
// and replaces the file of that write.
//
// On POSIX, Bun.write writes a string below 0.25 M characters on the JS thread.
//
// bun bench/snippets/write-string.mjs [sizes in M characters, default 0.125,0.5,4,32,128]
import { mkdtempSync, rmSync } from "node:fs";
import { writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { setTimeout as sleep } from "node:timers/promises";

const M = 1024 * 1024;
const ROUNDS = 5;
const sizes = (process.argv[2] ?? "0.125,0.5,4,32,128").split(",").map(Number);

// A character is 1 byte of the file for ASCII, 2 for Latin-1 and 3 for UTF-16.
const kinds = {
"ASCII": length => Buffer.alloc(length, "a").toString("latin1"),
"Latin-1": length => Buffer.alloc(length, 0xe9).toString("latin1"),
"UTF-16": length => Buffer.alloc(length * 2, "\u4e2d", "utf16le").toString("utf16le"),
};
const apis = {
...(typeof Bun !== "undefined" && { "Bun.write": (path, string) => Bun.write(path, string) }),
"fs.promises.writeFile": (path, string) => writeFile(path, string),
};

async function time(write, path, string) {
let pending = true;
let stall = 0;
let idle;
const stopped = new Promise(resolve => (idle = resolve));
const start = performance.now();
let last = start;
const promise = write(path, string);
const inCall = performance.now() - start;
setImmediate(function turn() {
const now = performance.now();
stall = Math.max(stall, now - last);
last = now;
if (pending) setImmediate(turn);
else idle();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
});
let total;
try {
await promise;
total = performance.now() - start;
} finally {
pending = false;
}
await stopped;
return { inCall, stall, total };
}

function row(api, label, samples) {
const median = key => samples.map(sample => sample[key]).sort((a, b) => a - b)[samples.length >> 1];
const ms = key => median(key).toFixed(3).padStart(8);
console.log(` ${api.padEnd(22)} ${label.padEnd(13)} ${ms("inCall")} ${ms("stall")} ${ms("total")}`);
}

const dir = mkdtempSync(join(tmpdir(), "bun-bench-write-string-"));
const runtime =
typeof Bun !== "undefined" ? `bun ${Bun.version} (${Bun.revision.slice(0, 9)})` : `node ${process.version}`;
console.log(`# ${runtime}, ms, median of ${ROUNDS}`);
try {
for (const size of sizes) {
for (const [kind, make] of Object.entries(kinds)) {
const string = make(size * M);
console.log(
`${size} M characters of ${kind}`.padEnd(39) +
"in call".padStart(9) +
"stall".padStart(9) +
"total".padStart(9),
);
for (const [api, write] of Object.entries(apis)) {
const spaced = [];
for (let round = 0; round < ROUNDS; round++) {
const path = join(dir, `spaced-${round}`);
await sleep(150);
spaced.push(await time(write, path, string));
rmSync(path);
}
row(api, "spaced", spaced);
const path = join(dir, "back-to-back");
const backToBack = [];
await write(path, string);
for (let round = 0; round < ROUNDS; round++) backToBack.push(await time(write, path, string));
rmSync(path);
row(api, "back-to-back", backToBack);
}
}
}
} finally {
rmSync(dir, { recursive: true, force: true });
}
2 changes: 1 addition & 1 deletion src/bun_core/string/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ impl String {
/// atom, symbol or substring), so the value may be handed to one other
/// thread. Non-WTF tags are inert and always qualify.
#[inline]
pub(crate) fn is_thread_isolated(&self) -> bool {
pub fn is_thread_isolated(&self) -> bool {
self.tag != Tag::WTFStringImpl || self.as_wtf().is_thread_isolated()
}
/// What [`make_thread_shareable`] yields: isolated, pre-hashed and never
Expand Down
14 changes: 14 additions & 0 deletions src/runtime/node/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,20 @@ impl StringOrBuffer<'static> {
unsafe { ThreadIsolated::new(isolated) }
}

/// `str` for a work-pool job to read in place, or `Err` with the UTF-8 bytes of a string that it cannot lend.
pub(crate) fn lend_string(str: bun_core::String) -> Result<ThreadIsolated<Self>, Vec<u8>> {
// An atom belongs to its thread, and a substring keeps its whole base string alive.
if !str.is_thread_isolated() {
return Err(str.to_owned_slice());
}
let utf8 = str.into_utf8_with_string();
if !utf8.is_shared() {
return Err(utf8.into_utf8().into_vec());
}
// SAFETY: `ThreadIsolatedString` of an isolated string.
Ok(unsafe { ThreadIsolated::new(Self::ThreadIsolatedString(utf8)) })
}

#[inline]
pub(crate) fn from_js_with_encoding(
global: &JSGlobalObject,
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/server/server_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2067,7 +2067,7 @@ where
}

if let Some(body__) = init.fast_get(global, jsc::BuiltinName::Body)? {
match Blob::get::<true, false>(global, body__) {
match Blob::from_js_move(global, body__) {
Ok(new_blob) => body = BodyValue::Blob(new_blob),
Err(err) => {
return Ok(JSPromise::rejected_promise_with_caught_exception(
Expand Down
Loading
Loading