Skip to content

Releases: sdogruyol/gcry

v0.18.0

Choose a tag to compare

@sdogruyol sdogruyol released this 04 Aug 19:34
be46a18

Product release for upstream Crystal ≥ 1.21 — no compiler fork.

  • Finalizer fix + Linux retain=0 → fat-app RSS ~1–1.6× (Linux); Darwin acik ~90% @ ~0.63×
  • Darwin Kemal tip ~84% /json @ ~1.01×
  • Linux Kemal headline still v0.16 carry (~87% @ ~0.80×)
  • Stack maps included but dormant (GCRY_PRECISE_STACK off)
  • Opt-in GCRY_TIGHT_GROW (not default)
    Changed: Linux empty-chunk/large-cache retain default 0.
    Escape: GCRY_EMPTY_CHUNK_RETAIN / GCRY_LARGE_CACHE.
    See CHANGELOG.md.

v0.17.0

Choose a tag to compare

@sdogruyol sdogruyol released this 02 Aug 11:52
c41fd56

Darwin Kemal re-cut (first since v0.13) + Parallel TLAB-off + lazy sweep as a supported opt-in (~79% /json).

  • macOS Kemal: /json ~84% @ ~0.93× RSS
  • Linux Kemal: carry v0.16 (~87% @ ~0.80×)
  • Parallel opt-in: EC>1 + TLAB off + lazy (~79%); not default
  • Fat-app: Linux ~90% / ~3.43×; Darwin ~71% / ~18×
  • CI: hardened RSS leak gate (post-warmup sampling)
    See CHANGELOG and docs/PERF-macos.md.

v0.16.0

Choose a tag to compare

@sdogruyol sdogruyol released this 01 Aug 08:08
9e97a5b

EC1 thr recovery after Parallel-era STW / scrub / counter fallout. Supported
path remains EC parallelism 1, GCRY_TLAB off (Parallel+TLAB stays
experimental — FINDINGS only, not folded into PERF).

Performance

  • Linux Kemal (same-host median-of-3, wrk -c 100 -d 30, scrub on): /json
    ~87% of Boehm @ ~0.80× post-GC RSS; / ~82% @ ~0.79×. Session
    bench/log/linux/2026-08-01-093130/ (cb4d7f2; idle / from slash-recut/).
    Fair Boehm ~40k baseline. See docs/PERF.md (Linux).
  • EC1 thr levers (Boehm ~40k fair): restore v0.15 parked-fiber scrub on EC1
    (4 KiB blind clear; Parallel keeps 512 B + clear_range_safe). Tip with
    512 B + safe retained ~4× more live_objects than bebedae. EC1 alloc/free
    counters use plain get/set (heap_counters_atomic only when
    EC_PARALLELISM>1) — avoid LOCK XADD/CAS on the hot path.
  • EC1 sweep pause: STW live_objects / free_bytes updates no longer
    CAS-loop per dead object. Empty dormant/munmap freelist cleanup batches
    into one rebuild_size_class_freelist per size class. Dormant post-STW
    flush early-outs when dormant_chunk_bytes == 0.
  • EC>1 thr gap (experimental): auto-collect trylock-or-skip on
    @post_stw (no waiter pile-up; wait_total ~11s/20s → ~0). Default major
    threshold 64 MiB when EC_PARALLELISM>1 (GCRY_THRESHOLD still wins;
    EC1 stays 32 MiB). Same-host re-cut: gcry EC4 /json ~68% of Boehm EC4
    @ ~53k abs (was ~52% @ ~36k). Long soak 100/100 soft=0 hard=0
    (2026-07-31-ec4-soak-100-post-thr). No PERF.md fold-in. See FINDINGS.
  • Parallel empty-chunk reclaim opt-in: default stays off under EC>1 (thr).
    GCRY_PARALLEL_DORMANT=1 DONTNEEDs empty chunks (RSS ~3× better, thr ~25%
    down on Kemal EC4). GCRY_PARALLEL_RELEASE=1 adds munmap excess (hung in
    A/B). EC1 dormant+munmap unchanged. See FINDINGS RSS A/B.
  • EC>1 alloc-path A/B: GCRY_TLAB=1 @ EC4 still ~½ of TLAB-off thr (soft 0
    — keep opt-in). @alloc_lock as pthread_mutex deadlocks under STW
    (collections=0) — rejected; stay on Crystal::SpinLock. Fold
    note_alloc_bytes into the freelist lock (one acquire per small alloc /
    TLAB hit). Session 2026-07-31-ec4-alloc-thr-ab. No PERF.md fold-in.
    See FINDINGS.
  • Atomic alloc counters: bytes_since_gc / live_objects / free_bytes /
    etc. are Atomic so TLAB hits need no @alloc_lock for accounting. EC4
    TLAB-off thr unchanged (~51k); TLAB-on still ~52% of off. Session
    2026-07-31-ec4-atomic-counters. No PERF.md fold-in. See FINDINGS.
  • Per-size-class freelist SpinLocks: TLAB-off small alloc/free lock only
    that size class (not global @alloc_lock). Large + TLAB table/refill keep
    @alloc_lock (per-class refill hurt TLAB-on via @index_lock×find_block).
    Quiet EC4 /json ~55k (was ~51k). Session
    2026-07-31-ec4-sizeclass-locks. No PERF.md fold-in. See FINDINGS.

Fixed

  • EC1 STW stack scan thr regression (Parallel fallout): process-STW full
    fiber/pthread scans added for EC>1 mid-swap were also applied on EC1
    (main+SYSMON). Every Thread root fiber is named "main", so SYSMON hit a
    full pthread map scan (phase_stacks 0.02→3ms; Kemal /json 86%→80%
    Boehm). Restore cheap SP/stack_top other-thread scans when
    !multi_mutator_threads?; keep aggressive Parallel path. Limit
    foreign-SP scrub skip to Parallel only. Sessions 2026-07-31-164302
    (regress), 2026-07-31-173530 (fix); final cut above.

  • Parallel @suppress_collect race: plain Int +=/-= under concurrent
    realloc lost decrements so suppress stuck high (≈4607) and auto-collect
    never ran (collections=0, thr collapsed). Use Atomic(Int32). Exposed when
    alloc counters left @alloc_lock (shorter critical section). See FINDINGS.

  • chunk_containing lock during post-STW: skipped @index_lock whenever
    @collecting (not only @world_stopped). Flush keeps @collecting after
    start_world, so Parallel mutators index_insert while peers realloc
    unlocked → false owns_user_pointer? (pointer is not a gcry allocation on
    String::Builder). Lock skip only under true STW. Soft errors 0/60 after
    empty-chunk gate (was 2–3/60). See FINDINGS.

  • Parallel empty-chunk release off: under multi-mutator STW, skip empty-chunk
    munmap even when release_empty_chunks is on (EC1 unchanged). Residual
    mark-miss × post-STW munmap surfaced as Kemal /json soft
    pointer is not a gcry allocation (22/40 → 3/40 with the gate; hard
    deaths 0/40). GCRY_STW_STACK_LAG env for LAG A/B (default 512 KiB). See
    FINDINGS mark-miss triage.

  • EC1 stw_sp_clamp counters: idle/stack_top other-thread scan now
    increments sp_clamp_fallbacks (missed after cheap-scan restore; aarch64 /
    Darwin CI samples/stw_sp_clamp saw hits=0 fallbacks=0).

  • pattern_fuzz Stride CI floor: raise Stride p99/max vs-baseline limit
    20→80× after EC1 4 KiB parked-fiber scrub (quiet ~11×; GHA crystal-latest
    hit ~45–57×).

  • No live TLAB steal: steal_from_other_tlabs could null another thread's freelist head while that thread was in lock-free tlab_alloc_small (TOCTOU dual-alloc). Removed cross-TLAB steal; idle freelists return via STW flush_all_tlabs. @tlab_steals stays 0 (metric reserved for a future CAS steal).

  • FREE-claim × minor: stack/thread FREE-claim cleared FREE before the minor/old filter, so an old freelist node became USED-unmarked and scrub dropped it. Skip claim entirely for old nodes during minor (minor never munmaps old chunks); nursery nodes still claim+mark.

  • Parallel worker STW stack scan: scan_other_thread_stacks used max(stack_top, sp) for running fibers; stale stack_top above hardware SP skipped live frames, so Parallel+TLAB in-flight mallocs were swept (pin saw FREE). Prefer suspend SP (+ x86_64 red zone), mark saved GP registers from the suspend ucontext, and with TLAB scan the full fiber stack (SP/greg alone still flaked under Parallel>2). CI: stw_mt_property_test --tlab --nursery mixes minors.

  • TLAB FREE-claim chain mark: stack/thread FREE-claim only marked the current freelist user; TLAB batch tails reachable via next_free stayed unmarked FREE, so empty-chunk release munmapped them and tlab_alloc_small SEGVd in BlockHeader.free? (Kemal GCRY_TLAB=1 @ EC1). Claim now marks the next_free chain (keep FREE on tails); abandon TLAB heads that fail find_block.

  • Parallel mutator heap-index races (partial): under EC_PARALLELISM>1, chunk_containing / last-chunk cache raced index_insert (false owns_user_pointer? / corruption). Added @index_lock; with_alloc_lock always locks (was a no-op when TLAB off); ensure_tlabs boots under @alloc_lock. Process-STW other-thread fiber stacks always full-scan. Kemal EC>1 HTTP still fails — see FINDINGS.

  • TLAB per-slot freelist locks: Parallel dual-alloc on lock-free TLAB heads (ec_alloc_stress double-free / not a gcry allocation). Per-slot Crystal::SpinLock (StaticArray — no GC malloc under @alloc_lock at boot). STW flush_all_tlabs must not take slot locks (suspended mutator may hold them). Refill always re-claims under the slot lock. Kemal EC>1 still open.

  • STW running-fiber scan: scan_all_fiber_roots skipped fiber.running?, relying on thread.@current_fiber; under Parallel that TLS can be briefly nil so stacks were missed. Under process STW, scan running fiber stacks too; if current_fiber is nil, fall back to pthread stack bounds + greg.

  • STW × ExecutionContext deadlock (GCRY_STRESS): signal-suspending SYSMON deadlocks (fiber yield wait, or lost SIG_RESUME leaving sigsuspend forever). Fix: skip SIGPWR for the Monitor; cooperative STW via @world_stopped barriers in allocate / lock_read; busy-wait @suspended for other threads (no yield_current); hold Thread.lock for stop→start; harden resume handshake; forbid process collect on SYSMON so the Monitor cannot STW-suspend the mutator.

  • TLAB@EC1 measured: correctness OK (Kemal 20/20 default + thr=32KiB; STW MT --tlab). /json thr ~71–77% of TLAB-off on same host — keep opt-in (GCRY_TLAB=1), not an EC1 default. Hit-path find_block dominates; stripping it SEGVs. See FINDINGS.

  • EC>1 thr vs Boehm (measured): Kemal EC4 TLAB-off /json ~23% of Boehm EC4 and ~0.52× gcry EC1 (session 2026-07-31-100844-ec-parallel-thr). Correctness quieter; Parallel still anti-scales — experimental.

  • Multi-mutator STW stack LAG: full guard→bottom on every parked fiber dominated EC4 phase_roots (~100ms+/collect). Prefer suspend SP−red_zone when present; otherwise scan from stack_top − 512KiB (not full guard). Same-host A/B /json median-of-5: LAG ~30k vs stw_full ~16k (~1.9×); EC4 soak 30×8s 0/30. Quiet re-cut vs Boehm: EC4 /json ~37% Boehm EC4 and ~0.87× gcry EC1 (was ~23% / ~0.52×). GCRY_TLAB=1 @ EC4: soak 3/20, thr not above good TLAB-off — keep opt-in. See FINDINGS.

  • EC4 post-STW queue: SpinLock wait on @post_stw burned ~8–11s/20s of worker time under Parallel HTTP. Switch to embedded pthread_mutex; auto-collect coalesce when a peer already cleared the debt; pause stats exclude queue wait. EC4 /json ~40k med (d=20) + soak 20/20 (was ~22k + crash outliers). Quiet d=30 re-cut vs Boehm: EC4 /json ~52% Boehm EC4 and ~1.17× gcry EC1 (was ~23% / ~0.52× pre-LAG). Long soak 96/100 (4× SEGV/MARK_MISS). See FINDINGS.

  • Post-STW flush keeps @collecting + @suppress_collect: clearing @collecting before flush allowed stress/auto re-entry while still holding @post_stw_lock (non-recursive SpinLock). Hold collecting through flush.

  • realloc suppress-collect + Boehm-like thread stacks: growing realloc sets @suppress_collect around the fresh allocate so a mark miss cannot free-the...

Read more

v0.15.0

Choose a tag to compare

@sdogruyol sdogruyol released this 29 Jul 16:01
e520f00

Correctness release: process-STW × TLAB freelist UAF class fixed and CI-gated;
process-STW MT property harness; acikturkiye Linux re-cut measured; shard RSS
dead-end defaults documented. Supported path remains EC parallelism 1,
GCRY_TLAB off (Parallel+TLAB stays experimental).

Performance

  • Linux Kemal (same-host median-of-3, wrk -c 100 -d 30, scrub on): /json ~86% of Boehm @ ~0.77× post-GC RSS; / ~86% @ ~0.76×. Session bench/log/linux/2026-07-29-151144/ (bebedae). Collector defaults unchanged vs 0.14 — thr within host noise of the v0.14 ~89% cut. See docs/PERF.md (Linux).
  • acikturkiye Linux re-cut (measured): /api/v1/ ~90% of Boehm thr @ ~2.54× post-GC RSS (median-of-3, wrk -c 100 -d 30, scrub on). Session bench/log/linux/2026-07-29-112202/ (9decd01). Replaces the v0.14.0 ~93% / ~2.65× estimate. See docs/ACIKTURKIYE.md.
  • Shard RSS A/B (defaults unchanged): same-host cuts rejected as defaults — Linux HOLED GCRY_PAGE_DONTNEED, process-default curated HTTP::Headers::Key Hash layout, collect-time mutator clear_stack, Linux 1 MiB large-cache floor. Keep fiber scrub, Linux 4 MiB large-cache, HOLED opt-in; Headers layout stays app-side / GCRY_AUTO_LAYOUTS. See docs/ACIKTURKIYE.md “Don’t bother”.

Fixed

  • Explicit-root list × process STW race: add_root/delete_root could run concurrently with stop_world, freezing a mutator mid-list splice so @roots.each walked a freed/next-corrupt RootNode (SEGV at run_collection during stw_mt_property_test). Serialize mutations with @roots_lock acquired before STW; collector may mutate without the lock while @world_stopped.
  • Parked-fiber scrub on thinly mapped stacks: Cap wipe to the same 512 B fiber path as clear_stack and zero only readable pages via Roots.clear_range_safe (defense in depth; Crystal fiber stacks grow on demand).
  • TLAB + Parallel under process STW: mid-tlab_alloc_small STW could leave FREE freelist nodes only reachable from mutator stacks; mark ignored FREE, then empty-chunk release munmapped them (and unlink_freelist_range could coerce USED→FREE). Fix: claim FREE stack/thread roots when TLAB+STW (clear FREE but keep next_free so scrub can walk the chain — set_used was severing freelists → OOM), freelist scrub after flush/mark (TLAB-only), flush only FREE nodes, TLAB epoch + detach-before-claim (no dual-alloc after flush), no nested collect under @alloc_lock (deadlock), unlock-and-collect retry on refill miss, steal stranded TLAB freelists, skip nil Thread#current_fiber under Parallel. CI gates stw_mt_property_test --tlab --workers=2,4.

Added

  • Process-GC STW MT property harness: bench/stw_mt_property_test.cr (-Dgc_none) runs Parallel allocator workers while the default EC pins roots (ACK handshake) and GC.collects under real STW. Closes the gap left by library-heap mt_property_test (stop_the_world=false). CI gates --workers=2,4 and --tlab --workers=2,4. (make stw-mt-property-test)

Changed

  • Docs / knobs: Linux HOLED page release documented as opt-in (post-STW; not “STW-heavy”). Large-cache defaults clarified (Linux process 4 MiB, Darwin 1 MiB). Darwin GCRY_DISABLE_PAGE_RELEASE=1 / GCRY_DISABLE_MADVISE=1 explicitly clear madvise_free_pages.

v0.14.0

Choose a tag to compare

@sdogruyol sdogruyol released this 29 Jul 04:20

Trust and tooling release: industry-style test suite, debug observability, and a
measured Linux Kemal re-cut. Collector throughput unchanged; Kemal post-GC RSS
now measured (not estimated).

Performance

  • Linux Kemal (same-host median-of-3, wrk -c 100 -d 30, scrub on): /json ~89% of Boehm @ ~0.79× post-GC RSS; / ~89% @ ~0.78×. Session bench/log/linux/2026-07-29-035426/. See docs/PERF.md (Linux). Fat-app (acikturkiye) not re-cut — still ~93% thr / ~2.65× RSS est. (ACIKTURKIYE.md).

Added

  • Debug invariant checker (GCRY_DEBUG_INVARIANTS=1): validates heap invariants at runtime -- live_objects counter accuracy, freelist cycle/consistency checks, chunk index integrity, and block overlap detection. Hooks into malloc, free, and collect. Diagnostics use write(2) / no managed-heap alloc (not a claim that GC is async-signal-safe). -Dgcry_invariant_abort for core dumps. Exposed Heap#each_chunk, #freelist_for, #nursery_freelist_for for the checker. CI runs invariants on every PR. (spec/invariant_spec.cr, make invariants, CI Debug invariants step.)
  • Coverage infrastructure: spec/all_specs.cr entrypoint for kcov (DWARF-based line/branch coverage). ci/coverage.sh wrapper runs kcov + crystal tool unreachable + crystal tool macro_code_coverage. make coverage / coverage-kcov / coverage-unreachable / coverage-macro targets. CI coverage job builds the spec binary, installs kcov from Debian, and uploads the report. (ci/coverage.sh, Makefile, .github/workflows/ci.yml)
  • Memory safety CI: make asan builds and runs specs with AddressSanitizer (-Dasan). make valgrind-samples runs samples under Valgrind memcheck (--leak-check=full). CI asan and valgrind jobs on every PR. (Makefile, .github/workflows/ci.yml)
  • Deterministic replay fuzzing: bench/fuzz.cr rewritten with --seed=, --seconds=, --log=, and --replay= flags. Fuzz logs every operation to a replayable log file (opcode + args). Replay mode reads the log and replays the exact sequence of heap operations. Op 9 (spawn + Channel) excluded from logs as non-deterministic Crystal runtime. CI runs fuzz + replay on every PR. (make fuzz-replay FUZZ_LOG=path, CI Fuzz with log + replay step.)
  • Property-based testing: bench/property_test.cr -- random alloc/free/collect sequences with deep heap invariant verification: live_objects counter accuracy (reported == walked count), heap_size == sum of chunk mapped_bytes, freelist consistency, and per-node live? assertion. 100k iterations in ~8s. (make property-test, CI Property test step.)
  • Layout property test: bench/layout_property_test.cr -- 5 self-contained sub-tests verifying precise scan offset correctness, conservative fallback, leaf layout (scan_cap=0), noscan offset keep-alive semantics, and scan_cap limiting. Runs 10k iterations in ~2.5s. (make layout-property-test, CI Layout property test step.)
  • MT property test: bench/mt_property_test.cr -- concurrent allocation via fiber workers (2, 4, 8) with periodic collect; verifies no objects lost under concurrent alloc, live_objects counter accuracy after TLAB flush, and parallel mark (workers=2) produces the same live set as serial mark (workers=1). 500 iterations × 3 worker counts in ~2.4s. (make mt-property-test, CI MT property test step.)
  • 24-hour soak test: bench/soak.cr -- sustained load with alloc storm (~1000 obj/s), periodic collect (1 Hz), fiber spawn (10 Hz), finalizer load (100 obj/s), WeakRef via disappearing links (10 Hz). Hourly telemetry: heap size, free bytes, live objects, pause p50/p99, RSS. Post-soak RSS check (< 10% growth) and drain verification. Weekly CI cron (Monday 06:00 UTC). (make soak, CI soak job.)
  • Alloc pattern fuzzing: bench/pattern_fuzz.cr -- 3 allocation distributions (Zipfian power-law, bimodal small+large, stride array-growth) each checked against baseline uniform-random. Verifies pause p99 < 8-10x baseline and RSS growth < 10%. 200 phases × 5000 objects per phase. (make pattern-fuzz, CI Alloc pattern fuzz step.)
  • Thread storm test: bench/thread_storm.cr -- 3 phases: thread spawn storm (OS threads doing alloc/free/collect in batches), rapid thread create/destroy (250 short-lived threads), Crystal Signal.trap deferred alloc (event-loop mutator path; GC is not async-signal-safe — see POLICY.md). 1000+ iterations total, 0 errors. (make thread-storm, CI Thread storm step.)
  • OOM scenarios: bench/oom_test.cr -- 3 phases: bounded heap (low gc_threshold, 500 iterations, no crash), mmap failure (graceful OutOfMemoryError), finalizer under OOM (no crash under pressure). (make oom-test, CI OOM test step.)
  • Bug-fix test policy: CONTRIBUTING.md with "bug fix must include test" rule, .github/PULL_REQUEST_TEMPLATE.md with reproducing test checkbox, and spec/regression/ directory with 4 regression tests (live_objects dormant chunk, hash_layout entries_size, scan_cap alloc_size mismatch, signal_stack false root). (spec/regression/, CI regression jobs.)
  • API misuse test suite: spec/api_misuse_spec.cr -- tests covering GC.free(null), GC.realloc(null, 0), GC.malloc(0), GC.malloc_atomic(0), Gcry.add_root(null), Gcry.register_disappearing_link(null, ...), collect inside finalizer (no deadlock), Crystal Signal.trap deferred alloc (Linux; not async-signal-safe), add_root with large pointer, alternating malloc/free. (make spec, CI spec step.)
  • Fork reinit test: bench/fork_reinit.cr -- standalone LibC.fork + after_fork_child_reinit + alloc in child + parent continues allocating after collect. 3 assertions, all pass. (make fork-test, CI Fork reinit test step.)
  • Finalizer complex scenarios: bench/finalizer_complex.cr -- 7 phases: finalizer chain, finalizer calling GC.collect, finalizer adding root (resurrection), finalizer + disappearing links interaction, finalizer under heavy allocation pressure (500 objects), finalizer creating 1000 objects, and many disappearing links (200). 8/8 assertions pass. (make finalizer-complex, CI Finalizer complex scenarios step.)
  • Perf regression alerting: bench/perf_smoke.sh rewritten with variance protocol -- 5 wrk runs per path, min/max discarded, median reported, noise ratio computed (IQR/median). same-host variance protocol (N wrk runs, min/max discard, median, noise ratio); gate is gcry /json % of Boehm only. Absolute RPS is not compared across hosts. Per-run JSON under bench/log/ uploaded as CI artifact. (bench/perf_smoke.sh, CI perf smoke job.)
  • Microbenchmark suite: bench/micro/run_all.cr -- 6-phase suite measuring alloc latency (10 size classes, p50/p99/max), free latency, collect latency (5000 obj, p50/p99/max), TLAB refill cost, STW suspend/resume latency, and GC lock overhead. Runs in < 10s. (make microbench, CI Microbenchmark suite step.)
  • Pause time budget: bench/pause_budget.cr -- major p99/max budgets scaled to live set, incremental collect_a_little slice budget (STW-aware), minor vs major pause ratio. (make pause-budget, CI Pause budget step.)
  • RSS leak detection: bench/rss_leak.cr -- cyclic alloc/free/collect; gate is intra-run RSS growth only (late-half vs early-half <10%). RSS/heap ratio is informational. Writes gitignored bench/trend.json. (make rss-leak, CI RSS leak detection step.)
  • Darwin platform parity tests (Phase 6.1): spec/platform_darwin_spec.cr asserts soft-dirty/mprotect stubs return unsupported, pthread_get_stackaddr_np stack bounds contain the current SP, and host-page-aligned MADV_FREE_REUSABLE reclaim works. process_spec Darwin section exercises Mach thread_suspend/resume STW round-trip + SP clamp under -Dgc_none. Windows process-GC gap documented in docs/INTEGRATION.md (crystal#15173 HeapAlloc stub ≠ gcry port).
  • Compiler GC contract (Phase 6.3): bench/compiler_gc_contract.cr mirrors Crystal spec/std/gc_spec.cr (stats/prof_stats/enable) plus malloc/realloc/collect, disable/enable, and runtime @crystal_type_id vs crystal_instance_type_id. CI also runs crystal tool hierarchy / unreachable on gcry sources. (make compiler-gc-contract)
  • Kemal E2E (Phase 6.4): bench/kemal_e2e.sh hits every endpoint (/, /json, /gc-collect, /gc-stats, /metrics) before and after concurrent wrk load. CI runs 60s; full 10-min DoD via KEMAL_E2E_DURATION=600 make kemal-e2e.
  • GC trace log (Phase 7.1): GCRY_TRACE=1 emits NDJSON events (alloc/free sampled, collect_start/collect_end, finalizer, barrier_arm) via Gcry::Trace. Reentrancy guard avoids malloc recursion. (make trace-smoke, spec/trace_dump_spec.cr)
  • Heap dump (Phase 7.2): Gcry.dump_heap(io) / dump_heap_addresses / heap_dump_gone/new for live-object NDJSON and leak diffs. Dump count matches live_objects.
  • Mutation harness (Phase 7.3): bench/mutations/run.sh — 10 hand-crafted sed mutants; kill suite scores 10/10. Feasibility notes in docs/MUTATION.md.

Fixed

  • Gcry::Trace under -Dgc_none: do not require "json" or write via abstract IO — both pulled JSON/OpenSSL into the GC bootstrap and broke process builds. Trace now emits NDJSON with a stack buffer + LibC.write to a raw fd.

  • Darwin release_physical_pages spec: do not assert immediate zero-fill after MADV_FREE_REUSABLE (kernel may keep contents until reclaim). Assert aligned success + still-mapped only.

  • Nursery HTTP::Headers regression: moved from process_spec to standalone bench/nursery_headers.cr — Spec + process GC + nursery was flaky on CI (SEGV during Spec reporting).

  • Process parallel mark: moved from process_spec to bench/parallel_mark_process.cr for the same Spec+process-GC flake; CI retries process_spec up to 3 times.

  • **live_objects counter drif...

Read more

v0.13.0

Choose a tag to compare

@sdogruyol sdogruyol released this 27 Jul 19:08
4ebdfef

Changed

  • Darwin empty_chunk_retain 8 MB → 512 KB: Aggressive MADV_FREE_REUSABLE reclaim on Darwin. Kemal RSS drops from ~160 MiB to ~18 MiB (1.04× Boehm). ACIKTURKIYE RSS unchanged (~700 MiB); conservative live set remains the dominant driver.
  • scrub_fibers_enabled = true (Linux + macOS): Default-on fiber stack scrubbing to reduce false roots from parked fiber stacks. Linux: Kemal RSS 0.99×→0.95×, acikturkiye RSS 3.00×→2.65×. macOS: ACIKTURKIYE RSS steady at ~700 MiB (conservative live set dominant). Opt-out via GCRY_DISABLE_SCRUB_FIBERS=1.
  • Darwin gc_threshold 32 MB → 16 MB: More frequent major collections on Darwin; pause halved (47→25 ms p50) on ACIKTURKIYE.
  • Darwin small_chunk_bytes 128 KiB → 256 KiB: The 128 KiB chunk inflated collection count (~290 majors in 30s) and crushed acikturkiye throughput to ~57% Boehm. 256 KiB recovers throughput to ~78% without meaningful Kemal RSS cost (1.06× vs 0.88×). Set in gc_override.cr for Darwin only; library default stays 128 KiB. Escape: GCRY_CHUNK_BYTES=131072.

Added

  • Darwin large-freelist MADV_FREE_REUSABLE: darwin_release_large_freelist_pages issues MADV_FREE_REUSABLE for every cached large-object chunk after major collection on Darwin, dropping physical pages without unmapping. Linux unchanged (mmap-resident for cache budget).

Performance

  • macOS v0.13.0 (Apple Silicon M2 Pro, median-of-3, wrk -c 100 -d 30, --release, 256 KiB chunk default):

v0.12.0

Choose a tag to compare

@sdogruyol sdogruyol released this 26 Jul 19:35
06d049e

Added

  • -Dgcry_side_bitmap (opt-in): side MarkBitmap mmap path kept for experiments. Default is in-header MARK again after Linux A/B showed bitmap default at 82% /json @ ~9.2× RSS vs header 89% @ 0.99× (acikturkiye 50%→93%, 5.6×3.0×) — bench/log/bitmap-ab/FINDINGS.txt.
  • Bitmap shrinking + adaptive headroom (P1.1): MarkBitmap#shrink_to_fit! reduces the side-mark bitmap mmap when the heap range contracts. Adaptive headroom (25% of recent growth history) prevents immediate re-growth. Combined with tighter update_heap_bounds_after_unmap, Kemal RSS drops from ~10× to ~5–7× (when -Dgcry_side_bitmap).
  • Darwin MADV_FREE_REUSABLE (P1.1, macOS): release_physical_pages switched from the expensive 3-syscall mach_vm_deallocate+allocate+protect to a single madvise(..., 5). empty_chunk_retain lowered from 64 MiB to 8 MiB on Darwin (no cost; MADV_FREE_REUSABLE is cheaper than the retain budget).
  • Deferred madvise — STW pause damping (P1.4): All madvise / page-release syscalls defer to post-STW flush functions (flush_pending_dormant_chunks, flush_pending_page_release_chunks). DORMANT/HOLED flags set during STW; actual syscalls run after threads resume, eliminating kernel VM lock contention that caused 132–150 ms pause tails.
  • Cross-chunk dormant coalescing (P1.4): flush_pending_dormant_chunks merges contiguous dormant chunks into a single madvise region (one syscall per run instead of one per chunk).
  • Per-chunk free-page coalescing (P1.4): dontneed_free_pages_in_chunk pre-computes a live-page mask and issues one madvise per contiguous free run instead of one per free page (reduces from up to 64 syscalls/chunk to 1–3).
  • Auto-layouts (P2.1): Gcry.register_layouts whole-program walk + @unsafe_layouts blacklist (Cry / Crystal::* / LibC::*; metric layout_unsafe_skips). Opt-in via GCRY_AUTO_LAYOUTS=1 (Linux Kemal /json ~−7pp vs builtins-only — see bench/log/thr-abis). Escape when opted in: GCRY_DISABLE_AUTO_LAYOUTS=1.
  • Per-source root reject counters: New type_id_stack_rejects / type_id_static_rejects / type_id_thread_rejects count where false roots come from (fiber/mutator stacks, BSS/data, TLS). Plus type_id_root_false_negatives is now exposed in /gc-stats, metrics, and Prometheus — was tracked but never surfaced. Sum invariant: stack + static + thread == type_id_root_rejects.
  • Adaptive nursery threshold: @nursery_threshold adjusts dynamically after each minor based on the moving-average survival rate (last 10 minors). Target survival rate is 50%; when survival rises above it the threshold grows by 25% per minor (reducing collection frequency); when survival drops below 25% the threshold shrinks by 25% (collecting sooner to limit survivor pressure). Clamped to [64 KiB, 8 MiB]. Default-on for process GC (adaptive_nursery=true); disable via GCRY_DISABLE_ADAPTIVE_NURSERY=1.
  • Large-cache LRU eviction + adaptive retain (P3.3): cache_large_chunk inserts at tail (LRU). trim_large_cache evicts from head. Adaptive retain: after each major, hit-rate above 50% doubles retain (capped at 64 MiB); hit-rate below 10% halves it (floor 1 MiB). Default: 1 MiB on Darwin (macOS), 8 MiB on Linux.
  • Bitmap headroom reduced 25% → 12.5%: note_bitmap_growth now uses avg_range >> 3 instead of >> 2, shrinking side-mark bitmap reserve — less RSS waste on stable heaps.

Fixed

  • Hash layout walk used entries_capacity instead of Crystal entries_size: precise scan_hash_object iterated (1 << indices_size_pow2) / 2 slots. After realloc, slots past @size + @deleted_count are uninitialized; non-zero garbage @hash words caused false marks / mutator UAF under acikturkiye (GCRY_DISABLE_LAYOUT=1 was the only green bisect). Now walks @size + @deleted_count, capped by capacity. Also word-scans @block (Proc?, 16 bytes) instead of treating it as a single pointer.
  • Layout scan_cap required alloc_size match: on size mismatch (raw buffer whose leading Int32 collided with a registered type_id), the old path still applied that type's scan_cap and returned — truncating the mark scan and dropping live pointers (acikturkiye SEGV with layouts on; green with GCRY_DISABLE_LAYOUT=1). Size mismatch now falls through to full conservative scan.

Changed

  • In-header MARK is default again: side mark bitmap moved to -Dgcry_side_bitmap after Linux HTTP A/B (bench/log/bitmap-ab). Headline cut: Kemal /json 88.8% @ 0.99× RSS; acikturkiye 92.8% @ 3.0×docs/PERF.md, docs/ACIKTURKIYE.md.
  • Nursery + incremental default-off for process GC: Linux no longer enables nursery / incremental_auto by default. Soft-dirty false-negatives under WSL release HTTP (Kemal) caused Hash key UAF / SEGV (0x0/0x4/0x11). Opt in with GCRY_NURSERY=1 / GCRY_INCREMENTAL=1 after measuring. Darwin unchanged (already off). Related fixes kept: realloc pins old buffers across collect; explicit roots skip type_id_gate; old→young always full-walks (soft-dirty is additive only) with one-level buffer chase.
  • incremental_auto defaults (P1.3, Linux/Darwin): (superseded — both off by default; see above.)
  • GCRY_AUTO_LAYOUTS opt-in (P2.1): briefly default-on; reverted after Linux A/B — builtins-only /json ~85% Boehm vs auto-on ~78% (bench/log/thr-abis). Set GCRY_AUTO_LAYOUTS=1 to enable.
  • Bench default build: bench/run_all.sh uses pure --release again (PERF.md). --release --debug --error-trace cost ~15–18pp thr; use CRYSTAL_FLAGS / DEBUG=1 only for SEGV hunting.
  • Nursery default-on for Linux process GC: (superseded — off by default again; see above.)
  • Darwin blacklist re-enabled: Previously default-off on Darwin (freelist-abandonment spiral under all-conservative scanning). Layout-precise scans (P2.1) cut false root hits sharply, making the blacklist safe. Escape via GCRY_DISABLE_BLACKLIST=1.
  • Darwin aggressive free-page release: flush_pending_page_release_chunks walks ALL kept size-class chunks (not just HOLED) on Darwin. MADV_FREE_REUSABLE is page-table-level (no VM lock churn), so the extra walk is cheap per major.
  • Darwin large cache reduced to 1 MiB (adaptive): Adaptive LRU policy starts at 1 MiB on Darwin (vs 8 MiB on Linux). mach_vm reclaim already punches holes on free, so a fat cache is wasteful; 1 MiB floor avoids mmap churn for the common case.

Performance

  • Linux Kemal (WSL2 x86_64, median of 3, pure --release, in-header MARK default, session bench/log/2026-07-26-173602/): / 90.4% of Boehm; /json 88.8%; post-GC RSS 0.99×. acikturkiye /api/v1/: 92.8% of Boehm, post-GC RSS 3.00×. Side-bitmap A/B (2026-07-26-171942): /json 82.3% @ ~9.2×, acik 50.1% @ 5.58×. See docs/PERF.md, docs/ACIKTURKIYE.md.
  • macOS Kemal (Apple Silicon M2 Pro, median of 3, pure --release, in-header MARK default, session bench/log/2026-07-26-181318/): / 85.4% of Boehm; /json 86.5%; post-GC RSS 1.34–1.36×. acikturkiye /api/v1/: 76.7% of Boehm, post-GC RSS 22.3× (RSS improved 2.6× vs prior session; conservative live set remains the dominant driver). See docs/PERF-macos.md, docs/ACIKTURKIYE-macos.md.
  • STW pause tail eliminated: deferred madvise removes kernel VM lock from the STW window. Max pause drops from 132–150 ms to well under 50 ms on Kemal /json c=100.

v0.11.0

Choose a tag to compare

@sdogruyol sdogruyol released this 25 Jul 14:34
42289ee

Added

  • Side mark bitmap: mark bits live in a separate mmap (one bit per word-aligned heap address), replacing the in-header MARK flag. clear_all_marks is now a UInt64 word-by-word zero over the bitmap (full memory bandwidth) instead of a per-block header write. marked?/set_mark/clear_mark are answered from heap-inlined mirror fields (@mark_bitmap_base / @mark_bitmap_base_addr / @mark_bitmap_cap_bits) so the mark hot path no longer dereferences Gcry.current_mark_bitmap plus a MarkBitmap method. Bitmap relocation publishes the new base pointer before unmapping the old mapping; Heap#destroy clears the global first then nulls the mirrored fields so stale readers short out.
  • Chunk coalescing on flush: flush_pending_empty_chunks walks the pending list and merges fully-contiguous chunks (next.base == current end) into single munmap regions (one syscall + one VMA teardown per run instead of one per chunk). Stricter than the naive <= check so chunks with a gap (kernel-placed VMA between) are flushed independently.
  • empty_chunk_retain bumped to 64 MiB in the process GC override — keeps recently-freed chunks as MADV_DONTNEED dormant (kernel drops the physical pages, VMA cache survives for fast reuse). 0 MiB regressed ~70% via mmap/madvise cycling; 32 MiB regressed ~50% (reclaim thrashing); 64 MiB is the sweet spot.

Changed

  • HDR pause histogram: @pause_hdr is a StaticArray(UInt64, 64) with bucket indices chosen by clz on the elapsed-ns value (1–3 ns, 4–7 ns, …). Exposed via Gcry.pause_percentile_hdr_ns(p) and Gcry.pause_hdr_snapshot (per Kemal /gc-stats).
  • type_id gate instrumentation: type_id_root_false_negatives counter for objects rejected by the ambient-root gate that later proved live by other means; bounds the false-negative rate under workloads that mix static-root scanning with type_id gating.
  • Mark-stack prefetch + chunk batching: the mark loop walks chunk ranges in size-class order with __builtin_prefetch on the next chunk header; cache miss count drops on Kemal /json.

Fixed

  • Flush coalescing under-counted unmapped_bytes on Linux. The old <= coalescing predicate (nxt.base <= run_end) silently skipped chunks whose ranges overlapped or had a small gap (4 KiB page between two separately-mmap'd size-class chunks is common on Linux x86_64). The result was unmapped_bytes ~½× released_chunk_bytes on spec/collect_spec.cr:159 ("munmaps fully free size-class chunks on major"), failing CI on Linux x86_64 + aarch64 native + aarch64 cross-compile. Tightened to nxt.base == run_end (only fully-contiguous chunks coalesce) so the release count and the unmapped count always match. Verified in crystallang/crystal:1.21.0 Docker (Linux x86_64): 94/94 unit specs + 13/13 process specs + 5 samples + format + Ameba all pass.

Performance

  • macOS Kemal (Apple Silicon, median of 3, scrub off): / ~100% of Boehm (was ~97%); /json ~94% of Boehm (was ~90%); post-GC RSS ~10× (was ~0.97× — see notes). Latency p50: /json 2.3 ms (was 18 ms, −87%); / 1.7 ms (was 14 ms, −95%). p99 latency within 2× of Boehm on both paths. See docs/PERF-macos.md.
  • Note on RSS: the side mark bitmap itself allocates a separate mmap region covering the live heap (1 bit per word-aligned address). For the Kemal workload this adds ~200 MiB of mapped address space on top of the managed heap — hence the ~10× post-GC RSS. This is the explicit price paid for moving mark bits off the object headers; further reduction requires the bitmap to follow heap-range tightening (see ensure_bitmap_covers) or a shared page-cache strategy. The throughput + latency win more than compensates for the higher mapped set on the HTTP workload.
  • Linux numbers unchanged (this host is Darwin) — re-record on Linux before citing a new Linux cut. See docs/PERF.md.

v0.10.0

Choose a tag to compare

@sdogruyol sdogruyol released this 25 Jul 08:29
db8781d

Added

  • macOS process GC (the headline): -Dgc_none + require "gcry" is a real collector on Darwin (arm64 + x86_64), Crystal ≥ 1.21 — not stubs.
    • STW: Mach thread_suspend / thread_resume (signal STW under HTTP was ~hang / ~2 req/s)
    • SP clamp: thread_get_state + pthread_get_stackaddr_np stack bounds
    • Static roots: dyld main-image __DATA / __DATA_CONST (__data / __bss / __common; skip __const)
    • Free-page RSS: host-page mach_vm_deallocate + allocate(FIXED) (Apple Silicon 16 KiB; MADV_DONTNEED does not drop Darwin RSS)
    • Defaults: page blacklist off (opt-in GCRY_BLACKLIST=1); large_cache_retain 0
    • CI: macos-latest native specs + samples
  • Gcry.register_set(T) — registers Hash(T, Nil) for Set backing maps.
  • GCRY_SCAN_CAPS=1 — optional whole-program instance_sizeof scan caps (fat-app live set often unchanged).

Changed

  • Layout builtins: broader curated coverage — primitive/String arrays, Set-backing hashes, Hash/Array + JSON::Any, IO::Memory (noscan buffer), more Deques. Still not whole-program GCRY_AUTO_LAYOUTS.
  • Layout correctness: Pointer(T) noscan uses !T.has_inner_pointers? (safe for Array(JSON::Any)). Hash keys/values with inner pointers word-scanned.
  • Mark: size-class mismatch falls back to scan_cap when present; precise entries store instance_sizeof.
  • Large objects: mmap aligned to Platform.host_page_size; LARGE_CACHE_LIMIT hard-caps freelist retain.
  • Blacklist: page granularity uses host_page_size.
  • Docs: Linux vs Darwin PERF / ACIKTURKIYE split; README highlights macOS.

Performance

  • macOS Kemal (0.10.0 cut, Apple Silicon, median of 3, scrub off): / ~97% of Boehm; /json ~90%; post-GC RSS ~0.96–0.97× — see docs/PERF-macos.md.
  • macOS acikturkiye /api/v1/ (median of 3): thr trial-median ~80%; post-GC RSS ~11.8× (dense conservative-live; reclaim works) — see docs/ACIKTURKIYE-macos.md.
  • Linux Kemal / acikturkiye cut numbers unchanged from 0.9.0 (this host is Darwin; re-record on Linux before citing a new Linux cut) — docs/PERF.md, docs/ACIKTURKIYE.md.

v0.9.0

Choose a tag to compare

@sdogruyol sdogruyol released this 24 Jul 18:09
71f4657

Added

  • Process-GC parallel mark (STW-exempt): with GCRY_PARALLEL_MARK=N / parallel_mark_workers > 1, helpers are raw LibC.pthread_create threads (not Crystal::Thread), so stop_world does not suspend them. They steal grey objects under @mark_lock (parallel_mark_stolen). Fork child abandons the pool via reset_mark_workers_after_fork.
  • Library-heap parallel mark: with parallel_mark_workers > 1 and stop_the_world == false, helper Threads steal grey objects (parallel_mark_stolen).
  • Stack scrubbing (no Crystal patch): GCRY_CLEAR_STACK=1 zeros a window below SP (skips x86_64 red zone; default every 16 allocs) without calling Fiber/Thread APIs; GCRY_SCRUB_FIBERS=1 zeros a capped window below each parked fiber's saved SP before mark (not the full unused stack — that faults pages in and blows RSS). Metrics: clear_stack_* / fiber_scrub_* (json_stats + Prometheus). Not stack maps; measure before enabling as default.
  • Richer Gcry::Observability.json_stats (phase timers, mapped/live bytes, TLAB, parallel-mark, barrier) — Kemal /gc-stats uses it.
  • Prometheus: TLAB, parallel-mark, phase, layout, SP clamp, barrier, size-class live / released chunk gauges; gcry_clear_stack_* / gcry_fiber_scrub_*.
  • Median-of-3 helpers: bench/median_kemal_boehm.sh, bench/median_acikturkiye_boehm.sh.

Changed

  • README / HARDENING / POLICY: GCRY_PARALLEL_MARK is real for process GC (pthread steals), not counter-only — and labeled experimental / measure first (Kemal /json + acikturkiye /api/v1/ thr regressed vs N=1 in same-host wrk).
  • README / HARDENING: document GCRY_DISABLE_* escapes, GCRY_TLAB, stack-scrub knobs.
  • Dogfood docs: docs/ACIKTURKIYE.md + docs/API.md point at Observability routes; acikturkiye make run-demo-gcry / README GC section.
  • Same-host Kemal (0.9.0 cut, median of 3, scrub off): / ~89% of Boehm; /json ~92%; post-GC RSS ~0.97× — see docs/PERF.md.
  • Same-host acikturkiye /api/v1/ (median of 3, scrub off): thr trial-median ~93%; post-GC RSS ~2.84× (was ~3.20× at 0.8.0) — see docs/ACIKTURKIYE.md.

Fixed

  • clear_stack aarch64 SEGV: wipe used approximate pointerof(local) as SP (mid-frame). With no x86_64 red zone that zeroed the leaf frame (Invalid memory access @ 0x0 on CI test (aarch64 native)). Now reads hardware SP (Roots.hardware_stack_pointer) plus a leaf margin.