Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 7 additions & 0 deletions docs/guides/ecosystem/systemd.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ The command attaches the capability to the `bun` binary itself. Replacing the bi

---

Two of systemd's resource-control and sandboxing options change how Bun runs:

- `LimitAS=` caps the address space of the service, like `ulimit -v`. Bun reserves more address space than the memory it uses, and it sizes those reservations to fit the limit. Set `LimitAS=` to 512M or more. To cap memory, use `MemoryMax=`.
- `MemoryDenyWriteExecute=yes` forbids memory that is writable and executable at the same time, and forbids making memory executable later. A JIT compiler needs one of the two, so Bun runs JavaScript and WebAssembly in the interpreter. Compute-heavy code runs several times slower.

---

With the service file configured, _enable_ the service. Once enabled, it starts automatically on reboot. Enabling the service requires `sudo` permissions.

```bash terminal icon="terminal"
Expand Down
2 changes: 1 addition & 1 deletion scripts/build/deps/mimalloc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import type { Dependency, DirectBuild } from "../source.ts";

const MIMALLOC_COMMIT = "eab09015a5850ae18fc43ccfaa5bbe8272992314";
const MIMALLOC_COMMIT = "3dd6c1ba4e2aff964076caa4a3d952d30ca88d3e";

export const mimalloc: Dependency = {
name: "mimalloc",
Expand Down
2 changes: 1 addition & 1 deletion scripts/build/deps/webkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* for local mode. Override via `--webkit-version=<hash>` to test a branch.
* From https://github.com/oven-sh/WebKit releases.
*/
export const WEBKIT_VERSION = "fb1167ebf2cb9edc1f6771a2c11771b024693ae0";
export const WEBKIT_VERSION = "autobuild-preview-pr-586-d4aa2237";

/**
* WebKit (JavaScriptCore) — the JS engine.
Expand Down
75 changes: 74 additions & 1 deletion test/js/bun/jsc/heapStats-mimalloc.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { heapStats } from "bun:jsc";
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, isASAN, isLinux, isMacOS, tempDir } from "harness";
import { bunEnv, bunExe, isASAN, isDebug, isLinux, isMacOS, tempDir } from "harness";

describe("heapStats() mimalloc integration", () => {
test("mimalloc aggregate stats are present", () => {
Expand Down Expand Up @@ -294,3 +294,76 @@ describe("heapStats() mimalloc integration", () => {
},
);
});

// RLIMIT_AS (`ulimit -v`, systemd `LimitAS=`) caps the address space a process may reserve, committed or not. At
// startup bun reserves a mimalloc arena, the JSC structure heap and the JIT pool. Each had a fixed size (1 GiB, up to
// 4 GiB, and 1 GiB on x64): under a limit of a couple of GiB the JIT pool did not fit and bun ran interpreter-only
// with no message, and at some limits the structure heap did not fit either and bun aborted at startup. Not ASAN or
// debug: the shadow memory and the unoptimized code do not fit these limits.
describe.skipIf(!isLinux || isASAN || isDebug)("under an address-space limit (ulimit -v)", () => {
const jitScript = /* js */ `
function hot(n) { let s = 0; for (let i = 0; i < n; i++) s = (s + i * 7) % 1000003; return s; }
for (let i = 0; i < 30; i++) hot(10000);
const { numberOfDFGCompiles } = require("bun:jsc");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
// numberOfDFGCompiles() reports 1000000 for any function while the JIT tiers are unavailable.
process.stdout.write(JSON.stringify({ jit: numberOfDFGCompiles(hot) !== 1000000 }));
`;
// Allocates untouched 16 MB buffers until the address space runs out and reports how far it got.
const heapScript = /* js */ `
const buffers = [];
let mb = 0;
try {
for (;;) { buffers.push(new Uint8Array(16 << 20)); mb += 16; }
} catch (e) {
process.stdout.write(JSON.stringify({ error: e.constructor.name, mb }));
}
`;
// 200_000 objects with distinct shapes: each takes a Structure from the structure heap.
const structureScript = /* js */ `
const objects = [];
for (let i = 0; i < 200_000; i++) objects.push({ ["k" + i]: i });
process.stdout.write(JSON.stringify({ structures: objects.length }));
`;

async function run(limitMB: number | undefined, script: string) {
const cmd =
limitMB === undefined
? [bunExe(), "-e", script]
: ["sh", "-c", `ulimit -v ${limitMB * 1024} && exec "$0" -e "$1"`, bunExe(), script];
Comment thread
claude[bot] marked this conversation as resolved.
Resolved
await using proc = Bun.spawn({ cmd, env: bunEnv, stdout: "pipe", stderr: "pipe" });
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
return { stdout, stderr, exitCode, signalCode: proc.signalCode };
}

test("without a limit the JIT is available", async () => {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
const { stdout, exitCode, signalCode } = await run(undefined, jitScript);
expect({ stdout, exitCode, signalCode }).toEqual({ stdout: `{"jit":true}`, exitCode: 0, signalCode: null });
});

test.concurrent.each([768, 1024, 1200, 1536, 2048, 2250])(
"ulimit -v %dM: bun starts and keeps the JIT",
async limitMB => {
const { stdout, stderr, exitCode, signalCode } = await run(limitMB, jitScript);
// stderr first: on failure it carries the crash banner or the allocation error.
expect({ stderr, stdout }).toEqual({ stderr: expect.any(String), stdout: `{"jit":true}` });
Comment thread
robobun marked this conversation as resolved.
Resolved
expect({ exitCode, signalCode }).toEqual({ exitCode: 0, signalCode: null });
},
);

// The startup reservations have to leave room for the program: under 1.5 GB it gets a catchable out-of-memory
// error, and not before it has allocated a quarter of the limit (about 600 MB is what fits).
test.concurrent("ulimit -v 1536M: the reservations leave room for the heap", async () => {
const { stdout, stderr, exitCode, signalCode } = await run(1536, heapScript);
expect(stderr).toBe("");
const { error, mb } = JSON.parse(stdout);
expect(error).toBe("RangeError");
expect(mb).toBeGreaterThanOrEqual(384);
expect({ exitCode, signalCode }).toEqual({ exitCode: 0, signalCode: null });
});

test.concurrent("ulimit -v 512M: the structure heap still holds 200k shapes", async () => {
const { stdout, stderr, exitCode, signalCode } = await run(512, structureScript);
expect({ stderr, stdout }).toEqual({ stderr: expect.any(String), stdout: `{"structures":200000}` });
expect({ exitCode, signalCode }).toEqual({ exitCode: 0, signalCode: null });
});
});
Loading