Skip to content

Fix weak-key hash ephemeron semantics - #1357

Open
Tutez64 wants to merge 1 commit into
HaxeFoundation:masterfrom
Tutez64:fix/weakmap-ephemeron-values
Open

Fix weak-key hash ephemeron semantics#1357
Tutez64 wants to merge 1 commit into
HaxeFoundation:masterfrom
Tutez64:fix/weakmap-ephemeron-values

Conversation

@Tutez64

@Tutez64 Tutez64 commented Jul 25, 2026

Copy link
Copy Markdown

Problem

hxcpp currently marks every value stored in a weak-key hash during the normal
marking phase, regardless of whether its key is still reachable.

This does not implement ephemeron semantics. In particular, if a value directly
or indirectly references its own key, the following cycle becomes permanently
reachable:

WeakMap -> value -> weak key

The key is consequently never removed from the map, and the complete object
graph remains alive indefinitely. This can cause substantial heap growth and
increasing GC pause times in long-running applications.

Fix

This change treats weak-key hashes as ephemerons:

  • weak keys and their values are omitted from ordinary hash traversal;
  • after the strongly reachable graph and zombies have been marked, live weak
    hashes are scanned;
  • a value is marked only when its corresponding key is reachable;
  • newly marked values are processed using the regular parallel marker;
  • weak hashes are scanned to a fixed point because a value may make a key in
    another weak hash reachable.

Dead weak keys are then removed by the existing post-GC cleanup.

Tests

The hxcpp native test suite now covers:

  • collecting a dead key/value cycle after the first full GC;
  • retaining a value while its key remains alive;
  • propagating reachability through a chain of ephemerons until a fixed point.

Minimal reproduction

HxcppWeakMapEphemeronRepro.zip
It creates 4,000 unreachable keys.
Each mapped value references its key and owns a 64 KiB payload. One additional
key is intentionally retained as a correctness check.

Before this change:

baselineMiB=0.1
afterFirstGcMiB=250.8 entries=4001
afterSecondGcMiB=250.8 entries=4001
retainedDeltaMiB=250.7
FAIL: dead weak keys and their values are still retained

After this change:

baselineMiB=0.1
afterFirstGcMiB=0.2 entries=1
afterSecondGcMiB=0.2 entries=1
retainedDeltaMiB=0.1
PASS: only the live key and its value remain

Real-world validation

The issue was originally identified in a long-running Haxe/OpenFL application.
In comparable six-minute sessions, this change produced the following results:

Metric Before After
Peak live hxcpp heap 1,130 MiB 840 MiB
Final live hxcpp heap 1,000 MiB 687 MiB
Peak RSS 2,393 MiB 1,837 MiB
Median GC pause 95.7 ms 51.9 ms
p95 GC pause 131.2 ms 88.5 ms
Total instrumented GC time 3,455 ms 2,944 ms

Heap censuses also showed that object graphs from completed game levels were
released instead of accumulating.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant