Skip to content

Memory not free in benchmark #5476

Description

@zaaack

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions