Bun

Bun v1.1.10


Jarred Sumner · 2024年5月24日

Bun v1.1.10 版本在此!此版本修正了 20 個錯誤。在 Windows 上,未快取的 bun install 速度快了 2 倍。fetch() 使用的記憶體最多減少了 2.8 倍。針對 bun install、原始碼地圖、Windows 可靠性改進和 Node.js 相容性改進進行了多項錯誤修正

我們正在舊金山招聘系統工程師,以打造 JavaScript 的未來!

先前的版本

  • v1.1.9 修正了 67 個錯誤(解決了 150 個 👍)。修正項目包括:bun install 中的工作區、bun build 中的原始碼地圖、IPv6 和 VPN 連線能力、載入 UNC 路徑、接合點、符號連結以及 Windows 上的 pnpm、fetch() 速度更快、新增 dns.prefetch() API、atob() 速度快了 8 倍、toString('base64url') 速度快了 5 倍、expect().toBeReturned() 匹配器、Node.js 相容性改進、Bun Shell 修正,以及更多錯誤修正。
  • v1.1.8 修正了 54 個錯誤(解決了 184 個 👍)。支援 process.on("uncaughtException")process.on("unhandledRejection")JSON.parse 速度更快、node:zlib 中的 Brotli 支援、Bun API 中的 [Symbol.dispose]、修正了 Windows 上許多當機問題,以及許多其他錯誤修正。
  • v1.1.0 Bundows。Windows 支援登場!

安裝 Bun

curl
npm
powershell
scoop
brew
docker
curl
curl -fsSL https://bun.dev.org.tw/install | bash
npm
npm install -g bun
powershell
powershell -c "irm bun.sh/install.ps1|iex"
scoop
scoop install bun
brew
brew tap oven-sh/bun
brew install bun
docker
docker pull oven/bun
docker run --rm --init --ulimit memlock=-1:-1 oven/bun

升級 Bun

bun upgrade

fetch 使用更少記憶體

fetch() 在判斷回應本文不再使用時變得更聰明,並更快釋放記憶體。

512 KB 回應(未消耗)

在以下程式碼中,Bun v1.1.10 使用的記憶體比 Bun v1.1.7 少 2.8 倍,比 Node v22 少 3.6 倍。

const server = process.argv.at(-1);
const fmt = new Intl.NumberFormat();
let total = 0;
const batch = 50;
const delay = 32;
while (total < 20_000) {
  for (let i = 0; i < batch; i++) {
    fetch(server);
  }
  await new Promise((r) => setTimeout(r, delay));
  total += batch;
}

console.log(
  "RSS",
  (process.memoryUsage.rss() / 1024 / 1024) | 0,
  "MB after",
  fmt.format((total += batch)) + " fetch() requests",
);

在 macOS arm64 機上執行 20,050 次請求後

執行階段記憶體用量
Bun v1.1.10166 MB
Bun v1.1.7467 MB
Node v22601 MB

512 KB 回應(消耗 arrayBuffer())

消耗本文也會更快釋放記憶體。在以下程式碼中,Bun v1.1.10 使用的記憶體比 Bun v1.1.7 少 1.6 倍,比 Node v22 少 10 倍。

const server = process.argv.at(-1);
const fmt = new Intl.NumberFormat();
let total = 0;
const batch = 50;
const delay = 32;
while (total < 20_000) {
  for (let i = 0; i < batch; i++) {
    fetch(server).then((r) => r.arrayBuffer());
  }
  await new Promise((r) => setTimeout(r, delay));
  total += batch;
}

console.log(
  "RSS",
  (process.memoryUsage.rss() / 1024 / 1024) | 0,
  "MB after",
  fmt.format((total += batch)) + " fetch() requests",
);

在 macOS arm64 機上執行 20,050 次請求後

執行階段記憶體用量
Bun v1.1.10285 MB
Bun v1.1.7469 MB
Node v221911 MB

非常感謝 @cirospaciari 的這項改進!

我們如何讓 fetch 使用更少記憶體

JavaScriptCore 的垃圾收集器讓原生程式碼可以持有 JavaScript 物件的弱參考和強參考。fetch() 特別複雜,因為它牽涉到

  • Promise,必須保持存活直到回應完成。
  • Response,必須至少保持存活直到標頭和狀態碼可用。
  • ReadableStream,如果讀取,必須在本文下載時保持存活。
  • 緩衝資料 - Response 可以將資料緩衝到 Uint8ArrayArrayBuffertextjsonformDatablob 中,這些資料必須在本文讀取時保持存活。

首先,這對 Promise 設定了硬性限制 - Promise 物件必須至少保持存活直到我們收到 HTTP 狀態碼和標頭。這表示它是 JSC::Strong 最簡單的做法是到此為止,並讓 Response 及其本文保持存活,直到 Response 完成且 HTTP 回應完全讀取。 這是我們在 Bun v1.1.9 和更早版本(此版本之前)中採用的方法。

一旦我們取得標頭和狀態碼,我們就可以釋放 Promise 的強參考。從那時起,我們需要追蹤 Response 物件的生命週期。如果正在讀取回應本文,我們只需要讓 Response 物件在 JavaScript 可觀察到的時間內保持存活。JavaScriptCore 的 JSC::Weak 讓我們可以將終結器附加到 JSC::JSCell(JavaScript 物件)。此終結器函式會在 JSC::JSCell 從 JavaScript 無法再存取時呼叫,這就是我們如何知道 Response 物件何時不再可供 JavaScript 觀察。

但是,我們需要持有 Response 物件的 JSC::Weak 值,但一旦終結器被呼叫,我們就無法存取 Response 物件本身 - JavaScript 物件在那時已經被釋放。因此,JSC::StrongJSC::Weak 的組合不足以讓我們完全做到這一點。我們需要繼續能夠在 Response 物件不再可供 JavaScript 存取後存取它,以便我們可以處理 ReadableStream 和緩衝資料。

為此,我們採用了常見的手動記憶體管理方法:參考計數。當 fetch 仍有待處理的緩衝或串流資料時,我們會增加 Zig 中 Response 物件的參考計數。當垃圾收集器呼叫 Response 的終結器時,它會減少其參考計數。如果該參考計數為零,則它會釋放 Response 本文和 Response 物件。

當垃圾收集器呼叫 fetch 的終結器,通知它 Response 物件已被收集時,我們現在知道本文是否將不再可存取,並且可以告訴 HTTP 用戶端忽略回應本文。

在 Windows 上,未快取的 bun install 速度快了 2 倍

在 Windows 上,bun install 在解析套件版本時速度提升了 2 倍。

PS C:\bun> hyperfine "bun install --ignore-scripts" "bun-1.1.8 install --ignore-scripts" --prepare="del /s /q bun.lockb && del /s /q C:\Users\window\.bun\install\cache" --warmup=1
Benchmark 1: bun install --ignore-scripts
  Time (mean ± σ):      1.343 s ±  0.398 s    [User: 0.321 s, System: 0.178 s]
  Range (min … max):    0.830 s …  1.861 s    10 runs

Benchmark 2: bun-1.1.8 install --ignore-scripts
  Time (mean ± σ):      3.997 s ±  0.204 s    [User: 0.264 s, System: 0.192 s]
  Range (min … max):    3.753 s …  4.409 s    10 runs

Summary
  bun install --ignore-scripts ran
    2.98 ± 0.89 times faster than bun-1.1.8 install --ignore-scripts

我們很快會撰寫一篇部落格文章,詳細說明此改進的技術細節。

已修正:v1.1.9 中 bun install 在 Windows 上掛起的迴歸

您知道 2009 年非阻塞 I/O 是一項新事物嗎?嗯,Windows 上的 bun 使用者體驗了非阻塞 I/O 之前的時代的縮小版。

當系統負載過重或網路不穩定時,bun install 有時會在解析套件版本時無限期掛起。這個問題在 Windows 上早就存在,但在 v1.1.9 中由於實作了 Happy Eyeballs 的子集而加劇。問題在於我們沒有在 Socket 已經連線之後才將 TCP 用戶端連線設為非阻塞。這也會在透過 fetchBun.connect()new WebSocket() 連線時造成阻塞 I/O。它不會影響 Bun.serve()Bun.listen()

與 v1.1.8(迴歸之前)相比,修正此問題也使 Windows 上 bun install 的效能提升了 20%。

已修正:ENOENT 解析 package.json 錯誤

Bun v1.1.9 中引入的迴歸已修正,在工作區套件內的子資料夾中執行 bun add <package-name> 在某些情況下會失敗,感謝 @dylan-conway

已修正:使用工作區修正 package-lock.json 遷移

Bun 支援從 package-lock.json -> bun.lockb 自動遷移,但當該 package-lock.json 包含工作區套件時,可能會發生錯誤。這已修正,感謝 @dylan-conway

已修正:修改「overrides」或「resolutions」後當機

在修改 package.json 中的 overridesresolutions 欄位後,下一次 bun install 可能會發生的當機問題已修正,感謝 @gvilums

新增:bun:test 中的 setDefaultTimeout

bun:test 現在支援 setDefaultTimeout 函式,以修改目前範圍或模組中測試的預設逾時。

import { test, setDefaultTimeout } from "bun:test";

// Timeout after 10 milliseconds:
setDefaultTimeout(10);

test("timeout", async () => {
  await Bun.sleep(9999999);
});

先前,您必須個別設定每個測試的逾時

import { test } from "bun:test";

test("timeout", async () => {
  await Bun.sleep(9999999);
}, 10);

您仍然可以個別設定每個測試的逾時。

為了與 Jest 相容,我們也在 bun:test 中實作了 jest.setTimeout

import { test, jest } from "bun:test";

jest.setTimeout(10);

test("timeout", async () => {
  await Bun.sleep(9999999);
});

我們選擇將 bun:test 的版本命名為 "setDefaultTimeout" 而不是 "setTimeout",以避免與全域 setTimeout 計時器函式混淆。

感謝 @dylan-conway 的這項功能!

已修正:macOS 上衍生程序中可能發生的掛起

已修正衍生程序在從 stdout、stderr 或 stdin 讀取時可能無限期掛起的錯誤。

為什麼會發生這種情況?

在 macOS 上,使用 MSG_NOWAIT 旗標和阻塞 Socket 呼叫 send(2) 仍然會阻塞。在 Linux 上,MSG_NOWAIT 會使其不阻塞。因此,如果您設法將大量資料傳送到 Socket,則父程序可能會阻塞。

與管道不同,將 Socket 的一端標記為非阻塞對另一端是不可觀察的。

為了驗證這一點,我們可以編譯以下 C 程式

#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main() {
  // Sleep for 10 seconds so that the other process has time to mark stdout as
  // non-blocking
  sleep(10);

  // Check if stdout is blocking or non-blocking
  int stdout_flags = fcntl(fileno(stdout), F_GETFL);
  if (stdout_flags & O_NONBLOCK) {
    printf("stdout is non-blocking\n");
  } else {
    printf("stdout is blocking\n");
  }

  // Check if stderr is blocking or non-blocking
  int stderr_flags = fcntl(fileno(stderr), F_GETFL);
  if (stderr_flags & O_NONBLOCK) {
    printf("stderr is non-blocking\n");
  } else {
    printf("stderr is blocking\n");
  }

  // Check if stdin is blocking or non-blocking
  int stdin_flags = fcntl(fileno(stdin), F_GETFL);
  if (stdin_flags & O_NONBLOCK) {
    printf("stdin is non-blocking\n");
  } else {
    printf("stdin is blocking\n");
  }

  return 0;
}

然後在 Bun 中衍生它

import { spawn, $ } from "bun";

await $`cc -o a.out a.c`;

const { stdout } = spawn({
  cmd: ["a.out"],
  stdout: "pipe",
  stderr: "pipe",
  stdin: "pipe",
});

console.log(await new Response(stdout).text());

輸出將為

stdout is blocking
stderr is blocking
stdin is blocking

此輸出是正確的,因為阻塞 stdout、stderr 和 stdin 對於許多 UNIX 程式正常運作是必要的。例如,如果 stdout 是非阻塞並傳回 EAGAIN,則 cat 將無法正常運作。我們希望 Bun 沒有阻塞 stdout、stderr 和 stdin,但依賴阻塞 stdout、stderr 和 stdin 的程式繼續正常運作非常重要。

因此,我們可以安全地將 Socket 標記為非阻塞,而不會影響另一端。

為了防止未來發生迴歸,我們新增了一個迴歸測試,在正確的條件下將大量資料寫入和讀取到另一個程序,以導致此錯誤,並驗證此測試先前在 macOS 上失敗。

已修正:bun build 中使用 --splitting 時,原始碼地圖 JSON 無效

已修正一個錯誤,在某些情況下,當在 bun build 中使用 --splitting 時,原始碼地圖不會產生正確的 JSON 物件,感謝 @paperclover。此錯誤是由於錯誤地聯接來自不同檔案的原始碼地圖字串所致。為了防止未來發生迴歸,我們改進了測試覆蓋率以驗證原始碼地圖是否有效。

Bun 中原始碼地圖的更多改進即將推出。

Windows 修正

已修正:Worker 結束時延遲 1 秒

一個事件迴圈錯誤導致 Worker 在 Windows 上結束時延遲 1 秒。這已修正,感謝 @gvilums

bun build 中匯入路徑中的反斜線

已修正一個錯誤,其中未逸出的反斜線可能會插入到 bun build 的匯入規範中,感謝 @paperclover

已修正:bun --watch 未終止 Bun 執行個體

先前,當使用 bun --watch 時,Bun 的執行個體可能會在退出後繼續執行。 此問題已修正,感謝 @paperclover

Node.js 相容性改進

已修正:串流錯誤中的 EventEmitter

已修正一個錯誤,其中在 emit 呼叫中移除的事件接聽器會被略過。此錯誤影響了 node:stream,但不影響 node:events。感謝 @gvilums 修正此問題。

此錯誤影響了關閉連線時的 Postgres.js。

當機報告上傳

在 macOS 和 Linux 上,當發生當機時,Bun 會嘗試自動將 bun.report 連結上傳到我們的當機報告服務。這將有助於我們更快地修正錯誤。您可以透過設定 BUN_CRASH_REPORTER_URL="" 來停用此功能。請繼續在 GitHub 上向我們報告當機問題,因為這確實有助於我們修正錯誤。深入瞭解 bun.report

在 Linux 上,這在 Bun 的 Canary 組建中已啟用,但在發行組建中未啟用。

感謝 9 位貢獻者!