What happened
RSS memory grows monotonically throughout execution and never shrinks. After running a series of short-lived allocation benchmarks (arrays, Maps, Sets, strings, JSON), Perry's RSS reached 1836 MB while actual heap usage was only 164 MB. Node.js running the identical code stayed at 347 MB RSS / 122 MB heap.
What you expected
RSS should stabilize near the live-set size once temporary objects are collected.
Minimal reproduction
const ITERATIONS = 100;
const N = 100_000;
function bench(name: string, fn: () => void, iters: number) {
for (let i = 0; i < Math.min(10, Math.floor(iters / 10)); i++) fn();
const start = performance.now();
for (let i = 0; i < iters; i++) fn();
const mem = process.memoryUsage();
console.log(`[${name}] ${(performance.now() - start).toFixed(0)}ms | rss=${(mem.rss / 1024 / 1024).toFixed(0)}MB | heap=${(mem.heapUsed / 1024 / 1024).toFixed(0)}MB`);
}
bench('array-sort-100k', () => {
const arr = new Array<number>(N);
for (let i = 0; i < N; i++) arr[i] = Math.random();
arr.sort((a, b) => a - b);
}, ITERATIONS);
bench('filter-map-reduce-100k', () => {
const arr = new Array<number>(N);
for (let i = 0; i < N; i++) arr[i] = i;
arr.filter(x => x % 2 === 0).map(x => x * x).reduce((acc, x) => acc + x, 0);
}, ITERATIONS * 2);
bench('map-set-get-100k', () => {
const m = new Map<number, number>();
for (let i = 0; i < N; i++) m.set(i, i * 2);
let sum = 0;
for (let i = 0; i < N; i++) sum += m.get(i) ?? 0;
return sum;
}, ITERATIONS * 2);
bench('set-intersection-50k', () => {
const half = N / 2;
const a = new Set<number>();
const b = new Set<number>();
for (let i = 0; i < half; i++) { a.add(i); b.add(i + half); }
const intersection = new Set<number>();
for (const x of a) { if (b.has(x)) intersection.add(x); }
return intersection.size;
}, ITERATIONS);
bench('string-concat-10k', () => {
let s = '';
for (let i = 0; i < 10_000; i++) s += 'a' + i.toString();
}, ITERATIONS);
const obj = { a: 1, b: 'foo', c: [1, 2, 3], d: { x: true } };
const jsonStr = JSON.stringify(obj);
bench('json-stringify-100', () => { for (let i = 0; i < 100; i++) JSON.stringify(obj); }, ITERATIONS * 10);
bench('json-parse-100', () => { for (let i = 0; i < 100; i++) JSON.parse(jsonStr); }, ITERATIONS * 10);
console.log('\n--- Done ---');
Command:
perry compile bench.ts && ./bench
Environment
- Perry version: 0.5.1180
- Host OS: Ubuntu 24.04.4 LTS (WSL2), kernel 6.6.87.2-microsoft-standard-WSL2
- Target: native (linux x86_64)
- Installed via: npm
Diagnostic output
Perry output:
[array-sort-100k] 3461ms | rss=102MB | heap=1MB
[filter-map-reduce-100k] 2748ms | rss=568MB | heap=4MB
[map-set-get-100k] 4308ms | rss=1343MB | heap=4MB
[set-intersection-50k] 1149ms | rss=1663MB | heap=4MB
[string-concat-10k] 161ms | rss=1749MB | heap=80MB
[json-stringify-100] 1189ms | rss=1799MB | heap=131MB
[json-parse-100] 374ms | rss=1836MB | heap=164MB
Node.js (v24.9.0) output with the same code:
[array-sort-100k] 3831ms | rss=281MB | heap=81MB
[filter-map-reduce-100k] 651ms | rss=323MB | heap=108MB
[map-set-get-100k] 2136ms | rss=361MB | heap=99MB
[set-intersection-50k] 812ms | rss=390MB | heap=147MB
[string-concat-10k] 22ms | rss=347MB | heap=100MB
[json-stringify-100] 27ms | rss=347MB | heap=106MB
[json-parse-100] 52ms | rss=347MB | heap=122MB
Heap delta per benchmark on Perry:
| Benchmark |
heap_delta |
RSS growth |
| array-sort-100k |
1 MB |
+95 MB |
| filter-map-reduce-100k |
4 MB |
+466 MB |
| map-set-get-100k |
4 MB |
+775 MB |
| set-intersection-50k |
4 MB |
+320 MB |
| string-concat-10k |
80 MB |
+86 MB |
| json-stringify-100 |
131 MB |
+50 MB |
| json-parse-100 |
164 MB |
+37 MB |
Heap delta is small relative to RSS growth. The GC is collecting object references, but arena blocks are not being returned to the OS.
globalThis.gc is undefined at Perry runtime, so manual GC triggers are not possible from user code.
Anything else
Tested with the following environment variables — all produce the same RSS growth pattern:
PERRY_GC_FORCE_EVACUATE=1
PERRY_GEN_GC=0
PERRY_WRITE_BARRIERS=0 (compile + runtime)
- Reducing
ITERATIONS from 100 to 10
Workaround: prlimit --as=4G to prevent the process from OOMing the host.
What happened
RSS memory grows monotonically throughout execution and never shrinks. After running a series of short-lived allocation benchmarks (arrays, Maps, Sets, strings, JSON), Perry's RSS reached 1836 MB while actual heap usage was only 164 MB. Node.js running the identical code stayed at 347 MB RSS / 122 MB heap.
What you expected
RSS should stabilize near the live-set size once temporary objects are collected.
Minimal reproduction
Command:
perry compile bench.ts && ./benchEnvironment
Diagnostic output
Perry output:
Node.js (v24.9.0) output with the same code:
Heap delta per benchmark on Perry:
Heap delta is small relative to RSS growth. The GC is collecting object references, but arena blocks are not being returned to the OS.
globalThis.gcis undefined at Perry runtime, so manual GC triggers are not possible from user code.Anything else
Tested with the following environment variables — all produce the same RSS growth pattern:
PERRY_GC_FORCE_EVACUATE=1PERRY_GEN_GC=0PERRY_WRITE_BARRIERS=0(compile + runtime)ITERATIONSfrom 100 to 10Workaround:
prlimit --as=4Gto prevent the process from OOMing the host.