diff --git a/README.md b/README.md index 0da85aa..a7f6e8e 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ $ ls -lh ant - [Why Ant?](#why-ant) - [Installation](#installation) +- [Benchmarks](#benchmarks) - [Spec conformance](#spec-conformance) - [Building Ant](#building-ant) - [Security](#security) @@ -25,13 +26,13 @@ $ ls -lh ant ## Why Ant? -| | Ant | Node | Bun | Deno | -| ------------------- | ----------- | --------- | -------- | --------- | -| Binary size | **~9 MB** | ~120 MB | ~60 MB | ~90 MB | -| Cold start | **~3-5 ms** | ~30-50 ms | ~5-10 ms | ~20-30 ms | -| Engine | Ant Silver | V8 | JSC | V8 | -| JIT | ✓ | ✓ | ✓ | ✓ | -| WinterTC conformant | ✓ | partial | ✓ | ✓ | +| | Ant | Node | Bun | Deno | +| ------------------- | ---------- | ------- | ------ | ------ | +| Binary size | **~9 MB** | ~120 MB | ~60 MB | ~90 MB | +| Cold start | **~5 ms** | ~31 ms | ~13 ms | ~25 ms | +| Engine | Ant Silver | V8 | JSC | V8 | +| JIT | ✓ | ✓ | ✓ | ✓ | +| WinterTC conformant | ✓ | partial | ✓ | ✓ | Ant is designed for environments where size and startup time matter: serverless functions, edge computing, embedded systems, CLI tools, and anywhere you'd want JavaScript but can't afford a 50MB+ runtime. @@ -55,6 +56,43 @@ Ant targets the [WinterTC Minimum Common API](https://min-common-api.proposal.wi | js-zoo (overall) | **88%** | 1211/1368 passing | | test262 | ~50% | Improving, focus is on real-world coverage | +## Benchmarks + +### Cold start + +Measures the time to import [Hono](https://hono.dev), register routes, and exit. Each runtime loads he same `bench-coldstart.js` script from `examples/npm/hono/` that creates a Hono app with two routes, prints "ready", and calls `process.exit(0)`. No HTTP server is actually started, this isolates module resolution and initialization overhead. + +Measured with hyperfine (10 warmup runs, 100 timed runs): + +```bash +hyperfine --warmup 10 --runs 100 \ + 'ant examples/npm/hono/bench-coldstart.js' \ + 'node examples/npm/hono/bench-coldstart.js' \ + 'bun examples/npm/hono/bench-coldstart.js' \ + 'deno run --allow-read --allow-env examples/npm/hono/bench-coldstart.js' +``` + +| Runtime | Mean | Min | Max | Relative | +| ------- | ---------- | ------- | -------- | ------------ | +| **Ant** | **5.7 ms** | 5.0 ms | 7.3 ms | **1.00** | +| Bun | 12.8 ms | 11.6 ms | 16.4 ms | 2.24× slower | +| Deno | 24.8 ms | 22.2 ms | 29.4 ms | 4.32× slower | +| Node | 31.1 ms | 27.1 ms | 151.7 ms | 5.41× slower | + +
+Environment + +| Detail | Value | +| -------- | --------------------------------- | +| Hardware | Apple M4 Pro, 24 GB RAM, 14 cores | +| OS | macOS 15.7.5 (arm64) | +| Ant | 0.9.1 | +| Node | 25.9.0 | +| Bun | 1.3.13 | +| Deno | 2.7.12 | + +
+ ## Building Ant See [BUILDING.md](BUILDING.md) for instructions on how to build Ant from source and a list of supported platforms. diff --git a/examples/npm/hono/bench-coldstart.js b/examples/npm/hono/bench-coldstart.js new file mode 100644 index 0000000..c9d890a --- /dev/null +++ b/examples/npm/hono/bench-coldstart.js @@ -0,0 +1,10 @@ +import { Hono } from 'hono'; + +const app = new Hono(); + +app.get('/', c => c.text('hello hono!')); +app.get('/json', c => c.json({ message: 'hello' })); + +// signal that startup is complete, then exit +console.log('ready'); +process.exit(0); diff --git a/include/arena.h b/include/arena.h index 716d60e..13fe4d8 100644 --- a/include/arena.h +++ b/include/arena.h @@ -16,18 +16,6 @@ #include #endif -#define ANT_ARENA_MIN (32 * 1024) -#define ANT_ARENA_MAX (64ULL * 1024 * 1024 * 1024) - -#define ANT_ARENA_THRESHOLD (256ULL * 1024 * 1024) -#define ARENA_GROW_INCREMENT (8ULL * 1024 * 1024) -#define ANT_CLOSURE_ARENA_MAX (2ULL * 1024 * 1024 * 1024) - -// preferred base for mmap on platforms where the kernel may hand out -// addresses above the 47-bit NaN-boxing ceiling. We pick 0x100000000 -// inside the 47-bit range and above the typical text/data segments. -#define ANT_MMAP_HINT ((void *)0x100000000ULL) - typedef struct { uint8_t *base; size_t committed; @@ -39,6 +27,44 @@ typedef struct { void *free_list; } ant_fixed_arena_t; +#define ANT_ARENA_MIN (32 * 1024) +#define ANT_ARENA_MAX (64ULL * 1024 * 1024 * 1024) + +#define ANT_ARENA_THRESHOLD (256ULL * 1024 * 1024) +#define ARENA_GROW_INCREMENT (8ULL * 1024 * 1024) +#define ANT_CLOSURE_ARENA_MAX (2ULL * 1024 * 1024 * 1024) + +// the kernel can hand out mmap addresses above the 47-bit NaN-boxing ceiling. +// ant_mmap_low() probes the low VA range with MAP_FIXED_NOREPLACE +// before falling back to an unpinned mmap. only needed on Linux +#ifdef __linux__s + +#ifndef MAP_FIXED_NOREPLACE +#define MAP_FIXED_NOREPLACE 0x100000 +#endif + +#define ANT_MMAP_LOW_START 0x10000000000ULL +#define ANT_MMAP_LOW_END 0x7F0000000000ULL +#define ANT_MMAP_LOW_STEP 0x10000000000ULL + +static inline void *ant_mmap_low(size_t size, int prot, int extra_flags) { + int flags = MAP_PRIVATE | MAP_ANON | extra_flags; + + for ( + uintptr_t addr = ANT_MMAP_LOW_START; + addr + size <= ANT_MMAP_LOW_END; + addr += ANT_MMAP_LOW_STEP + ) { + void *p = mmap((void *)addr, size, prot, flags | MAP_FIXED_NOREPLACE, -1, 0); + if (p != MAP_FAILED) return p; + } + + void *p = mmap(NULL, size, prot, flags, -1, 0); + return (p == MAP_FAILED) ? NULL : p; +} + +#endif + #ifdef _WIN32 static inline void *ant_arena_reserve(size_t max_size) { @@ -70,14 +96,13 @@ static inline int ant_arena_decommit(void *base, size_t old_size, size_t new_siz #else static inline void *ant_arena_reserve(size_t max_size) { - void *p = mmap(ANT_MMAP_HINT, max_size, PROT_NONE, MAP_PRIVATE | MAP_ANON, -1, 0); - if (p == MAP_FAILED) return NULL; - if ((uintptr_t)p >> 47) { - munmap(p, max_size); - p = mmap(NULL, max_size, PROT_NONE, MAP_PRIVATE | MAP_ANON, -1, 0); - if (p == MAP_FAILED) return NULL; - } - return mantissa_chk(p, "mmap"); +#ifdef __linux__ + void *p = ant_mmap_low(max_size, PROT_NONE, 0); +#else + void *p = mmap(NULL, max_size, PROT_NONE, MAP_PRIVATE | MAP_ANON, -1, 0); + if (p == MAP_FAILED) p = NULL; +#endif + return p ? mantissa_chk(p, "mmap") : NULL; } static inline int ant_arena_commit(void *base, size_t old_size, size_t new_size) { @@ -105,14 +130,13 @@ static inline void ant_arena_free(void *base, size_t reserved_size) { } static inline void *ant_os_alloc(size_t size) { - void *p = mmap(ANT_MMAP_HINT, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); - if (p == MAP_FAILED) return NULL; - if ((uintptr_t)p >> 47) { - munmap(p, size); - p = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); - if (p == MAP_FAILED) return NULL; - } - return mantissa_chk(p, "mmap"); +#ifdef __linux__ + void *p = ant_mmap_low(size, PROT_READ | PROT_WRITE, 0); +#else + void *p = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); + if (p == MAP_FAILED) p = NULL; +#endif + return p ? mantissa_chk(p, "mmap") : NULL; } static inline int ant_arena_decommit(void *base, size_t old_size, size_t new_size) { diff --git a/meson.build b/meson.build index 7900f18..7fe3bb2 100644 --- a/meson.build +++ b/meson.build @@ -94,7 +94,7 @@ if host_machine.system() == 'windows' elif host_machine.system() == 'darwin' ant_link_args += ['-Wl,-stack_size,0x2000000'] else - ant_link_args += ['-Wl,-z,stacksize=134217728'] + ant_link_args += ['-Wl,-z,stack-size=134217728'] endif if get_option('linker_map') ant_linkmap = meson.project_build_root() + '/ant.linkmap'