forked from WebKit/WebKit
-
Notifications
You must be signed in to change notification settings - Fork 56
Expand file tree
/
Copy pathConcurrentButterfly.cpp
More file actions
4204 lines (3923 loc) · 243 KB
/
Copy pathConcurrentButterfly.cpp
File metadata and controls
4204 lines (3923 loc) · 243 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
/*
* Copyright (C) 2026 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "ConcurrentButterfly.h"
// ConcurrentButterfly.cpp - runtime slow paths of the shared-memory-threads
// object model (SPEC-objectmodel.md, frozen rev 14). This translation unit
// owns (Task 5):
//
// - the §9.3 exported spine/fragment accessors (declared in
// ConcurrentButterfly.h since Task 2; the layout landed in Butterfly.h with
// Task 4);
// - the §10.6 interim stop-the-world veneer + witness flag (integration
// manifest entry 6): g_jsThreadsStubWorldStopped,
// jsThreadsStopTheWorldAndRun, butterflyWorldIsStopped;
// - the §4.2 flat->segmented conversion (convertToSegmentedButterfly):
// step-0 TTL firing, RESTART discipline, zero-copy slice aliasing,
// aliased-allocation base/size recording, and the nuke + 128-bit DCAS
// publication (PA cells: the I36 fenced order instead).
//
// Task 6 (landed below): the §4.3 segmented-transition protocol
// (trySegmentedTransition/segmentedTransition) with the full DCAS-failure
// taxonomy (a)-(d) and RESTART discipline; the N2 locked structure-only
// transition (tryStructureOnlyTransition/structureOnlyTransition); and the
// §9.5 full-§2-dispatch slow paths JSObjectWithButterfly::getDirectConcurrent
// / putDirectConcurrent (M7-conforming, I34 poll-free).
//
// Task 6b (landed below): the §4.5 GC visit (visitSegmentedButterfly,
// explicitly instantiated for SlotVisitor/AbstractSlotVisitor; the segmented
// branches of visitButterflyImpl live in owned JSObject.cpp) and the I25
// barrier audit (recorded at the definition).
//
// Task 7 (landed below): ensureSharedWriteBit - the §3 foreign first write:
// F1 fire-then-DCAS under the §3.0 merge loop (I12), the §4.6 ArrayStorage
// per-event-stop publication, the §4.8/I35 CoW materialize-first path, the
// I36 PA cell-locked 64-bit flip, and the R-DOUBLE (§4.7) no-rebox rule.
//
// Task 8 (landed below): casButterfly + the §4.4 array-CAS plumbing - the
// frozen §9.3 casButterfly with the I27/I17 assert set, the cell-locked §4.6
// AS-COPY publication form (publishArrayStorageButterflyLocked), the T2
// replacement-spine growth (tryGrowSegmentedVectorLength), the flag-on
// ensureLengthSlow/reallocateAndShrinkButterfly drivers (T1/T2; the former T5
// in-place vectorLength growth was REMOVED in review round 1 - flat
// vectorLengths are immutable flag-on), and the §9.5 indexed slow
// paths JSObjectWithButterfly::getIndexConcurrent / putIndexConcurrent
// (AS accesses cell-locked, I31).
//
// Task 9 (landed below): the §6 per-server-heap quarantine-epoch registry
// (ButterflyQuarantineEpochs - Lock + stable Heap* -> Atomic<uint64_t> map),
// its §10.4c Heap::addStopTheWorldSafepointHook adapter, and the exported
// butterflyQuarantineEpochSlot / registerButterflyQuarantineEpochHook
// (declared in runtime/PropertyTable.h, their sole runtime consumer).
//
// Flag-off (I22): nothing here is reachable - no spine is ever published, the
// veneer is only called from flag-on paths, and the witness stays false.
#include "ArrayConventions.h"
#include "ArrayStorage.h"
#include "ButterflyInlines.h"
#include "GCDeferralContextInlines.h"
#include "JSCInlines.h"
#include "JSThreadsSafepoint.h"
#include "PropertyTable.h"
#include <algorithm>
#include <cstring>
#include <wtf/HashMap.h>
#include <wtf/HashSet.h>
#include <wtf/Lock.h>
#include <wtf/NeverDestroyed.h>
#include <wtf/Threading.h>
#include <wtf/Vector.h>
WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN
namespace JSC {
// ===== §9.3 spine/fragment accessors (frozen signatures; layout = Butterfly.h) =====
ButterflyFragment* spineOutOfLineFragment(ButterflySpine* spine, unsigned fragmentIndex)
{
spine->tsanConsume(); // V7: import the publisher's clock (see Butterfly.h).
return spine->outOfLineFragment(fragmentIndex);
}
ButterflyFragment* spineIndexedFragment(ButterflySpine* spine, unsigned fragmentIndex)
{
spine->tsanConsume(); // V7
return spine->indexedFragment(fragmentIndex);
}
WriteBarrierBase<Unknown>* segmentedOutOfLineSlot(ButterflySpine* spine, PropertyOffset offset)
{
// Precondition (I33, out-of-line clause): the 0-based out-of-line index
// < butterflyFragmentSlots * outOfLineFragmentCount; the member ASSERTs it.
// (offsetInOutOfLineStorage() is the NEGATIVE PropertyStorage index, not
// the spine index - see outOfLineButterflyIndex in ConcurrentButterfly.h.)
ASSERT(isOutOfLineOffset(offset));
spine->tsanConsume(); // V7
return spine->outOfLineSlot(outOfLineButterflyIndex(offset));
}
// segmentedIndexedSlot / segmentedPublicLength: ALWAYS_INLINE in
// ConcurrentButterflyInlines.h (T2-segmented-accessors-inline).
void setSegmentedPublicLength(ButterflySpine* spine, uint32_t value)
{
spine->tsanConsume(); // V7
spine->setPublicLength(value); // RELEASE_ASSERTs indexedFragmentCount (C2).
}
// Bounds-checked consumer variants (segmentedOutOfLineSlotIfWithinBounds /
// segmentedIndexedSlotIfReadable / segmentedIndexedSlotIfWithinVectorLength /
// segmentedVectorLength): ALWAYS_INLINE in ConcurrentButterflyInlines.h
// (T2-segmented-accessors-inline — these were the dominant W>=2 serial-phase
// self% in the SCALEBENCH profile).
// ===== Task 10: O3/I20 cell-lock depth witness =====
namespace {
// Per-thread count of JSCellLocks acquired by THIS translation unit's
// protocols. O3: a thread holds at most one JSCellLock; I20/O2 add that no
// §6-ranked lock is held across a safepoint (hence the veneer-entry assert
// below). The counter observes only locks taken here - a foreign thread's
// locks, GC's visitor locks, and unowned runtime sites are invisible to it
// (other threads, or manifest-7/7b audits) - which is exactly the O3
// obligation these protocols carry: each documents "the only cell lock we
// hold".
thread_local unsigned t_cellLocksHeldByConcurrentButterfly = 0;
ALWAYS_INLINE void lockCellChecked(JSCellLock& lock)
{
RELEASE_ASSERT(!t_cellLocksHeldByConcurrentButterfly); // O3: max one JSCellLock per thread; also O2 (no nested blocking acquire).
lock.lock();
++t_cellLocksHeldByConcurrentButterfly;
}
ALWAYS_INLINE void unlockCellChecked(JSCellLock& lock)
{
RELEASE_ASSERT(t_cellLocksHeldByConcurrentButterfly == 1); // Pairing witness (a missed unlock path would trip the next lockCellChecked anyway).
--t_cellLocksHeldByConcurrentButterfly;
lock.unlock();
}
// RAII flavor for the Locker { cellLock() } accessor sites (I31/L5).
struct CellLockDepthScope {
CellLockDepthScope()
{
RELEASE_ASSERT(!t_cellLocksHeldByConcurrentButterfly); // O3
++t_cellLocksHeldByConcurrentButterfly;
}
~CellLockDepthScope()
{
RELEASE_ASSERT(t_cellLocksHeldByConcurrentButterfly == 1);
--t_cellLocksHeldByConcurrentButterfly;
}
};
} // anonymous namespace
// ===== §10.6 interim stop-the-world veneer + witness (manifest entry 6) =====
// Pre-M4 witness. std::atomic<bool> (relaxed accesses) because it is read
// cross-thread: SPEC-jit section 5.6 disjunct 4 reads it through
// JSC_OM_PROVIDES_JSTHREADS_STUB_WITNESS (defined next to the declaration in
// ConcurrentButterfly.h), and compiler/GC threads consult
// butterflyWorldIsStopped() - matching the jit side's atomic depth-counter
// discipline (JSThreadsSafepoint.cpp s_stubWorldStoppedDepth). Deleted at M4
// integration.
std::atomic<bool> g_jsThreadsStubWorldStopped { false };
void jsThreadsStopTheWorldAndRun(VM& vm, const ScopedLambda<void()>& work)
{
// INTERIM STUB until integration manifest M4 (then: real VMManager STWR +
// CS2 GC-conductor bracket). Per jit CS6's preferred option we DELEGATE to
// JSThreadsSafepoint::stopTheWorldAndRun (bytecode/JSThreadsSafepoint.cpp,
// landed by jit Task 1), which RELEASE_ASSERTs the §10.6 single-mutator
// witness contract - vm.currentThreadIsHoldingAPILock() and at most one
// entered VM - and runs `work` inline on this stack, raising its own
// worldIsStopped() depth counter.
//
// ORDERING IS LOAD-BEARING (adversarial-review round 1): the owned witness
// is raised INSIDE the delegated closure, NOT around the delegated call.
// JSThreadsSafepoint::stopTheWorldAndRun begins with `if (worldIsStopped(vm))
// run-inline-and-return`, and its worldIsStopped() consults this witness
// (disjunct 4) - raising the witness before delegating would make EVERY
// outermost OM stop look "already stopped" and silently skip the delegate's
// API-lock + <=1-entered-VM RELEASE_ASSERTs. With the raise inside the
// closure, the delegate's entry checks always execute on the outermost
// call, while butterflyWorldIsStopped() still holds for everything `work`
// does. Genuinely nested veneer calls (R1.h) are still inline: the
// delegate's own depth counter is raised around the closure.
// // THREADS-INTEGRATE(objectmodel)
//
// Caller contract (GT11): entered mutator; no §6-ranked lock (SAL /
// JSCellLock / Structure::m_lock) held - O2: never block on a safepoint
// holding them; `work` must not allocate in the GC heap (O4: pre-allocate
// before requesting the stop, re-validate inside, RESTART on refit).
RELEASE_ASSERT(!t_cellLocksHeldByConcurrentButterfly); // O2/I20 (Task 10): never block on a stop holding a §6-ranked lock; §4.3(b2) bans converting under the cell lock.
// Watchdog context (review round): OM transition stops are the dominant
// requester under property-write storms (e.g. counter-lock); a wedged
// stop must name this requester class instead of crashing context-nil.
JSThreadsSafepoint::ClassAStopWatchdogContext watchdogContext(&vm, "OM transition stop");
JSThreadsSafepoint::stopTheWorldAndRun(vm, scopedLambda<void()>([&] {
bool savedWitness = g_jsThreadsStubWorldStopped.load(std::memory_order_relaxed); // Nested veneer calls (R1.h) just nest.
g_jsThreadsStubWorldStopped.store(true, std::memory_order_relaxed);
work();
g_jsThreadsStubWorldStopped.store(savedWitness, std::memory_order_relaxed);
}));
}
bool butterflyWorldIsStopped(VM& vm)
{
// §9.4 fire-assert predicate (I13): owned stub witness OR the jit
// workstream's disjuncts (VMManager mode Stopped, legacy per-VM GC stop,
// shared-server stop once the heap workstream lands). At M4 integration
// this becomes the jit predicate alone. // THREADS-INTEGRATE(objectmodel)
return g_jsThreadsStubWorldStopped.load(std::memory_order_relaxed) || JSThreadsSafepoint::worldIsStopped(vm);
}
// ===== Per-event-stop claim table (STW dedup; T1-butterfly-stw-growth) =====
//
// Problem: every per-event stop in this file is requested from a lock-free
// re-dispatch loop, so W threads racing the SAME event (the tail migration of
// one object, the F1/F2 TTL fire of one structure, the §4.6 AS SW flip of one
// object) EACH pay a full stop-the-world rendezvous, only for W-1 of the stop
// closures to observe the winner's publication and no-op. The locksites
// profile measured the mode-(b) grow stop below as the single largest STW
// source on the W=16 scale bench for exactly this reason.
//
// Mechanics: a fixed table of striped claim slots keyed by the event's anchor
// pointer (the object, or the structure whose sets are being fired). The
// first requester CASes its key in, pays the stop, and releases. Racers
// (same key - or, rarely, a stripe collision) wait in a STOP-PARTICIPATING
// loop: parkSitePollAndParkForStopTheWorld every iteration, so the holder's
// rendezvous never waits on a spinner (a spinner parks like any mutator -
// no deadlock), re-evaluating the caller's shouldAbandon() predicate
// (typically "the word / TTL set I planned against moved") between polls.
// Abandon => the caller re-dispatches/RESTARTs WITHOUT stopping the world;
// that is precisely the winner-already-published case. On a pure stripe
// collision the predicate stays false and the loop re-attempts the CAS once
// the unrelated holder releases, so progress is guaranteed.
//
// The claim is an OPTIMIZATION ONLY: every stop closure re-validates its
// preconditions inside the stop exactly as before, so correctness never
// depends on holding a claim. Hence the world-stopped BYPASS below: a caller
// already running under a stop (nested veneer calls run inline) must never
// wait - the holder it would wait on may itself be parked by OUR stop and
// could only release after resume.
//
// Lifetime/safety notes: claims are held only across one (pre-revalidated)
// stop request - never across a return (RAII) and never while a §6-ranked
// lock is held (RELEASE_ASSERTed; the wait parks, O2). The key is used only
// as a hash + busy marker, never dereferenced, and the keyed cell is pinned
// while we spin: the spinner's stack references it and parked stacks are
// conservatively scanned. GIL-on the table is dead weight but harmless:
// claim+stop+release happen within one uninterrupted API-lock tenure (the
// stub stop runs inline and never releases the lock), so a held claim is
// never observable by a second thread and the wait loop is unreachable.
// Flag-off (I22): nothing in this TU is reached.
namespace {
constexpr size_t perEventStopClaimStripeCount = 256; // Power of two; collisions only cost a bounded wait + retry.
Atomic<uintptr_t> s_perEventStopClaimStripes[perEventStopClaimStripeCount]; // Static storage: zero-initialized (0 = free).
thread_local bool t_holdsPerEventStopClaim = false;
ALWAYS_INLINE Atomic<uintptr_t>& perEventStopClaimStripe(const void* key)
{
uintptr_t hash = reinterpret_cast<uintptr_t>(key);
hash ^= hash >> 17;
hash *= 0x9e3779b97f4a7c15ull;
hash ^= hash >> 32;
return s_perEventStopClaimStripes[hash & (perEventStopClaimStripeCount - 1)];
}
class PerEventStopClaim {
WTF_MAKE_NONCOPYABLE(PerEventStopClaim);
public:
template<typename ShouldAbandonFunctor>
PerEventStopClaim(VM& vm, const void* key, const ShouldAbandonFunctor& shouldAbandon)
: m_stripe(perEventStopClaimStripe(key))
{
// World already stopped (we are inside a stop closure / a nested
// veneer call): BYPASS - proceed claim-free. Waiting here could
// deadlock (see the header comment), and the nested stop runs inline
// anyway, with every closure re-validating its own preconditions.
if (butterflyWorldIsStopped(vm)) {
m_acquired = true;
m_bypassed = true;
return;
}
RELEASE_ASSERT(!t_holdsPerEventStopClaim); // Claims bracket exactly ONE stop request; nesting outside a stop is a protocol error.
RELEASE_ASSERT(!t_cellLocksHeldByConcurrentButterfly); // O2/I20: the wait below parks; same contract as the veneer itself.
uintptr_t keyBits = reinterpret_cast<uintptr_t>(key);
ASSERT(keyBits); // 0 is the free marker.
while (true) {
if (m_stripe.compareExchangeWeak(static_cast<uintptr_t>(0), keyBits)) {
m_acquired = true;
t_holdsPerEventStopClaim = true;
return;
}
// Stop-participating wait: park for any in-flight window so the
// holder's rendezvous never waits on us, then re-test whether the
// holder's stop already did our work.
JSThreadsSafepoint::parkSitePollAndParkForStopTheWorld(vm);
if (shouldAbandon())
return; // m_acquired stays false: the caller re-dispatches without a stop.
Thread::yield();
}
}
~PerEventStopClaim()
{
if (!m_acquired || m_bypassed)
return;
RELEASE_ASSERT(t_holdsPerEventStopClaim);
t_holdsPerEventStopClaim = false;
m_stripe.store(0, std::memory_order_release); // Publication order: the stop's effects precede the release.
}
bool acquired() const { return m_acquired; }
private:
Atomic<uintptr_t>& m_stripe;
bool m_acquired { false };
bool m_bypassed { false };
};
} // anonymous namespace
// ===== Task 9: §6 quarantine-epoch registry (ButterflyQuarantineEpochs) =====
//
// §6 release path (r13: PER-SERVER-HEAP, never a process-global counter -
// manifest entry 4c). One epoch counter per JSC::Heap (server heap). The
// counter is bumped by the §10.4c hook adapter below, which the integrator
// registers via Heap::addStopTheWorldSafepointHook at VM/Heap init, BEFORE a
// second client can attach (heap §9): the hook runs once per collection of
// THAT heap, in BOTH protocols (legacy runEndPhase and shared-mode step 7),
// while the world is stopped. PropertyTable entries stamp the OWNING heap's
// epoch at deletion; a stamp strictly below the current epoch proves at least
// one world-stopped window separates the deletion from the reuse attempt, so
// every mutator passed a safepoint and no stale offset/slot pointer survives
// (I18, leaning on I34's no-poll rule). The exported accessors are declared in
// runtime/PropertyTable.h (their sole runtime consumer).
namespace {
struct ButterflyQuarantineEpochs {
Lock lock;
// Heap* -> boxed Atomic<uint64_t>. The Atomic is heap-allocated (boxed in
// a unique_ptr) so its ADDRESS is stable across map rehashes: PropertyTable
// caches the slot pointer at first quarantine (§6) and reads it lock-free
// forever after. Entries are NEVER removed - a destroyed Heap's slot is
// simply retired in place, and a recycled Heap* address re-adopts the old
// slot, which stays sound: the counter is monotone, and stamps taken from
// a slot are only ever compared against that same slot's later values.
UncheckedKeyHashMap<JSC::Heap*, std::unique_ptr<Atomic<uint64_t>>> slots WTF_GUARDED_BY_LOCK(lock);
UncheckedKeyHashSet<JSC::Heap*> hookRegistered WTF_GUARDED_BY_LOCK(lock);
};
ButterflyQuarantineEpochs& butterflyQuarantineEpochs()
{
static NeverDestroyed<ButterflyQuarantineEpochs> epochs;
return epochs.get();
}
// §10.4c hook adapter: runs world-stopped, once per collection of `heap`
// (Heap::runStopTheWorldSafepointHooks contract, heap §9/CR §13.10d). Bumps
// ONLY the collecting heap's slot. Allocation-free after the slot exists; the
// slot is created eagerly at registration below, so the hook itself never
// takes the registry lock on a hot path beyond the get.
void butterflyQuarantineEpochSafepointHook(JSC::Heap& heap)
{
butterflyQuarantineEpochSlot(heap).exchangeAdd(1);
}
} // anonymous namespace
WTF::Atomic<uint64_t>& butterflyQuarantineEpochSlot(JSC::Heap& heap)
{
// Leaf lock (§6 lock-context): callers may hold Structure::m_lock (the
// PropertyTable mutation context, L6) and/or the JSCellLock; this lock is
// inner to both and is held for a hash lookup only - no allocation in the
// GC heap, no safepoint, no other lock acquired under it.
ButterflyQuarantineEpochs& registry = butterflyQuarantineEpochs();
Locker locker { registry.lock };
auto result = registry.slots.ensure(&heap, [] {
return makeUnique<Atomic<uint64_t>>(0);
});
return *result.iterator->value;
}
void registerButterflyQuarantineEpochHook(JSC::Heap& heap)
{
// Idempotent per heap. Heap::addStopTheWorldSafepointHook takes the heap's
// own hook lock, so it is called OUTSIDE the registry lock (which must
// remain a leaf). Racing registrants are harmless here in principle, but
// the manifest-4c call site runs at VM/Heap init (single-threaded for the
// heap), so the add below completes before any quarantine can occur.
{
ButterflyQuarantineEpochs& registry = butterflyQuarantineEpochs();
Locker locker { registry.lock };
if (!registry.hookRegistered.add(&heap).isNewEntry)
return;
// Create the slot eagerly so the world-stopped hook never populates
// the map (allocation-free stop windows, O4-adjacent hygiene).
registry.slots.ensure(&heap, [] {
return makeUnique<Atomic<uint64_t>>(0);
});
}
heap.addStopTheWorldSafepointHook(&butterflyQuarantineEpochSafepointHook);
}
// ===== §4.2 flat -> segmented conversion =====
namespace {
// --- Cell-word atomics. Bytes [0,16) of the cell = {8B header, 8B tagged
// butterfly word} (GT#3); the header's little-endian lanes are
// structureID [0,4), indexingTypeAndMisc [4], type [5], flags [6],
// cellState [7] (static_asserted in ConcurrentButterfly.h).
ALWAYS_INLINE Atomic<uint64_t>* cellHeaderAtomic(JSCell* cell)
{
return reinterpret_cast<Atomic<uint64_t>*>(cell);
}
ALWAYS_INLINE Atomic<uint32_t>* structureIDAtomic(JSCell* cell)
{
static_assert(!JSCell::structureIDOffset());
return reinterpret_cast<Atomic<uint32_t>*>(cell);
}
ALWAYS_INLINE Atomic<uint64_t>* butterflyWordAtomic(JSObjectWithButterfly* object)
{
return reinterpret_cast<Atomic<uint64_t>*>(object->butterflyAddress());
}
ALWAYS_INLINE uint8_t headerByte(uint64_t header, unsigned byteOffset)
{
return static_cast<uint8_t>(header >> (8 * byteOffset));
}
ALWAYS_INLINE uint64_t withHeaderByte(uint64_t header, unsigned byteOffset, uint8_t value)
{
uint64_t mask = 0xffULL << (8 * byteOffset);
return (header & ~mask) | (static_cast<uint64_t>(value) << (8 * byteOffset));
}
ALWAYS_INLINE uint64_t withStructureIDLane(uint64_t header, uint32_t idBits)
{
return (header & ~0xffffffffULL) | idBits;
}
// The DCAS's desired header: `fresh` (whose structureID lane is nuked, and
// whose volatile bytes are the freshest read - I26) with the semantic bytes a
// structure transition owns rewritten: un-nuked new structureID; and, when the
// trigger carries a new structure, the shape bits / m_type / m_flags exactly
// as JSCell::setStructure computes them (lock bits 0xC0 of the indexing byte
// are volatile and copied from fresh - §3.0).
ALWAYS_INLINE uint64_t headerForPublication(uint64_t fresh, StructureID newStructureID, Structure* newStructureOrNull)
{
uint64_t header = withStructureIDLane(fresh, newStructureID.bits());
if (newStructureOrNull) {
uint8_t indexingByte = headerByte(header, JSCell::indexingTypeAndMiscOffset());
uint8_t newIndexingByte = static_cast<uint8_t>((indexingByte & ~AllArrayTypesAndHistory) | newStructureOrNull->indexingModeIncludingHistory());
header = withHeaderByte(header, JSCell::indexingTypeAndMiscOffset(), newIndexingByte);
header = withHeaderByte(header, JSCell::typeInfoTypeOffset(), static_cast<uint8_t>(newStructureOrNull->typeInfo().type()));
header = withHeaderByte(header, JSCell::typeInfoFlagsOffset(),
TypeInfo::mergeInlineTypeFlags(newStructureOrNull->typeInfo().inlineTypeFlags(), headerByte(header, JSCell::typeInfoFlagsOffset())));
}
return header;
}
// I8 debug validation for a conversion spine whose out-of-line side may carry
// FRESH (non-aliased) tail fragments (the trigger's new structure grew
// out-of-line capacity). validateSpineAliasesFlatButterfly (ButterflyInlines.h)
// covers the exact-aliasing case; this sweeps only the aliased prefix.
void validatePartiallyAliasedSpine(const ButterflySpine* spine, Butterfly* flat, size_t aliasedOutOfLineCapacity, bool hasIndexingHeader)
{
if (!ASSERT_ENABLED && !verifyConcurrentButterflyEnabled())
return;
spine->validateConsistency();
char* base = flat->pointer();
for (size_t k = 0; k < aliasedOutOfLineCapacity; ++k)
RELEASE_ASSERT(reinterpret_cast<char*>(spine->outOfLineSlot(static_cast<unsigned>(k))) == base - 16 - sizeof(EncodedJSValue) * k);
if (hasIndexingHeader) {
// C4 holds by ADDRESS identity: the slot-0 assert below proves the
// spine's live publicLength (fragment 0 slot 0, low half — half
// layout static_asserted against IndexingHeader in Butterfly.h) and
// flat->publicLength() are the SAME 4 bytes at B - 8. Do NOT
// value-compare two reads of that live word: a lock-free in-bounds
// dense grower (Butterfly::bumpPublicLengthToAtLeast) legally
// advances it between the reads even while we hold the cell lock —
// §4.4 in-bounds stores touch neither the butterfly word nor the
// structureID, so neither the step-3 re-read nor the DCAS-failure
// taxonomy excludes them. The old equality assert was a TOCTOU; it
// was also a TSAN-visible data race, since flat->publicLength() is a
// plain (non-atomic) IndexingHeader load racing the grower's CAS.
// Note the RETAINED I9b assert below is likewise a same-address
// double read (high half of B - 8) when aliased; it is sound only
// because flat vectorLength is immutable flag-on (every bound
// increase publishes fresh storage, caught by the step-3 word
// re-read/DCAS) and the grower CAS is a 32-bit low-half-only RMW
// that cannot tear the high half. If I9b ever flakes under load,
// apply the same TOCTOU-vs-invariant analysis before suspecting the
// engine.
RELEASE_ASSERT(reinterpret_cast<char*>(&spine->indexedFragment(0)->slots[0]) == base - 8);
RELEASE_ASSERT(spine->frozenFlatVectorLength() == flat->vectorLength()); // I9b: frozen high half vs immutable flat VL — stable, keep.
for (uint32_t i = 0; i < spine->vectorLengthConcurrent(); ++i)
RELEASE_ASSERT(reinterpret_cast<char*>(spine->indexedSlot(i)) == base + sizeof(EncodedJSValue) * static_cast<size_t>(i));
}
}
} // anonymous namespace
// §4.2: zero-copy flat->segmented conversion. ONE publication: when the
// trigger is a transition (newStructureOrNull != nullptr, adding the
// out-of-line property at `offset` with `value`), the single DCAS publishes
// {new structure, fully sized spine} - never an intermediate {old structure,
// undersized spine}. newStructureOrNull == nullptr is the in-place form (T2
// array-resize trigger: the structure is unchanged; offset/value are ignored,
// pass invalidOffset / JSValue()).
//
// Returns the published spine on success. Returns nullptr for RESTART (§4.2):
// the caller must re-enter the WHOLE operation from §2 dispatch on the fresh
// tag + structureID (fresh target computation, fresh F1/F2 checks, fresh
// allocation) - lock-free at restart. nullptr covers: step-0 fired (the world
// changed under the stop), the structure changed before/under the lock, a
// racing conversion already published a spine (re-dispatch lands on §4.3), or
// the butterfly vanished/was replaced incompatibly.
ButterflySpine* convertToSegmentedButterfly(VM& vm, JSObjectWithButterfly* object, Structure* expectedSourceOrNull, Structure* newStructureOrNull, PropertyOffset offset, JSValue value)
{
RELEASE_ASSERT(Options::useJSThreads());
ASSERT(vm.currentThreadIsHoldingAPILock());
RELEASE_ASSERT(offset == invalidOffset || isOutOfLineOffset(offset)); // Inline adds are N2 (structureOnlyTransition), never §4.2 step 4.
ASSERT(newStructureOrNull || offset == invalidOffset); // A value store needs the transition that exposes it.
RELEASE_ASSERT(!newStructureOrNull || expectedSourceOrNull); // AB18-S2: a transition trigger must name the source it derived the target from. RELEASE: the stale-parent guard below is load-bearing only when the source is named; a future caller (e.g. JIT-tier E4 emission, SPEC-jit 5.5) passing a transition with a null source would silently reopen the I21 lost-add window in release builds.
// Planning-time source (§4.2 step 3 compares the re-read structureID
// against this). A nuked ID here means a racing E4 publication is mid
// flight - re-dispatch on the settled state.
StructureID sourceID = object->structureID(); // RAW bits (M5).
if (sourceID.isNuked())
return nullptr; // RESTART
// AB18-S2 stale-parent guard (I21, see the header comment): a transition
// trigger may publish newStructureOrNull only while the object still has
// the structure the target was derived FROM. A racing transition that
// settled between the caller's source check and this capture would
// otherwise be silently erased - this function would validate (and
// nuke-CAS) against the racer's fresh structureID while publishing a
// target whose lineage lacks the racer's add (lost property, I21).
if (expectedSourceOrNull && sourceID != expectedSourceOrNull->id())
return nullptr; // RESTART: re-derive the target from the settled source.
Structure* sourceStructure = sourceID.decode();
// I31: ArrayStorage never segments (its conversions are per-event STW that
// publish FLAT, §4.6). I35: CoW materializes a private flat butterfly
// (§4.8) before any §4.2 protocol; CoW words never reach this function.
RELEASE_ASSERT(!hasAnyArrayStorage(sourceStructure->indexingType()));
RELEASE_ASSERT(!isCopyOnWrite(sourceStructure->indexingMode()));
if (newStructureOrNull) {
// §4.2 covers property transitions; indexing-SHAPE transitions are full
// §4.3 (T4) and Double-touching shape changes are per-event STW (§4.7).
ASSERT(newStructureOrNull->indexingModeIncludingHistory() == sourceStructure->indexingModeIncludingHistory());
ASSERT(newStructureOrNull->hasIndexingHeader(object) == sourceStructure->hasIndexingHeader(object));
}
// ---- Step 0: source/target TTL sets still valid => fire F2 (both sets on
// S and target) in-closure under the §10.6 veneer, BEFORE any lock
// (O2/I13/I10b); after the stop returns, RESTART (the closure allocates
// nothing - O4). I10: foreign butterfly transitions (incl. element
// resizes) fire both sets under STW before producing a segmented object.
{
auto anySetStillValid = [&]() {
if (sourceStructure->transitionThreadLocalIsStillValid() || sourceStructure->writeThreadLocalIsStillValid())
return true;
return newStructureOrNull
&& (newStructureOrNull->transitionThreadLocalIsStillValid() || newStructureOrNull->writeThreadLocalIsStillValid());
};
if (anySetStillValid()) {
// STW dedup (T1-butterfly-stw-growth): W threads converting
// instances of the same structure race this fire; the sets are
// monotone, so the winner's single stop does everyone's work.
// Losers park-wait, observe the fired sets via shouldAbandon, and
// RESTART without a stop (identical caller contract: every path
// out of this block is `return nullptr`).
PerEventStopClaim claim(vm, sourceStructure, [&] { return !anySetStillValid(); });
if (claim.acquired() && anySetStillValid()) {
jsThreadsStopTheWorldAndRun(vm, scopedLambda<void()>([&] {
// Re-check inside the stop: a racing fire may have got here first.
if (sourceStructure->transitionThreadLocalIsStillValid() || sourceStructure->writeThreadLocalIsStillValid())
sourceStructure->fireTransitionThreadLocal(vm, "F2: flat->segmented conversion (foreign or shared-write transition)");
if (newStructureOrNull
&& (newStructureOrNull->transitionThreadLocalIsStillValid() || newStructureOrNull->writeThreadLocalIsStillValid()))
newStructureOrNull->fireTransitionThreadLocal(vm, "F2: flat->segmented conversion (transition target)");
}));
}
return nullptr; // RESTART (after our stop, or the racing fire that abandoned ours - the world changed either way).
}
}
const bool isPA = object->isPreciseAllocation(); // I36: no 16B DCAS at 8-mod-16 bases.
JSCellLock& cellLock = object->cellLock();
// Capacities are structure-determined and stable while the structureID
// check holds (re-verified under the lock each iteration).
size_t aliasedOutOfLineCapacity = sourceStructure->outOfLineCapacity();
size_t newOutOfLineCapacity = newStructureOrNull ? newStructureOrNull->outOfLineCapacity() : aliasedOutOfLineCapacity;
RELEASE_ASSERT(newOutOfLineCapacity >= aliasedOutOfLineCapacity);
RELEASE_ASSERT(!(newOutOfLineCapacity % butterflyFragmentSlots)); // C1 for the grown side too.
bool hasIndexingHeader = sourceStructure->hasIndexingHeader(object);
uint32_t aliasedOutOfLineFragments = aliasedOutOfLineFragmentCountForConversion(aliasedOutOfLineCapacity); // C1 RELEASE_ASSERT inside.
uint32_t totalOutOfLineFragments = static_cast<uint32_t>(newOutOfLineCapacity / butterflyFragmentSlots);
uint32_t freshOutOfLineFragments = totalOutOfLineFragments - aliasedOutOfLineFragments;
while (true) {
// Review round 4 (blocker fix): hold DeferGC across the WHOLE
// allocate-to-publication window (O1's sanctioned pre-lock form, like
// Task 8's unshiftCountSlowCase). Without it, a collection triggered by
// a later allocation (or while parked on the cell lock) sweeps fresh
// fragments that are reachable only from the heap-spilled
// freshFragments buffer (Vector<,4> spills to fastMalloc beyond 4 -
// invisible to the conservative scan) or only from the UNPUBLISHED
// spine (a stack-pinned auxiliary cell's CONTENTS are never traced;
// spines are visited only via the owning object after publication).
// The destructor runs at each refit/return boundary, so deferred
// collections make progress between attempts.
DeferGC deferGC(vm);
// ---- Step 1: allocate the spine (+ the fresh fragments the trigger
// needs) OUTSIDE the lock (O1). Indexed fragment count is planned from
// an unlocked read and re-validated under the lock (refit => back
// here). Spines/fragments are GC-auxiliary allocations (I25); until
// publication they are kept alive by the DeferGC above - stack reach
// alone is NOT enough (see the round-4 note): fragment pointers spill
// to fastMalloc and unpublished spine contents are never traced.
uint32_t plannedVectorLength = 0;
{
uint64_t plannedWord = butterflyWordAtomic(object)->load(std::memory_order_seq_cst);
ButterflyRegime regime = butterflyRegimeForWord(plannedWord);
if (regime != ButterflyRegime::Flat && regime != ButterflyRegime::FlatShared)
return nullptr; // RESTART: nothing flat to convert (None => N3 install path; Segmented => §4.3).
if (hasIndexingHeader)
plannedVectorLength = untaggedButterfly(plannedWord)->vectorLength();
}
uint32_t plannedIndexedFragments = aliasedIndexedFragmentCountForConversion(hasIndexingHeader, plannedVectorLength); // C2
uint32_t allocatedTotalFragments = totalOutOfLineFragments + plannedIndexedFragments;
ButterflySpine* spine = static_cast<ButterflySpine*>(
vm.auxiliarySpace().allocate(vm, ButterflySpine::allocationSize(allocatedTotalFragments), nullptr, AllocationFailureMode::Assert));
Vector<ButterflyFragment*, 4> freshFragments;
freshFragments.reserveInitialCapacity(freshOutOfLineFragments);
for (uint32_t j = 0; j < freshOutOfLineFragments; ++j) {
auto* fragment = static_cast<ButterflyFragment*>(
vm.auxiliarySpace().allocate(vm, sizeof(ButterflyFragment), nullptr, AllocationFailureMode::Assert));
for (size_t slotIndex = 0; slotIndex < butterflyFragmentSlots; ++slotIndex)
fragment->slots[slotIndex].clear(); // Beyond outOfLineSize: never value-visited (§4.5 step 4), cleared for safety.
freshFragments.append(fragment);
}
// ---- Step 2: acquire the cell lock (L2; O3: the only cell lock we hold).
lockCellChecked(cellLock); // O3/I20 depth witness (Task 10)
bool refit = false;
bool restart = false;
ButterflySpine* published = nullptr;
// ---- Step 3 (also the re-entry point of the §4.3 DCAS-failure
// taxonomy's (c): "goto 3", including the refit escape).
while (true) {
uint32_t rawStructureIDBits = structureIDAtomic(object)->load(std::memory_order_seq_cst);
if (rawStructureIDBits != sourceID.bits()) {
// A locked transition (or anything else) changed the structure
// in the window: RESTART (fresh target, fresh F1/F2 checks,
// fresh allocation). I10b's re-check under the lock.
restart = true;
break;
}
uint64_t expectedWord = butterflyWordAtomic(object)->load(std::memory_order_seq_cst);
if (isSegmentedButterfly(expectedWord)) {
// A racing conversion completed before we locked: unlock,
// retry segmented (the caller's re-dispatch lands on §4.3).
restart = true;
break;
}
if (!(expectedWord & butterflyPointerMask)) {
restart = true; // Butterfly vanished (cannot happen today; defensive RESTART).
break;
}
Butterfly* flat = untaggedButterfly(expectedWord);
// Re-read flat vectorLength/publicLength under the lock. Flag-on
// a published flat butterfly's vectorLength is IMMUTABLE (the T5
// in-place growth was removed in review round 1; every bound
// increase publishes fresh storage), and the word re-check above
// pins the payload, so this read is stable until we unlock
// (§4.2-3/history §16.1).
uint32_t flatVectorLength = hasIndexingHeader ? flat->vectorLength() : 0;
uint32_t indexedFragments = aliasedIndexedFragmentCountForConversion(hasIndexingHeader, flatVectorLength); // C2
if (totalOutOfLineFragments + indexedFragments > allocatedTotalFragments) {
// Counts no longer fit the step-1 spine: unlock, goto step 1.
refit = true;
break;
}
// ---- T3-segmented-born-fullcoverage: publish FULL COVERAGE
// (vectorLength == indexedFragments*4 - 1) so the very first
// tryGrowSegmentedVectorLength on this spine takes the lock-free
// mode-(a) CAS instead of a mode-(b) per-event STW. The flag-on
// optimalContiguousVectorLength alignment (ButterflyInlines.h)
// sizes every fresh contiguous flat butterfly so (1+flatVL)%4 == 0
// and there is NO C2 tail to begin with - that is the path the
// scalebench Phase-A arrays take. The branch below additionally
// covers butterflies sized by other paths whose size-class
// rounding left tail slack: it hole-fills the aliased tail
// [flatVectorLength, coveredVectorLength) ONLY when those bytes
// provably lie inside the flat butterfly's heap cell
// (availableContiguousVectorLength re-derives the cell's usable
// VL from the same MarkedSpace::optimalSizeFor the creator's
// allocator used; C3 + the structure-pinned outOfLineCapacity
// make the recorded aliased size the exact requested size). The
// unconditional fill the locksites note suggested would WRITE
// PAST THE HEAP CELL whenever the creator went through
// availableContiguousVectorLength itself (it back-computes VL to
// fill the size class exactly - zero slack), so it is gated. The
// fill races nothing: tail addresses are past flatVectorLength so
// every §4.4 in-bounds store and every flat-side reader (both
// C4-bounded by flat VL) stays clear, and fragment-0 slot-0 (the
// I9b frozen flat-era VL high half + the live publicLength low
// half) is untouched. When neither the alignment nor the slack
// check applies the spine publishes flatVectorLength exactly as
// before and the first grow falls into mode-(b) once - now a
// residual rare path.
uint32_t publishedVectorLength = flatVectorLength;
if (hasIndexingHeader && indexedFragments) {
uint32_t coveredVectorLength = indexedFragments * static_cast<uint32_t>(butterflyFragmentSlots) - 1;
ASSERT(flatVectorLength <= coveredVectorLength);
if (flatVectorLength < coveredVectorLength
&& Butterfly::availableContiguousVectorLength(aliasedOutOfLineCapacity, flatVectorLength) >= coveredVectorLength) {
bool fillDouble = hasDouble(sourceStructure->indexingType()); // structureID re-checked above; I28 relabels change it.
uint64_t hole = fillDouble ? std::bit_cast<uint64_t>(PNaN) : static_cast<uint64_t>(JSValue::encode(JSValue()));
for (uint32_t i = flatVectorLength; i < coveredVectorLength; ++i) {
// Direct flat-address lane store (== spine->indexedSlot(i)
// by I8 once the spine is built). V7: relaxed atomic so
// post-publication segmented readers' relaxed lane loads
// pair; tsanPublish below imports the edge.
WTF::atomicStore(std::bit_cast<uint64_t*>(flat->pointer()) + i, hole, std::memory_order_relaxed);
}
publishedVectorLength = coveredVectorLength;
} else if (flatVectorLength == coveredVectorLength)
publishedVectorLength = coveredVectorLength; // == flatVectorLength; the optimalContiguousVectorLength flag-on path.
}
// ---- Build the spine (private until publication; immutable
// after - I6). Slice-aliasing per the §4.1 equations (I8);
// aliased base/size recorded VERBATIM for GC (§4.5/I7 - the old
// flat allocation's only references after publication are the
// spine's interior fragment pointers).
butterflyConcurrentStore(&spine->outOfLineFragmentCount, totalOutOfLineFragments); // V7: paired with the reader-side relaxed loads (Butterfly.h).
butterflyConcurrentStore(&spine->indexedFragmentCount, indexedFragments);
butterflyConcurrentStore(&spine->vectorLength, publishedVectorLength); // Authoritative live VL (== flatVectorLength, or full coverage per T3 above); the flat-era copy stays frozen in fragment 0 slot 0's high half (I9b) regardless.
butterflyConcurrentStore(&spine->spineEpoch, 1u); // First spine this object publishes; replacements (§4.3-1/T2) copy + increment.
butterflyConcurrentStore(&spine->aliasedAllocationBase, aliasedAllocationBaseForConversion(flat, 0, aliasedOutOfLineCapacity)); // C3 RELEASE_ASSERT inside.
butterflyConcurrentStore(&spine->aliasedAllocationSize, aliasedAllocationSizeForConversion(aliasedOutOfLineCapacity, hasIndexingHeader, static_cast<size_t>(flatVectorLength) * sizeof(EncodedJSValue)));
for (uint32_t j = 0; j < aliasedOutOfLineFragments; ++j)
butterflyConcurrentStore(&spine->fragments()[j], aliasedOutOfLineFragmentForConversion(flat, j));
for (uint32_t j = 0; j < freshOutOfLineFragments; ++j)
butterflyConcurrentStore(&spine->fragments()[aliasedOutOfLineFragments + j], freshFragments[j]);
for (uint32_t f = 0; f < indexedFragments; ++f)
butterflyConcurrentStore(&spine->fragments()[totalOutOfLineFragments + f], aliasedIndexedFragmentForConversion(flat, f));
if (!freshOutOfLineFragments)
validateSpineAliasesFlatButterfly(spine, flat, 0, aliasedOutOfLineCapacity, hasIndexingHeader); // Full I8 sweep.
else
validatePartiallyAliasedSpine(spine, flat, aliasedOutOfLineCapacity, hasIndexingHeader);
// ---- Step 4: the trigger adds a property => release-store its
// value into the fragment slot BEFORE the type/structure publish
// (M2/I9: a reader seeing the new StructureID sees the value).
if (newStructureOrNull && offset != invalidOffset) {
uint64_t outOfLineIndex = outOfLineButterflyIndex(offset);
RELEASE_ASSERT(outOfLineIndex < static_cast<uint64_t>(butterflyFragmentSlots) * totalOutOfLineFragments); // I33 by construction.
WriteBarrierBase<Unknown>* slot = spine->outOfLineSlot(static_cast<unsigned>(outOfLineIndex));
reinterpret_cast<Atomic<uint64_t>*>(slot)->store(JSValue::encode(value), std::memory_order_release);
if (verifyConcurrentButterflyEnabled()) [[unlikely]]
RELEASE_ASSERT(reinterpret_cast<Atomic<uint64_t>*>(slot)->load(std::memory_order_relaxed) == JSValue::encode(value)); // I9/M2 witness (Task 10): value lands BEFORE the type publish.
}
// ---- Step 5: nuke + publish under the §3.0 discipline.
// Publication debug-assert (I11/E1, steps 0/3 made this so): never
// publish (notTTLTID, 1) while transitionThreadLocal is valid.
ASSERT(!sourceStructure->transitionThreadLocalIsStillValid());
ASSERT(!newStructureOrNull || !newStructureOrNull->transitionThreadLocalIsStillValid());
if (verifyConcurrentButterflyEnabled()) [[unlikely]] {
// I10b/I11 witness (Task 10): the step-0 fire preceded this
// lock and the step-3 RESTART re-check held - so no segmented
// word is ever published under a still-valid set.
RELEASE_ASSERT(!sourceStructure->transitionThreadLocalIsStillValid());
RELEASE_ASSERT(!newStructureOrNull || !newStructureOrNull->transitionThreadLocalIsStillValid());
RELEASE_ASSERT(!sourceStructure->writeThreadLocalIsStillValid()); // transitionThreadLocal fire implies writeThreadLocal (§5).
}
StructureID newStructureID = newStructureOrNull ? newStructureOrNull->id() : sourceID;
spine->tsanPublish(); // V7: last pre-publication store done; pairs with tsanConsume() in every segmented* reader (Butterfly.h rationale).
uint64_t spineWord = encodeSegmentedButterfly(spine); // (notTTLTID, SW=1, spine) - I3.
// Nuke: 32-bit CAS structureID -> nuke(old) (M5). The lane holds
// no volatile bytes and we own the semantic bytes under the lock
// (E4 transitioners are excluded - the source's sets are fired -
// so failure is a logic error, §3.0 step 4).
uint32_t previousIDBits = structureIDAtomic(object)->compareExchangeStrong(sourceID.bits(), sourceID.nuke().bits());
RELEASE_ASSERT(previousIDBits == sourceID.bits());
bool reenterStep3 = false;
if (!isPA) {
// 128-bit DCAS {nuked header, expected flat word} ->
// {new un-nuked header, spine word}; seq_cst (M3).
uint64_t nukedHeader = cellHeaderAtomic(object)->load(std::memory_order_seq_cst); // Freshest volatile bytes; ID lane nuked by us.
CellHeaderAndButterfly expected { nukedHeader, expectedWord };
CellHeaderAndButterfly desired { headerForPublication(nukedHeader, newStructureID, newStructureOrNull), spineWord };
while (!dcasHeaderAndButterfly(object, expected, desired)) {
// §4.3 DCAS-failure taxonomy (exhaustive); re-read both words.
uint64_t freshHeader = cellHeaderAtomic(object)->load(std::memory_order_seq_cst);
uint64_t freshWord = butterflyWordAtomic(object)->load(std::memory_order_seq_cst);
// (d) guards: the cell lock excludes competing conversions
// and locked transitions; the nuked ID excludes lock-free
// SW DCASes completing against the header (§3.0 abandons on
// semantic divergence). Any semantic header change here is
// a logic error.
RELEASE_ASSERT(!isSegmentedButterfly(freshWord));
RELEASE_ASSERT(headerDiffersOnlyInVolatileBits(expected.header, freshHeader));
if (untaggedButterfly(freshWord) != untaggedButterfly(expected.taggedButterfly)) {
// (c) A §4.4 array-resize CAS won (I16/I17 - element
// resizes touch only the butterfly word, lock-free).
// Un-nuke and goto step 3: recompute the slices, the
// C2 counts and the aliased base/size against the new
// flat butterfly (refit escape included). Never
// republish an older payload (I27b).
structureIDAtomic(object)->store(sourceID.bits(), std::memory_order_seq_cst);
reenterStep3 = true;
break;
}
// (b1) SW flip by a lock-free foreign first-writer (F1):
// monotone - merge it into the expected word; our desired
// payload is a spine and already carries SW=1. The TID
// cannot change without a payload change.
RELEASE_ASSERT(butterflyTID(freshWord) == butterflyTID(expected.taggedButterfly));
expected.taggedButterfly = freshWord;
// (a) Volatile header bytes (cellState GC CAS, lock parked
// bit) changed: fold the freshest values into expected AND
// desired (I26), retry.
expected.header = mergeVolatileHeaderBits(expected.header, freshHeader);
desired.header = mergeVolatileHeaderBits(desired.header, freshHeader);
}
if (!reenterStep3)
published = spine;
} else {
// I36: PreciseAllocation cells sit at 8-mod-16 addresses - the
// 16B DCAS would fault. Publication is the M8 fenced nuke
// order under the cell lock: structureID CAS -> nuke (done
// above); fence; publish the 64-bit butterfly word (8B-aligned
// - legal); fence; store the new structureID. The butterfly
// word still goes in by CAS (I17: lock-free §4.4 element
// resizes race even under our lock).
WTF::storeStoreFence();
while (true) {
uint64_t previousWord = butterflyWordAtomic(object)->compareExchangeStrong(expectedWord, spineWord, std::memory_order_seq_cst);
if (previousWord == expectedWord) {
published = spine;
break;
}
RELEASE_ASSERT(!isSegmentedButterfly(previousWord)); // (d): impossible under the lock (PA SW flips are cell-locked too, I36).
if (untaggedButterfly(previousWord) != untaggedButterfly(expectedWord)) {
// (c) §4.4 array CAS won: un-nuke, goto step 3.
structureIDAtomic(object)->store(sourceID.bits(), std::memory_order_seq_cst);
reenterStep3 = true;
break;
}
RELEASE_ASSERT(butterflyTID(previousWord) == butterflyTID(expectedWord));
expectedWord = previousWord; // (b1) SW merged; retry.
}
if (published) {
if (newStructureOrNull) {
// Remaining semantic header bytes, as JSCell::setStructure
// would write them (we cannot name the protected members
// from here; lanes are static_asserted in the header).
auto* cellBytes = reinterpret_cast<uint8_t*>(static_cast<JSCell*>(object));
auto* typeByte = reinterpret_cast<Atomic<uint8_t>*>(cellBytes + JSCell::typeInfoTypeOffset());
auto* flagsByte = reinterpret_cast<Atomic<uint8_t>*>(cellBytes + JSCell::typeInfoFlagsOffset());
auto* indexingByte = reinterpret_cast<Atomic<uint8_t>*>(cellBytes + JSCell::indexingTypeAndMiscOffset());
// Round 4: CAS-merge - the per-cell bit lane is volatile (lock-free setPerCellBit).
while (true) {
uint8_t oldFlags = flagsByte->load(std::memory_order_relaxed);
uint8_t newFlags = TypeInfo::mergeInlineTypeFlags(newStructureOrNull->typeInfo().inlineTypeFlags(), oldFlags);
if (oldFlags == newFlags || flagsByte->compareExchangeWeak(oldFlags, newFlags, std::memory_order_seq_cst))
break;
}
typeByte->store(static_cast<uint8_t>(newStructureOrNull->typeInfo().type()), std::memory_order_relaxed);
// The indexing byte's lock bits (0xC0; we HOLD the lock
// bit) and any concurrent parked-bit flip are volatile:
// CAS-merge exactly like JSCell::setStructure.
while (true) {
uint8_t oldValue = indexingByte->load(std::memory_order_relaxed);
uint8_t newValue = static_cast<uint8_t>((oldValue & ~AllArrayTypesAndHistory) | newStructureOrNull->indexingModeIncludingHistory());
if (oldValue == newValue)
break;
if (indexingByte->compareExchangeWeak(oldValue, newValue, std::memory_order_seq_cst))
break;
}
}
WTF::storeStoreFence();
structureIDAtomic(object)->store(newStructureID.bits(), std::memory_order_seq_cst); // Un-nuked new ID; readers stop spinning (M5).
}
}
if (reenterStep3)
continue; // §4.3(c)'s "goto 3" - re-read everything under the still-held lock.
break;
}
// ---- Step 6: release the cell lock.
unlockCellChecked(cellLock);
if (published) {
// Barriers (§4.5): spine/butterfly publication barriers the object
// like setButterfly; the step-4 value store barriers its value.
// Emitted after unlock (no poll between publication and here).
vm.writeBarrier(object);
if (newStructureOrNull) {
vm.writeBarrier(object, newStructureOrNull);
if (offset != invalidOffset)
vm.writeBarrier(object, value);
}
return published;
}
if (restart)
return nullptr; // RESTART: caller re-enters from §2 dispatch, lock-free.
ASSERT(refit);
// Refit (counts grew): the step-1 allocations are discarded
// unreferenced (GC reclaims them); allocate again, unlocked (O1).
}
}
// ===== Task 6: §4.3 transition protocol, N2, and the §9.5 *Concurrent accessors =====
namespace {
// The object's settled (un-nuked) structure: spin past a mid-publication nuke
// (M5; the nuke window is bounded, straight-line - O2), then decode.
Structure* settledStructure(JSCell* cell)
{
while (true) {
StructureID id = cell->structureID(); // RAW bits (M5)
if (!id.isNuked()) [[likely]]
return id.decode();
}
}
ALWAYS_INLINE bool anyTTLSetStillValid(Structure* source, Structure* target)