-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathheap.zig
More file actions
6993 lines (6482 loc) · 338 KB
/
Copy pathheap.zig
File metadata and controls
6993 lines (6482 loc) · 338 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//! Mark-sweep heap for Cynic's runtime.
//!
//! later ships the simplest correct thing: a list of GC-managed
//! objects, each carrying a `marked` bit. `collect(roots)` marks
//! reachable objects from the supplied root values and sweeps the
//! rest. Allocator: the host `std.mem.Allocator`. No bump-pointer
//! young space, no copying, no concurrency. The handbook's
//! [compiler-engineering.md] says start here; generational comes
//! after later.
//!
//! Roots fed to `collect` come from the caller. The interpreter
//! supplies its register file + accumulator + constant pool's
//! string entries; built-ins supply their `HandleScope`
//! contents. Cynic deliberately doesn't bake "find your own roots"
//! into `Heap` — explicit roots make the GC contract auditable.
//!
//! `HandleScope` is provided here too, for Zig-side runtime code
//! that allocates more than one heap object across a single
//! abstract operation (e.g. `String.prototype.concat` allocating
//! the result before its operands' last use). Mirrors V8's
//! `Local<T>` ergonomics, single-threaded.
const std = @import("std");
const builtin = @import("builtin");
const value_mod = @import("value.zig");
const Value = value_mod.Value;
const string_mod = @import("string.zig");
const JSString = string_mod.JSString;
const utf16 = @import("utf16.zig");
const JSFunction = @import("function.zig").JSFunction;
const HeapKind = @import("function.zig").HeapKind;
const JSObject = @import("object.zig").JSObject;
const Realm = @import("realm.zig").Realm;
const ShapeTree = @import("shape.zig").ShapeTree;
const Environment = @import("environment.zig").Environment;
const Chunk = @import("../bytecode/chunk.zig").Chunk;
const JSGenerator = @import("generator.zig").JSGenerator;
const jit_code_alloc = @import("jit/code_alloc.zig");
const BistromathStats = @import("bistromath/stats.zig").Stats;
const OhaimarkStats = @import("ohaimark/stats.zig").Stats;
const JSSymbol = @import("symbol.zig").JSSymbol;
const JSBigInt = @import("bigint.zig").JSBigInt;
/// macOS-only: the highest address of `thread`'s stack (the base —
/// the stack grows toward lower addresses). Used by the conservative
/// native-stack rooting backstop (`Heap.scanNativeStackForRoots`) to
/// bound the live native-stack region at GC time. Not exposed by
/// `std.c`, so declared here; referenced only under a comptime
/// `builtin.os.tag == .macos` guard, so non-Darwin targets never link
/// against it.
extern "c" fn pthread_get_stackaddr_np(thread: std.c.pthread_t) ?*anyopaque;
/// Monotonic nanosecond timestamp via libc `clock_gettime`.
/// Used by `Heap.collect`'s diagnostic pause-time field; the
/// std `std.Io` clock would require threading the io handle
/// down here, which costs more than this small libc detour.
///
/// On a freestanding target with no libc (the `wasm32-freestanding`
/// playground build) `std.c.timespec` is `void` — there is no
/// monotonic clock to read, so the GC pause-time diagnostic
/// degrades to a constant 0. Correctness of collection itself is
/// unaffected; only the `--gc-stats` timing field goes dark, and
/// that flag is harness-only.
fn monotonicNs() i128 {
if (@import("builtin").os.tag == .freestanding) return 0;
var ts: std.c.timespec = .{ .sec = 0, .nsec = 0 };
_ = std.c.clock_gettime(.MONOTONIC, &ts);
return @as(i128, @intCast(ts.sec)) * std.time.ns_per_s + @as(i128, @intCast(ts.nsec));
}
/// Heap-managed pointers are at least 8-byte aligned (the
/// allocator's minimum for any struct containing a pointer
/// field), which leaves the bottom three bits free. We encode
/// the heap kind in the bottom two bits of the stored pointer:
///
/// 00 = Function (JSFunction)
/// 01 = Plain object (JSObject)
/// 10 = Symbol (JSSymbol)
/// 11 = BigInt (JSBigInt)
///
/// The tag-object value tag (`0xFFF9`) is shared across all
/// four; predicate selection uses the pointer-tag bits. Real
/// pointers are reconstructed by masking out the tag bits.
// Pub: the JIT layout contract (`jit/layout.zig`) re-exports these
// so compiled tag checks and the Zig-side taggers can never drift.
pub const kind_mask: u64 = 0x3;
pub const kind_function: u64 = 0x0;
pub const kind_object: u64 = 0x1;
pub const kind_symbol: u64 = 0x2;
pub const kind_bigint: u64 = 0x3;
/// §26.2 FinalizationRegistry cleanup-job scheduler. The collector
/// discovers a dead registry target during the post-mark weak pass
/// and must enqueue a host job — `cleanupCallback(heldValue)` — to
/// run later via the normal microtask drain, NOT synchronously
/// inside GC. The `Heap` has no `Realm` in scope, so the realm
/// installs this callback (`Heap.setFinalizationEnqueue`) at init;
/// the collector invokes it with the opaque realm pointer. `ctx`
/// is the `*Realm`; `callback` the registry's `[[CleanupCallback]]`;
/// `held_value` the cell's `[[HeldValue]]`.
pub const FinalizationEnqueueFn = *const fn (
ctx: *anyopaque,
callback: Value,
held_value: Value,
) void;
/// Generational-GC age of a heap object. A `.young` object lives
/// in its kind's young list and is reclaimable by a cheap
/// `collectYoung` cycle; a `.mature` object survived at least one
/// collection and was relinked into the kind's mature list (the
/// object itself never moves — Cynic's collector is non-moving).
/// Two-bit enum so it packs into the existing flag-byte padding
/// next to each header's `marked` bit.
pub const Generation = enum(u2) { young, mature };
/// Heap-kind tag for the conservative native-stack rooting backstop's
/// young-pointer membership map (`Heap.young_ptr_set`). Records which
/// per-kind marker a matched stack word should be routed through — the
/// raw pointer alone doesn't carry its kind, and the NaN-box tag is
/// only present when the stack word is a `Value` rather than a bare
/// Zig pointer.
pub const ScanKind = enum(u3) {
object,
function,
environment,
generator,
string,
symbol,
bigint,
};
pub fn taggedFunction(ptr: *JSFunction) Value {
const p: u64 = @intFromPtr(ptr);
std.debug.assert(p & 0x7 == 0); // 8-byte aligned
return .{ .bits = (@as(u64, Value.tag_object) << 48) | p | kind_function };
}
pub fn taggedObject(ptr: *JSObject) Value {
const p: u64 = @intFromPtr(ptr);
std.debug.assert(p & 0x7 == 0);
return .{ .bits = (@as(u64, Value.tag_object) << 48) | p | kind_object };
}
pub fn taggedSymbol(ptr: *JSSymbol) Value {
const p: u64 = @intFromPtr(ptr);
std.debug.assert(p & 0x7 == 0);
return .{ .bits = (@as(u64, Value.tag_object) << 48) | p | kind_symbol };
}
pub fn taggedBigInt(ptr: *JSBigInt) Value {
const p: u64 = @intFromPtr(ptr);
std.debug.assert(p & 0x7 == 0);
return .{ .bits = (@as(u64, Value.tag_object) << 48) | p | kind_bigint };
}
fn valueKind(v: Value) ?u64 {
if (!v.isObject()) return null;
return v.bits & kind_mask;
}
// The NaN-boxed pointer field is masked out as a `u64`; on a
// 32-bit target (wasm32) `@ptrFromInt` wants a `usize`, so each
// site `@intCast`s down. Real pointers fit `usize` on every
// target Cynic builds for; on 64-bit the cast is a no-op.
pub fn valueAsFunction(v: Value) ?*JSFunction {
if (valueKind(v) != kind_function) return null;
const p = v.bits & Value.pointer_mask;
return @ptrFromInt(@as(usize, @intCast(p)));
}
pub fn valueAsPlainObject(v: Value) ?*JSObject {
if (valueKind(v) != kind_object) return null;
const p = (v.bits & Value.pointer_mask) & ~kind_mask;
return @ptrFromInt(@as(usize, @intCast(p)));
}
pub fn valueAsSymbol(v: Value) ?*JSSymbol {
if (valueKind(v) != kind_symbol) return null;
const p = (v.bits & Value.pointer_mask) & ~kind_mask;
return @ptrFromInt(@as(usize, @intCast(p)));
}
pub fn valueAsBigInt(v: Value) ?*JSBigInt {
if (valueKind(v) != kind_bigint) return null;
const p = (v.bits & Value.pointer_mask) & ~kind_mask;
return @ptrFromInt(@as(usize, @intCast(p)));
}
/// Erase a heap value to an opaque pointer for diagnostic
/// printing — used by `verifyRememberedSet` to name the
/// young-target of an un-barriered edge. Returns `null` for a
/// non-heap value.
fn valueHeapPtr(v: Value) ?*const anyopaque {
if (v.isString()) return v.asString();
if (valueAsFunction(v)) |f| return f;
if (valueAsPlainObject(v)) |o| return o;
if (valueAsSymbol(v)) |s| return s;
if (valueAsBigInt(v)) |b| return b;
return null;
}
/// Used by GC marking and printing — returns whether the value
/// is the function flavour without needing to coerce to a
/// concrete pointer type.
pub fn isFunction(v: Value) bool {
return valueKind(v) == kind_function;
}
pub fn isPlainObject(v: Value) bool {
return valueKind(v) == kind_object;
}
pub fn isSymbol(v: Value) bool {
return valueKind(v) == kind_symbol;
}
pub fn isBigInt(v: Value) bool {
return valueKind(v) == kind_bigint;
}
/// §6.1.7 — JS-level "Object" (plain object or function exotic).
/// Distinct from `Value.isObject`, which is a heap-tag predicate
/// that also covers Symbol and BigInt (those share the
/// tagged-pointer encoding but are primitives at the JS layer,
/// per §6.1.5 and §6.1.6.2). Spec checks like §7.1.1 ToPrimitive
/// "If Type(result) is Object" want this helper, not `isObject`.
pub fn isJSObject(v: Value) bool {
const k = valueKind(v) orelse return false;
return k == kind_object or k == kind_function;
}
/// Drop-in wrapper over `std.heap.MemoryPool(Item)` that can hold a
/// freed slab slot poisoned for N collection cycles before it becomes
/// eligible for reuse — a sweep-quarantine, the GC-aware analog of an
/// allocator quarantine. `MemoryPool.destroy` returns a slot to a LIFO
/// free-list, so the very next same-kind `create` reuses it; a stale
/// pointer to a swept header then reads a *live* object instead of
/// poison, and the use-after-free goes undetected. Holding the slot
/// out of circulation for N cycles keeps it `0xaa`-poisoned across
/// that window, so the dangling read faults — the spatial complement
/// to `setGcThreshold(1)`'s temporal widening. Off by default
/// (`quarantine_cycles == 0`): `create`/`destroy`/`deinit` then match
/// the bare pool one-for-one, so the production engine is unaffected.
///
/// Each cohort is an intrusive `SinglyLinkedList` threaded through the
/// dead slots themselves (slots are `>= @sizeOf(Node)`), so quarantine
/// needs no side allocation and `destroy` can never fail. A ring of N
/// cohorts is rotated once per GC cycle by `rotate`: the cohort about
/// to be reused for this cycle is exactly the one filled N cycles ago,
/// so it is flushed back to the pool before new slots land in it.
fn QuarantinedPool(comptime Item: type) type {
return struct {
const Self = @This();
const Inner = std.heap.MemoryPool(Item);
const Node = std.SinglyLinkedList.Node;
/// Mirrors the (private) `MemoryPool.ItemPtr` exactly — the
/// pool over-aligns slots to fit its intrusive free-list node,
/// and callers pass the same naturally-aligned pointers they do
/// today, so this is a drop-in alias.
pub const ItemPtr = *align(Inner.item_alignment.toByteUnits()) Item;
/// Largest quarantine depth the ring supports; `setQuarantine`
/// clamps to it. 32 cohorts is 3 × 32 list heads across the
/// pooled kinds — negligible, and far past any useful depth.
pub const max_cycles: u32 = 32;
inner: Inner = .empty,
cohorts: [max_cycles]std.SinglyLinkedList = @splat(.{}),
/// Active quarantine depth in GC cycles; 0 disables (bare-pool
/// behaviour).
quarantine_cycles: u32 = 0,
/// Ring cursor — the cohort this cycle's `destroy`s land in.
head: u32 = 0,
pub const empty: Self = .{};
pub fn create(self: *Self, allocator: std.mem.Allocator) std.mem.Allocator.Error!ItemPtr {
return self.inner.create(allocator);
}
pub fn destroy(self: *Self, ptr: ItemPtr) void {
if (self.quarantine_cycles == 0) {
self.inner.destroy(ptr);
return;
}
// Poison the whole header so a dangling read during the
// quarantine window faults (`MemoryPool.destroy` does this
// via `ptr.* = undefined`; we're deferring that call, so do
// it here). The intrusive `prepend` below overwrites the
// leading `Node`-sized bytes with the cohort link — fine,
// those are re-poisoned when the slot is finally flushed.
if (std.debug.runtime_safety) @memset(std.mem.asBytes(ptr), 0xaa);
self.cohorts[self.head].prepend(@ptrCast(@alignCast(ptr)));
}
/// Advance the ring one step and flush the cohort that has now
/// waited `quarantine_cycles` cycles. Called once per GC cycle,
/// before the sweep that fills the new head cohort.
pub fn rotate(self: *Self) void {
if (self.quarantine_cycles == 0) return;
self.head = (self.head + 1) % self.quarantine_cycles;
self.flushCohort(self.head);
}
fn flushCohort(self: *Self, idx: u32) void {
while (self.cohorts[idx].popFirst()) |node| {
self.inner.destroy(@ptrCast(@alignCast(node)));
}
}
/// Set the quarantine depth. Draining everything currently held
/// keeps the ring coherent if the depth changes mid-run; hosts
/// set it once at realm init, before any allocation.
pub fn setQuarantine(self: *Self, cycles: u32) void {
for (0..max_cycles) |i| self.flushCohort(@intCast(i));
self.head = 0;
self.quarantine_cycles = @min(cycles, max_cycles);
}
pub fn deinit(self: *Self, allocator: std.mem.Allocator) void {
// The quarantined headers live in the pool's own arena, so
// `inner.deinit` reclaims their slabs wholesale; the cohort
// lists are intrusive (no side storage to free).
self.inner.deinit(allocator);
self.* = undefined;
}
};
}
pub const Heap = struct {
/// Upper bound (exclusive) of the small-integer toString cache
/// (`small_int_strings` / `smallIntString`). 256 covers byte
/// values, small loop counters / array indices, and
/// `string_concat`'s `(i & 0xff)` range at a 2 KiB
/// (256 × `?*JSString`) per-realm cost; lazy population means only
/// the integers actually stringified allocate a backing string.
pub const small_int_cache_max = 256;
/// Two exact-operand shallow-rope memo entries cover the common
/// `(prefix + value) + suffix` expression shape without turning
/// concatenation into general string interning. The byte/depth
/// admission caps bound the graph retained by these strong roots:
/// two slots bound root count, but would not by themselves stop a
/// cache entry from pinning an arbitrarily large rope.
pub const shallow_cons_cache_size = 2;
pub const shallow_cons_cache_max_depth: u16 = 2;
pub const shallow_cons_cache_max_byte_len: u32 = 64;
const ShallowConsCacheEntry = struct {
left: *JSString,
right: *JSString,
result: *JSString,
};
/// A tiny memo should not tax a workload whose eligible operand
/// identities never repeat. With two FIFO slots, a third consecutive
/// miss proves both entries would be evicted before any reuse. Splay
/// instead has two cold misses followed by 62 hits; bypassing the next
/// 64 eligible pairs amortises miss/root-turnover cost while
/// periodically giving locality another chance.
pub const shallow_cons_miss_limit: u8 = 3;
pub const shallow_cons_bypass_length: u8 = 64;
/// Capacities (in Values) of the two pooled dense-element buffer
/// classes. The compact class matches Splay's retained leaf arrays
/// and the fused class matches the compiler's `make_array_n` cap.
/// Array length and backing capacity are distinct and the latter is
/// unobservable (§13.2.4.1 / §10.4.2), so the exact capacity also
/// serves as the internal pool-class discriminator without spending
/// another JSObject brand bit. Keep this measured two-class policy
/// narrow; add another arena only when a retained-length histogram
/// demonstrates that its saved live bytes outweigh idle slabs.
pub const compact_element_buf_cap = 10;
pub const element_buf_cap = 16;
allocator: std.mem.Allocator,
/// Monotonic counter for per-ClassTail-evaluation private brand
/// prefixes (§15.7.14 step 31; `"B{n}#"`). Lives on the Heap, not
/// the Realm: sibling realms over one shared heap exchange objects
/// freely, so brand identity must be unique across the whole
/// object-identity domain — two realms each minting their own
/// "B0#" would alias distinct classes' private brands and let a
/// cross-realm `A.read(new B())` slip past §7.3.27
/// PrivateElementFind's brand check.
class_brand_counter: u32 = 0,
/// Allocator backing large heap-owned payloads (JSString.bytes,
/// ArrayBuffer slabs) that the mark-sweep collector will free
/// during `sweep`. Defaults to `allocator`, but hosts running
/// many disjoint workloads on top of an `ArenaAllocator` (the
/// test262 harness, in particular) override it to a real
/// page-returning allocator — `arena.free()` is a no-op, so
/// without the split, freed string bytes stay resident inside
/// the arena's pages and per-fixture peak RSS never shrinks.
bytes_allocator: std.mem.Allocator,
/// §10.1 property-shape transition tree, shared by every object
/// allocated on this heap (agent-scoped, like a V8 Isolate's
/// Maps). The realm-agnostic `JSObject.set` reaches it through
/// each object's `heap` back-pointer. Realm-lifetime arena —
/// the GC does not trace into shapes.
shapes: ShapeTree,
/// Executable-code region for the JIT tiers, lazily reserved on
/// the first tier-up (docs/jit.md §8 — one reservation per
/// engine, all tiers install through it). Null until then, and
/// permanently null on targets without codegen support or when
/// the reservation fails (tier-up then degrades to "stay
/// interpreted").
jit_code: ?jit_code_alloc.CodeAllocator = null,
/// Opt-in T1 execution witness. Heap scope aggregates child realms; when
/// disabled, native entry pays only the predictable flag check.
bistromath_stats: BistromathStats = .{},
/// Opt-in T2 rollout counters. Heap scope deliberately aggregates child
/// realms, which share executable memory and object identity with their
/// parent. Disabled means no clock reads or hot-entry counter traffic.
ohaimark_stats: OhaimarkStats = .{},
// Per-kind live-object lists. Each kind is split into a
// `young` list (fresh allocations — reclaimable by the cheap
// `collectYoung` cycle) and a `mature` list (objects that
// survived at least one collection). `collectFull` sweeps
// both; `collectYoung` sweeps only the young lists, relinking
// survivors into the mature list (a pointer move — the object
// never relocates, the collector is non-moving). Stage 1
// wires the split but `collectFull` keeps the old behaviour.
/// Young `JSString` instances. Allocate appends here.
strings_young: std.ArrayListUnmanaged(*JSString) = .empty,
/// Mature `JSString` instances — survived a young collection,
/// or allocated straight here when pinned (chunk constants).
strings_mature: std.ArrayListUnmanaged(*JSString) = .empty,
/// Young `JSFunction` instances.
functions_young: std.ArrayListUnmanaged(*JSFunction) = .empty,
/// Mature `JSFunction` instances.
functions_mature: std.ArrayListUnmanaged(*JSFunction) = .empty,
/// `%Function.prototype%` — handed to the heap by realm init
/// once it exists. `allocateFunctionNative` reads it to wire
/// each native function's `[[Prototype]]` at creation time, so
/// `.call` / `.apply` / `.bind` resolve on every native — even
/// ones built lazily after init's one-time proto-wiring pass.
/// `null` only during the early bootstrap before
/// `%Function.prototype%` is allocated; those functions are
/// caught by that init pass instead. Borrowed — the object is
/// rooted via the realm's intrinsics and outlives the heap.
function_prototype: ?*JSObject = null,
/// Every `Realm` that shares this heap — the heap-owning realm
/// plus any child realms created via `Realm.initChild`
/// (`$262.createRealm` / `ShadowRealm`). The collector marks
/// roots from ALL of them before sweeping, because objects of
/// any sharing realm live in the same pools; marking only the
/// running realm's roots would sweep a sibling realm's live
/// objects (a cross-realm use-after-free). Realms register at
/// `installBuiltins` (stable-address) and deregister at
/// `deinit`.
realms: std.ArrayListUnmanaged(*Realm) = .empty,
/// Child realms whose owning `ShadowRealm` object was found dead
/// during the current sweep — torn down (freed) *after* the sweep
/// completes, never inline, since freeing a `Realm` re-enters the
/// allocator and touches maps the sweep is mid-walk over. Drained
/// by `Realm.drainRealmTeardown` once `collectFull` / `collectYoung`
/// return. See docs/multi-realm.md "per-realm teardown".
pending_realm_teardown: std.ArrayListUnmanaged(*Realm) = .empty,
/// Young plain `JSObject` instances (object literals,
/// prototypes, built-in constructors' return values).
objects_young: std.ArrayListUnmanaged(*JSObject) = .empty,
/// Mature plain `JSObject` instances.
objects_mature: std.ArrayListUnmanaged(*JSObject) = .empty,
/// Young `Environment` records — one per active scope that
/// holds named bindings.
environments_young: std.ArrayListUnmanaged(*Environment) = .empty,
/// Mature `Environment` records.
environments_mature: std.ArrayListUnmanaged(*Environment) = .empty,
/// Young `JSGenerator` instances. Each carries an owned
/// register file plus borrowed pointers into env / chunk;
/// `deinit` frees the register buffer.
generators_young: std.ArrayListUnmanaged(*JSGenerator) = .empty,
/// Mature `JSGenerator` instances.
generators_mature: std.ArrayListUnmanaged(*JSGenerator) = .empty,
/// Young `JSSymbol` instances. Identity is by pointer; two
/// `Symbol("x")` calls produce distinct entries here.
/// `Symbol.for("k")` interns into `symbol_registry`.
symbols_young: std.ArrayListUnmanaged(*JSSymbol) = .empty,
/// Mature `JSSymbol` instances.
symbols_mature: std.ArrayListUnmanaged(*JSSymbol) = .empty,
/// Young `JSBigInt` instances. Allocated by every
/// `0n`-literal and arithmetic result; identity is
/// by-value at the language level (the heap may dedupe
/// later as an optimization).
bigints_young: std.ArrayListUnmanaged(*JSBigInt) = .empty,
/// Mature `JSBigInt` instances.
bigints_mature: std.ArrayListUnmanaged(*JSBigInt) = .empty,
/// `Symbol.for` registry (§20.4.2.2 GlobalSymbolRegistry).
/// Maps the registry key (always a string) → JSSymbol pointer
/// so successive `Symbol.for(k)` calls return the same symbol.
symbol_registry: std.StringArrayHashMapUnmanaged(*JSSymbol) = .empty,
/// Monotonic counter feeding `<sym:N>` property keys for
/// user-created Symbols. Distinct from `symbols.items.len`
/// because Symbols can be GC'd; using the count would
/// recycle keys and create false collisions across realm
/// lifetime.
next_symbol_id: u64 = 0,
/// Open handle scopes, in nesting order. The top of the stack
/// is the innermost scope. Roots from every open scope are
/// scanned during a collect.
handle_scopes: std.ArrayListUnmanaged(*HandleScope) = .empty,
/// Chunk-constant heap values — permanently-live non-string
/// constants parked in a `Chunk`'s constant pool: the per-call-
/// site tagged-template `strs` / `raw` arrays, and `BigInt`
/// literal values (§12.9.5). Constant *strings* carry a `pinned`
/// flag the sweep honours directly; objects and bigints don't, so
/// `pinChunk` registers each here and every GC cycle marks them
/// as roots — `markValue`'s recursion then keeps the whole
/// template graph (the `raw` companion, segment strings, anchored
/// index keys) reachable. Realm-lifetime; freed in `deinit`.
const_roots: std.ArrayListUnmanaged(Value) = .empty,
/// Native-constructor instance roots — a LIFO stack of the
/// freshly-allocated instances currently "in flight" inside a
/// native constructor call. The `new_call` opcode / `constructValue`
/// push the instance here before invoking the native and pop it
/// after; a GC triggered by the native re-entering JS (argument
/// coercion, an executor callback) marks the stack so the instance
/// can't be swept mid-construction. A plain `Value` stack rather
/// than a `HandleScope` per construct — the backing capacity is
/// retained across calls, so steady-state push/pop is allocation-
/// free (a `HandleScope` per `new` cost two allocs each). Balanced
/// push/pop keeps it bounded; freed in `deinit`.
native_ctor_roots: std.ArrayListUnmanaged(Value) = .empty,
/// Dirty-container list — every mature container that may hold a
/// pointer to a young object. This is the pooled-heap adaptation
/// of a card-marking remembered set (Cynic's heap is pooled and
/// non-contiguous, so an address-indexed card table doesn't map
/// cleanly): one `dirty` flag per container plus this append-only
/// list of the dirty ones. The write barrier sets the flag +
/// appends on any store of a young heap value into a mature
/// container — edge-class-agnostic. `collectYoung` scans each
/// entry with a GENERIC `markAllPointerFields` (every outgoing
/// pointer of the container) so a young object reachable only
/// from old space survives regardless of which field holds it.
/// An entry is appended at most once (the container's `dirty`
/// bit guards re-insertion). `collectYoung` consumes and clears
/// the list each cycle: with promote-on-first every young survivor
/// tenures, so no mature→young edge can outlive the cycle that
/// created it (the referent is mature by the time the list clears).
/// `collectFull` clears it too — a full mark traces every mature
/// object and tenures every survivor. (When generational aging
/// lands — docs/gc-generational-aging.md — a survivor can stay
/// young across a cycle, so the consume-and-clear becomes a
/// retention + promotion-time rebuild; the generic marking here is
/// already complete-by-construction for that.)
dirty_list: std.ArrayListUnmanaged(Container) = .empty,
/// Conservative native-stack rooting backstop — an exact
/// membership map of every live young heap pointer to its kind,
/// rebuilt at the start of each minor cycle's mark phase
/// (`scanNativeStackForRoots`). The minor cycle scans aligned
/// `usize` words of the current thread's native call stack and,
/// for any word that is an exact key in this map, marks the
/// referent through the kind-correct marker. This layers UNDER
/// the precise `HandleScope`s as a completeness backstop: a
/// native that forgot to root a young heap pointer it holds
/// across a JS re-entry then costs a retained-too-long object,
/// never a use-after-free (the rooting analogue of the dirty-list
/// barrier — see docs/gc-generational-aging.md "Post-barrier
/// finding (2026-06-08): the rooting blocker is divergent").
///
/// EXACT pointers only: a key is `@intFromPtr` of a real,
/// currently-live young allocation, so a stack word matches ONLY
/// a genuine young object — marking it can never trace garbage. A
/// coincidental stack integer equal to a live young pointer just
/// retains that (real, already-reachable-or-not) object, which is
/// safe by construction (it only ADDS roots → can only retain,
/// never free more). Persistent (`clearRetainingCapacity` each
/// cycle) so the per-cycle rebuild is allocation-free in steady
/// state. Empty when no young objects exist, in which case the
/// stack scan is skipped entirely (the common minor-cycle fast
/// path).
young_ptr_set: std.AutoHashMapUnmanaged(usize, ScanKind) = .empty,
/// Gate for the conservative native-stack scan (root source 3 in
/// `collectYoung`). The backstop only earns its cost when a native
/// builtin is on the stack — that is the only time an unrooted young
/// heap pointer can live in a native local across a GC-triggering
/// re-entry. In pure-JS execution every young pointer is reachable
/// through the interpreter frame stack (a precise root), so both the
/// scan AND its per-cycle `buildYoungPtrSet` would be wasted work — a
/// ~20-30% regression on alloc-heavy loops (`object_alloc`,
/// `ctor_array_build`, `class_instantiate`). `Realm.collectGarbageYoung`
/// sets this from `active_native_fn != null` before each minor cycle;
/// direct callers (unit tests) leave it false. Purely additive either
/// way — gating only narrows WHICH cases the backstop helps, it never
/// makes the heap less safe than no backstop at all (the precise
/// `HandleScope`s remain the primary rooting mechanism).
scan_native_stack: bool = false,
/// Allocations (across every kind) since the last `collect`
/// call. Bumped by each `allocateX`; the interpreter dispatch
/// loop checks it against `gc_threshold` between opcodes and
/// runs `Realm.collectGarbage` when it crosses. Zero once GC
/// finishes. Stop-the-world mark-sweep means we never run
/// mid-opcode — pointers from native callbacks stay stable.
allocs_since_gc: u32 = 0,
/// Bytes charged since the last `collect`. A workload that
/// allocates a small number of huge payloads (`String += big`,
/// `new ArrayBuffer(MB)`, …) never trips the count-based
/// threshold, so dead intermediates pile up between collects.
/// Bytes-based trigger keeps GC firing on data volume too.
bytes_since_gc: usize = 0,
/// Heap-level validity epoch for the `sta_property` transition write
/// IC, complementing the cell's `proto_rev` / `proto_shape` checks.
/// Those catch a `setPrototypeOf` (realm counter) and an immediate-
/// proto shape change, but MISS a non-writable data property (or an
/// accessor) installed via `Object.defineProperty` / `freeze` on a
/// *dictionary-mode* or *non-immediate* prototype — its (null) shape
/// doesn't change and the realm counter isn't bumped, so the cached
/// transition would wrongly write past a setter / non-writable
/// (§10.1.9). This epoch is bumped at the low-level structural
/// funnels reachable from any path — accessor install/remove,
/// non-default flagged data install, named delete, shape demote —
/// regardless of which native (or none) drove them. A transition
/// cell snapshots it (`guard_epoch`); a mismatch falls back to the
/// full `[[Set]]`. Plain value writes (`shadowSet`) never bump it,
/// so a hot constructor loop keeps it stable. (Replaced by
/// per-prototype validity cells later — see docs/inline-caches.md.)
proto_struct_epoch: u64 = 1,
/// Allocation count that triggers a *major* (full) collection.
/// Tunable; the default is sized so an empty allocating loop
/// runs GC every few hundred ms at typical
/// `JSObject`/`Environment` sizes. `std.math.maxInt(u32)`
/// effectively disables the trigger (the unit-test paths that
/// call `collect` directly do this when they want full control
/// over when GC fires).
gc_threshold: u32 = 32768,
/// Allocation count that triggers a *minor* (young-only)
/// collection. The two-tier dispatch: a minor cycle fires
/// when `allocs_since_gc` crosses this; a major cycle when
/// `minor_cycles_since_full` reaches `full_every_n_minor`
/// (or the byte threshold trips). Sized at a quarter of the
/// major threshold — most allocations die young, so the cheap
/// young sweep absorbs the bulk of the churn and the
/// expensive full trace stays rare. `setGcThreshold` keeps
/// this coherent with `gc_threshold`.
gc_young_threshold: u32 = 8192,
/// Backstop for the adaptive major trigger: the maximum minor
/// cycles between forced major (full) cycles. A major ALSO fires
/// when the mature set grows past `2×` its post-last-major size
/// (see `mature_objects_at_last_major` + the dispatch in
/// `runSafePoint`), which bounds RSS on churning workloads. This
/// count is the floor that still reclaims slow mature garbage and
/// resets the sticky marks when the mature set is *stable* — a
/// large live retained set (Splay's tree) where the growth trigger
/// never fires. Raised from 8 to 32 once card marking made minor
/// cycles O(young + dirty): forced majors over a stable live set
/// are pure O(live) waste, so deferring them is a large win (Splay
/// ~2×). Bounded so a `--gc-threshold=1` stress run still exercises
/// `collectFull` regularly.
full_every_n_minor: u32 = 32,
/// Minor cycles run since the last major cycle. Reset to zero
/// by `collectFull`; bumped by `collectYoung`. Drives the
/// `full_every_n_minor` backstop in the dispatch.
minor_cycles_since_full: u32 = 0,
/// Mature object count right after the last major cycle — the
/// baseline for the adaptive major trigger. A major fires when
/// `objects_mature.items.len` grows past `2× this + 16384`: the 2×
/// bounds a large live set proportionally (Splay rarely trips it and
/// rides the backstop); the additive floor bounds a churning
/// workload whose post-major live set is tiny (2× small is still
/// small) and avoids a zero-baseline major-every-minor at startup.
/// So churn reclaims its mature garbage before RSS balloons while a
/// stable set defers to the `full_every_n_minor` backstop. Set by
/// `collectFull`.
mature_objects_at_last_major: usize = 0,
/// Byte counterpart to `gc_threshold` — collect when the
/// charged payload since the last sweep crosses this. 16 MiB
/// is loose enough to leave small workloads count-gated while
/// catching the property-escapes / huge-string-concat pattern
/// (each `result += chunk` charges N bytes; without this,
/// 80 += operations on a multi-MB result accumulate hundreds
/// of MB of dead intermediates before the count-based trigger
/// fires).
gc_byte_threshold: usize = 16 * 1024 * 1024,
/// Sweep-quarantine depth in GC cycles (`setGcQuarantine`). 0 = off
/// (production default — slab pools behave as bare `MemoryPool`s).
/// When > 0, a swept pooled header is held poisoned out of the pool
/// free-list for this many collection cycles before its slot is
/// reusable; `rotateQuarantine` advances the ring each cycle. A
/// fuzzing-only knob — see `QuarantinedPool`.
gc_quarantine_cycles: u32 = 0,
/// Sum of bytes charged across `allocateX` callers. Coarse
/// — counts the dominant payload (string bytes, ArrayBuffer
/// bytes, register files); approximate for the small headers.
/// Drives the hard ceiling check below.
bytes_live: usize = 0,
/// Hard ceiling on `bytes_live`. When `charge(n)` would push
/// it over, the heap forces a GC; if still over, returns
/// `error.OutOfMemory`. Mirrors V8's `--max-old-space-size`,
/// QuickJS's `JS_SetMemoryLimit`, Hermes's
/// `gcConfig.maxHeapSize`. Default `maxInt(usize)` =
/// unbounded; sandboxed hosts (test runners, browser tabs,
/// isolated workers) set a per-realm cap so a runaway
/// `new ArrayBuffer(2 ** 31)` can't exhaust system memory.
max_bytes: usize = std.math.maxInt(usize),
/// When non-zero, every `collect` cycle prints a one-line
/// stderr report of live counts per heap kind (before sweep,
/// after sweep). Diagnostic for finding leaks: a kind whose
/// post-sweep count climbs across cycles is being kept alive
/// by something. Counts as the cycle number for cross-
/// referencing.
gc_stats_cycle: u32 = 0,
gc_stats: bool = false,
/// Incremental major-marking phase. `.idle` outside a major mark;
/// `.marking` from `beginIncrementalMark` (roots snapshotted, worklist
/// draining) until the worklist empties. Gates the Dijkstra
/// incremental-update write barrier (`writeBarrier`): a store into an
/// already-scanned (black) container shades the new value grey only
/// while `.marking`. STW today (the drain runs to completion inside
/// `collectFull`), so the barrier is dormant — no mutator stores land
/// mid-mark; the safe-point interleaving activates it.
marking_phase: enum { idle, marking, terminating } = .idle,
/// Wall-clock origin (ns) of the in-flight major cycle, set by
/// `beginIncrementalMark` and read by `collectFullTail` for the
/// diagnostic pause-time field. A heap field (not a `collectFull`
/// local) so the incremental driver's begin / slice / finish can span
/// the safe-point.
cycle_t_start: i128 = 0,
/// Max single mark-slice STW pause (ns) for the in-flight major — the
/// `drainMarkWorklistBudget` worst case, reset at `beginIncrementalMark`
/// and reported under `--gc-stats`. The headline incremental-marking
/// number: the mark pause is now bounded by one slice instead of the
/// whole O(live) walk. Tracked only when `gc_stats` is on (zero cost
/// otherwise).
max_slice_pause_ns: i128 = 0,
/// Incremental-sweep phase. `.idle` outside a sweep; `.sweeping` while
/// the deferred `objects_mature` sweep is sliced across safe-points
/// (after a major mark terminates). While `.sweeping` no other GC runs
/// — the dispatch only sweeps — so the mature list can't grow mid-sweep
/// and the sliced sweep matches the monolithic `sweepList`.
sweep_phase: enum { idle, sweeping } = .idle,
/// Backward cursor into `objects_mature` for the in-flight incremental
/// sweep — the next index to examine, decreasing to 0 (then `.idle`).
sweep_cursor: usize = 0,
/// Max single lazy-sweep-slice STW pause (ns) for the in-flight sweep —
/// the `sweepObjectsMatureBudget` worst case, reset when the sweep is
/// armed and reported under `--gc-stats` at completion. The sweep
/// counterpart to `max_slice_pause_ns` (the mark).
max_sweep_pause_ns: i128 = 0,
/// Cumulative bytes charged across this heap's lifetime (never
/// reset on GC). Dual of `bytes_live`, which only sees what's
/// alive right now. Catches workloads that allocate-and-discard
/// at high rates — e.g. a loop of `result += chunk` looks small
/// in `bytes_live` (one buffer at a time) but huge in
/// `bytes_alloc_total` (every intermediate). Drives the harness
/// `--mem-summary` / `--top-alloc` reports.
bytes_alloc_total: u64 = 0,
/// High-water mark of `bytes_live` reached during this heap's
/// lifetime. Different from per-fixture RSS delta — that's a
/// process-level peak (includes binary, libc, allocator slack);
/// this is the engine's *charged* peak (the slice that GC could
/// theoretically reclaim).
bytes_live_peak: usize = 0,
/// Total `collect()` cycles run on this heap. Independent of
/// `gc_stats_cycle` (which only bumps when `gc_stats` is on).
gc_cycles_total: u32 = 0,
/// Current mark color (V8 / JSC / SM trick). An object is "live
/// this cycle" iff `obj.mark_color == heap.live_color`. The mark
/// phase sets `obj.mark_color = live_color` on every reachable
/// object; the sweep keeps `mark_color == live_color` and frees
/// everything else. Flipped once per *major* cycle (in
/// `beginMajorCycle`), which ages every mature mark to "unmarked"
/// in O(1) so the major trace re-marks the live ones. A *minor*
/// cycle keeps `live_color` stable (sticky mark bits — mature marks
/// persist so the nursery cycle never re-traces the mature set) and
/// instead clears the young generation in `beginMinorCycle`. Fresh
/// allocations seed `mark_color` from `live_color`; a minor's young
/// clear then unmarks them so the minor mark can distinguish live
/// from dead.
live_color: u1 = 0,
/// Set by `beginMajorCycle` / `beginMinorCycle`; cleared at the
/// end of `collectFull` / `collectYoung`. Lets the realm-driven
/// path (which calls the begin-cycle helpers before `markRoots`)
/// skip a redundant arm-cycle inside collectFull / collectYoung,
/// and lets a direct unit-test caller arm the cycle implicitly.
cycle_started: bool = false,
/// Accumulated GC pause time in nanoseconds across every
/// `collect()` cycle. Average pause = `gc_time_ns_total /
/// gc_cycles_total`.
gc_time_ns_total: u64 = 0,
/// Count of `JSObject.deinitFields` calls that ran the slow
/// path (object had accumulated heap-attached state).
/// Pristine deaths take the fast-return at the top of the
/// function and are NOT counted. Diagnostic only — drives the
/// `bench/micros/object_alloc.js` /
/// `bench/micros/ctor_array_build.js` regression tests that
/// verify the architectural invariant: a typical object-literal
/// death pays no per-field deinit cost.
deinit_slowpath_count: u64 = 0,
/// Weak-aware marking mode. `collectFull` sets this `true` for
/// the duration of its mark phase; `collectYoung` leaves it
/// `false`. When `true`, `markValue` does NOT strong-mark the
/// weak slots of a `WeakRef` / `WeakMap` / `WeakSet` /
/// `FinalizationRegistry` — instead each reached weak holder is
/// appended to one of the per-cycle lists below so the
/// post-mark weak-handling pass (§26.1 / §24.3 / §24.4 / §26.2)
/// can clear / prune / queue. A minor cycle keeps the old
/// strong-marking behaviour: a young weak target survives the
/// minor cycle, tenures, and is handled weakly at the next
/// `collectFull`. GC timing is spec-unspecified (§26.1 — a
/// WeakRef is only guaranteed to *eventually* clear), so
/// "weak refs clear at major GC" is fully conformant.
weak_aware_mark: bool = false,
/// Per-`collectFull`-cycle worklist: every reached `WeakRef`
/// object (`is_weak_ref`). Cleared at the start of each
/// `collectFull`. Used by the post-mark pass to clear a
/// `weak_ref_target` whose referent did not survive the trace.
weak_refs_seen: std.ArrayListUnmanaged(*JSObject) = .empty,
/// Per-`collectFull`-cycle worklist: every reached object that
/// carries a `WeakMap` / `WeakSet` `[[MapData]]` / `[[SetData]]`
/// with `is_weak == true`. Drives the ephemeron fixpoint and
/// the post-mark entry-pruning pass.
weak_collections_seen: std.ArrayListUnmanaged(*JSObject) = .empty,
/// Per-`collectFull`-cycle worklist: every reached
/// `FinalizationRegistry` object (`finalization_cells`).
/// The post-mark pass walks each cell and, for a dead target,
/// enqueues the cleanup job and tombstones the cell.
finalization_registries_seen: std.ArrayListUnmanaged(*JSObject) = .empty,
/// Deferred-mark worklists — values / environments whose
/// traversal would otherwise blow the call stack. Three
/// recursion chains that overflow at ~5-10k frames under GC
/// pressure pay the worklist cost: (1) Promise reaction chain
/// (`reaction.result_promise`), (2) closure-env chain
/// (`env.slots[i]` is a function whose captured_env contains
/// another function …), (3) proto chain
/// (`obj.prototype` walking up a 10k-deep `Object.create` tower).
/// Items pushed here are processed iteratively by
/// `drainMarkWorklist` at cycle boundaries (before sweep),
/// alternating between the two worklists until both are empty.
/// V8 / JSC / SM ship fully iterative markers; this is a
/// scoped subset covering the chains that actually hit
/// today's stack limit.
mark_worklist: std.ArrayListUnmanaged(Value) = .empty,
/// Companion to `mark_worklist` for `*Environment` traversals.
/// `markEnvironment` pushes `env.parent` here instead of
/// recursing, breaking the markValue ↔ markEnvironment chain
/// that 10k-deep closure scopes would otherwise blow the stack
/// through.
mark_env_worklist: std.ArrayListUnmanaged(*Environment) = .empty,
/// Worklist for `pinString`'s cons-rope descent — the right child
/// of each cons reached while pinning a chunk-constant rope. Used
/// only at `pinChunk` time (compile / realm init), never inside a
/// GC cycle, so it shares no state with the mark worklists.
pin_worklist: std.ArrayListUnmanaged(*JSString) = .empty,
/// §26.2 FinalizationRegistry cleanup-job scheduler context —
/// the `*Realm`, type-erased (the heap can't import realm.zig
/// without a cycle). `null` on a bare `Heap` (unit tests that
/// drive `collectFull` directly), in which case the post-mark
/// pass still tombstones dead cells but queues no job.
finalization_ctx: ?*anyopaque = null,
/// §26.2 cleanup-job scheduler — see `FinalizationEnqueueFn`.
/// Installed by the realm at init via `setFinalizationEnqueue`.
finalization_enqueue_fn: ?FinalizationEnqueueFn = null,
/// Slab allocator for `JSObject` headers — free-list-backed,
/// O(1) per `create`/`destroy` after warmup. Dramatically
/// outperforms going through the general-purpose allocator on
/// the `object_alloc` churn (every literal is a malloc + free
/// pair the GP allocator services through a lock + size-class
/// walk; the pool just pops a header pointer). The pool's
/// arena reclaims everything in one `deinit`; per-object
/// sub-field cleanup goes through `JSObject.deinitFields`
/// before the header returns to the pool.
object_pool: QuarantinedPool(JSObject) = .empty,
/// Slab pool for `Environment` headers. Every JS function call
/// that needs a binding env (params, locals) used to malloc a
/// fresh Environment struct from the general allocator. On a
/// 10M-iteration `class_instantiate.js` samply trace, those
/// `Heap.allocateEnvironment` calls into `Environment.init` were
/// the dominant remaining libsystem_malloc caller (~3 % of CPU)
/// once the JSObject pool had taken JSObject struct allocs out
/// of the hot path. Mirror the JSObject pool's MemoryPool slab:
/// O(1) acquire + release after warmup, no system-allocator
/// round-trip per call. The env's `slots: []Value` still goes
/// through the general allocator because slot counts vary per
/// function and a single-size pool won't cover the spread;
/// that's the next layer of cleanup if profiling shows it
/// still dominates after this lands.
env_pool: QuarantinedPool(Environment) = .empty,
/// Slab pool for `JSString` headers. A tight string-concat loop
/// (`s = s + "x"` × 300k, or any JSON.stringify hot path) used
/// to reach `allocator.create(JSString)` through the GP
/// allocator on every iteration — ~600k mallocs in the
/// `string_concat` micro alone (one per `(i&0xff).toString()`,
/// one per cons-node build). Mirror the JSObject / Environment
/// pool layout: a `MemoryPool` slab for the fixed-size header,
/// the byte payload still goes through `bytes_allocator`
/// because string lengths vary too much for a single-size pool
/// to cover them. The byte buffer is freed before the header
/// returns to the pool — see the `JSString` branches in
/// `sweepList` and `promoteYoungList`.
string_pool: QuarantinedPool(JSString) = .empty,
/// Slab pool for `PromiseReactionStore` nodes — every pending
/// promise that gains a reaction or waiter allocates one; a
/// `.then` chain allocates one per link, so the libc round-trip
/// was a measurable slice of promise-heavy profiles. Same
/// lifecycle as the sibling pools: `deinitFields` returns nodes
/// via `destroy`, teardown frees the slabs wholesale.
promise_store_pool: std.heap.MemoryPool(@import("object.zig").PromiseReactionStore) = .empty,
/// Slab pools of small dense-element buffers. Literals through ten
/// Values use the exact 80-byte compact class; the remaining fused
/// literals use the 128-byte class. The sweep returns each buffer
/// by its exact capacity; a compact buffer can promote once to the
/// fused class, then growth past that migrates to general-purpose
/// storage (see `JSObject.brand.elements_pooled`).
compact_element_buf_pool: std.heap.MemoryPool([compact_element_buf_cap]Value) = .empty,
element_buf_pool: std.heap.MemoryPool([element_buf_cap]Value) = .empty,
/// Cache of pinned `JSString`s for the decimal forms of small
/// non-negative integers `[0, small_int_cache_max)`. Number-to-
/// string on a small integer (`(i & 0xff).toString()`, an array
/// index, an HTTP status, a byte value) is extremely common and
/// otherwise allocates a fresh 1-3 byte `JSString` every call.
/// Lazily populated and pinned permanently: strings are immutable
/// and `===`-compared by value, so handing the SAME instance to
/// every caller is unobservable (string identity isn't visible to
/// JS). See `smallIntString`.
small_int_strings: [small_int_cache_max]?*JSString = @splat(null),
/// Tiny structural memo cache for `allocateConsString`. The redundant
/// operand pointers make a miss only two inline pointer-pair checks;
/// `result` is the sole marked strong root and keeps both keys live
/// through its immutable `.cons` payload. Flatten invalidates the whole
/// entry before destroying those edges. `shallow_cons_next` is the
/// oldest slot when both are populated, giving deterministic FIFO
/// replacement.
shallow_cons_cache: [shallow_cons_cache_size]?ShallowConsCacheEntry = @splat(null),
shallow_cons_next: u1 = 0,
shallow_cons_miss_streak: u8 = 0,
shallow_cons_bypass_remaining: u8 = 0,
pub fn init(allocator: std.mem.Allocator) Heap {
return .{
.allocator = allocator,
.bytes_allocator = allocator,
.shapes = ShapeTree.init(allocator) catch unreachable,
};
}
/// Same as `init` but with a distinct allocator for large heap-
/// owned byte payloads (`JSString.bytes`, ArrayBuffer slabs).
/// See `bytes_allocator` for the motivation.
pub fn initWithBytesAllocator(
allocator: std.mem.Allocator,
bytes_allocator: std.mem.Allocator,
) Heap {
return .{
.allocator = allocator,
.bytes_allocator = bytes_allocator,
.shapes = ShapeTree.init(allocator) catch unreachable,
};
}
/// Set the GC pressure threshold from a single harness knob
/// (`--gc-threshold=<n>`). `n` becomes the *minor* threshold —
/// a young collection fires every `n` allocations — and the
/// *major* count threshold is set to `n * full_every_n_minor`
/// so a full cycle still lands on the count path at the same
/// total allocation cadence as before the two-tier split,
/// while the minor-cycle counter promotes to full every
/// `full_every_n_minor` minor cycles regardless. The upshot:
/// `--gc-threshold=1` collects (minor) on every allocation and
/// runs a full cycle every `full_every_n_minor`-th — the exact
/// stress profile the generational collector needs exercised.
pub fn setGcThreshold(self: *Heap, n: u32) void {
self.gc_young_threshold = n;
self.gc_threshold = n *| self.full_every_n_minor;
}
/// Set the sweep-quarantine depth (`FUZZ_GC_QUARANTINE=<n>`) on
/// every slab-pooled kind. A swept `JSObject` / `JSString` /
/// `Environment` header then stays poisoned and out of the pool
/// free-list for `n` collection cycles before its slot can be
/// reused — widening the window in which a dangling pointer to a
/// swept header faults instead of reading a freshly-reused live
/// object. `0` disables (the production default; pools behave as
/// bare `MemoryPool`s). A fuzzing knob, complementary to
/// `setGcThreshold` — see `QuarantinedPool` and docs/fuzzing.md.
pub fn setGcQuarantine(self: *Heap, n: u32) void {
self.gc_quarantine_cycles = @min(n, QuarantinedPool(JSObject).max_cycles);
self.object_pool.setQuarantine(n);
self.string_pool.setQuarantine(n);
self.env_pool.setQuarantine(n);
}
/// Advance every pooled kind's quarantine ring one step, flushing
/// the cohort that has now waited `gc_quarantine_cycles` cycles.
/// Called once at the top of each collection's sweep phase; a
/// no-op when quarantine is disabled.