forked from WebKit/WebKit
-
Notifications
You must be signed in to change notification settings - Fork 56
Expand file tree
/
Copy pathVMManager.cpp
More file actions
1765 lines (1644 loc) · 89.1 KB
/
Copy pathVMManager.cpp
File metadata and controls
1765 lines (1644 loc) · 89.1 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) 2025 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 "VMManager.h"
#include "Heap.h" // UNGIL §A.3 (U-T5): Heap::JSThreadsStopScope (GCL bracket), GCClient::Heap access sampling.
#include "JSCConfig.h"
#include "JSLock.h"
#include "JSThreadsSafepoint.h" // UNGIL §A.3 (U-T5): stop watchdog (annex App. 5.6(d)).
#include "MachineStackMarker.h" // T5-rootscan-skip: DECLARE_AND_COMPUTE_CURRENT_THREAD_STATE at the sibling park sites.
#include "VM.h"
#include "VMEntryScopeInlines.h"
#include "VMLite.h" // UNGIL §A.3.1/EXIT1 (U-T5): the entered-thread set IS the lite registry.
#include "VMLiteShared.h"
#include "VMThreadContext.h"
#include "WasmDebugServerUtilities.h"
#include <atomic>
#include <cstdlib> // BUGHUNT instrumentation (getenv/atexit; NOT FOR LANDING).
#include <mutex> // BUGHUNT instrumentation (call_once; NOT FOR LANDING).
#include <wtf/DataLog.h> // V4 watchdog: arbitration-queue breadcrumb.
#include <wtf/HashMap.h>
#include <wtf/Locker.h>
#include <wtf/MonotonicTime.h>
#include <wtf/NeverDestroyed.h>
#include <wtf/ParkingLot.h> // T6-arbitration-park-not-sleep: §A.3 job-slot park/unpark.
#include <wtf/RunLoop.h>
#include <wtf/Scope.h>
#include <wtf/Threading.h>
namespace JSC {
VM* VMManager::s_recentVM { nullptr };
VMManager& VMManager::singleton()
{
static LazyNeverDestroyed<VMManager> manager;
static std::once_flag onceKey;
std::call_once(onceKey, [] {
manager.construct();
});
return manager.get();
}
VMThreadContext::VMThreadContext() = default;
VMThreadContext::~VMThreadContext() = default;
bool VMManager::isValidVMSlow(VM* vm)
{
bool found = false;
forEachVM([&] (VM& nextVM) {
if (vm == &nextVM) {
s_recentVM = vm;
found = true;
return IterationStatus::Done;
}
return IterationStatus::Continue;
});
return found;
}
void VMManager::dumpVMs()
{
unsigned i = 0;
WTFLogAlways("Registered VMs:");
forEachVM([&] (VM& nextVM) {
WTFLogAlways(" [%u] VM %p", i++, &nextVM);
return IterationStatus::Continue;
});
}
void VMManager::iterateVMs(const Invocable<IterationStatus(VM&)> auto& functor) WTF_REQUIRES_LOCK(m_worldLock)
{
for (auto* context = m_vmList.head(); context; context = context->next()) {
VM& vm = *VM::fromThreadContext(context);
IterationStatus status = functor(vm);
if (status == IterationStatus::Done)
return;
}
}
VM* VMManager::findMatchingVMImpl(const ScopedLambda<VMManager::TestCallback>& test)
{
Locker lock { m_worldLock };
if (s_recentVM && test(*s_recentVM))
return s_recentVM;
VM* result = nullptr;
iterateVMs(scopedLambda<IteratorCallback>([&] (VM& vm) {
if (test(vm)) {
result = &vm;
s_recentVM = &vm;
return IterationStatus::Done;
}
return IterationStatus::Continue;
}));
return result;
}
void VMManager::forEachVMImpl(const ScopedLambda<VMManager::IteratorCallback>& func)
{
Locker lock { m_worldLock };
iterateVMs(func);
}
VMManager::Error VMManager::forEachVMWithTimeoutImpl(Seconds timeout, const ScopedLambda<VMManager::IteratorCallback>& func)
{
if (!m_worldLock.tryLockWithTimeout(timeout))
return Error::TimedOut;
Locker locker { AdoptLock, m_worldLock };
iterateVMs(func);
return Error::None;
}
void VMManager::Info::dump(PrintStream& out) const
{
out.print("VMManager::Info(numberOfVMs:", numberOfVMs);
out.print(", numberOfActiveVMs:", numberOfActiveVMs);
out.print(", numberOfStoppedVMs:", numberOfStoppedVMs);
out.print(", worldMode:", worldMode);
out.print(", targetVM:", RawPointer(targetVM), ")");
}
auto VMManager::info() -> Info
{
Info info;
auto& manager = singleton();
// The reason for locking here is so that we capture a consistent snapshot
// of all the values in info.
Locker lock { manager.m_worldLock };
info.numberOfVMs = manager.m_numberOfVMs;
info.numberOfActiveVMs = manager.m_numberOfActiveVMs;
info.numberOfStoppedVMs = manager.m_numberOfStoppedVMs;
info.worldMode = manager.m_worldMode;
info.targetVM = manager.m_targetVM;
return info;
}
void VMManager::setWasmDebuggerOnStop(StopTheWorldCallback callback)
{
g_jscConfig.wasmDebuggerOnStop = callback;
}
void VMManager::setWasmDebuggerOnResume(PostResumeCallback callback)
{
g_jscConfig.wasmDebuggerOnResume = callback;
}
void VMManager::setMemoryDebuggerCallback(StopTheWorldCallback callback)
{
g_jscConfig.memoryDebuggerStopTheWorld = callback;
}
// THREADS-INTEGRATE(heap) manifest 5d (review round 4): file-local
// Atomic statics, deliberately NOT g_jscConfig slots — JSC::Config lives
// in the WTF::Config page that Config::finalize() (run from every VM
// constructor) mprotects read-only, and the sole installer
// (Heap::noteSharedServerSticky, second-client attach) always runs
// post-freeze; a config store would SIGSEGV at the ISS flip. seq_cst
// Atomic: the install happens-before the ISS flip publishes on the
// installing thread, and the hooks are inert (no-op unless ISS && GSP),
// so a load racing the install correctly no-ops.
static Atomic<void (*)(VM&)> s_gcWillParkInStopTheWorld { nullptr };
static Atomic<void (*)(VM&)> s_gcDidResumeFromStopTheWorld { nullptr };
void VMManager::setGCParkCallbacks(void (*willPark)(VM&), void (*didResume)(VM&))
{
// Heap-owned hooks (JSC::Heap::gcWillParkInStopTheWorld /
// gcDidResumeFromStopTheWorld); may be null (inert).
s_gcWillParkInStopTheWorld.store(willPark);
s_gcDidResumeFromStopTheWorld.store(didResume);
}
#if USE(BUN_JSC_ADDITIONS)
void VMManager::setJSDebuggerCallback(StopTheWorldCallback callback)
{
g_jscConfig.jsDebuggerStopTheWorld = callback;
}
#endif
// ============================================================================
// UNGIL §A.3 (U-T5): thread-granular stop-the-world for the gilOff VM.
//
// Re-freezes jit R1.c "N threads in ONE VM = thread-granular STW", both
// sides. The counting unit is the ENTERED THREAD (§A.3.1): the entered set
// IS the VMLiteRegistry filtered lite->vm == target (EXIT1.1) — there is no
// second entered-thread structure. Every conductor predicate sample RE-WALKS
// the registry under VMLiteRegistry::lock (EXIT1.2); lite/client pointers
// are never cached across samples; the walk is allocation-free and acquires
// nothing; the registry lock is dropped before the conductor blocks between
// samples.
//
// Conductor order (ANNEX HBT4, normative for ALL §A.3 conductors):
// release access (R1.i's first step KEPT) -> §A.3.3 arbitration on the
// park-aware pending-job-slot mutex -> (WINNER ONLY)
// Heap::JSThreadsStopScope (GCL) -> fan stop bits -> stop -> work ->
// resume -> drop scope -> re-acquire access.
// Losers park on the job-slot mutex access-released (they count as parked
// for the winner's predicate through the access sample) and never block raw
// on GCL (HBT4.3: at most one thread — the winner — ever blocks in
// GCL.lock(), access-released).
//
// §LK row 4b — s_jsThreadsJobSlotLock: §A.3-conductors ONLY; inner to
// rank 1/token (the requester holds its entry token entering arbitration;
// tokens are ordering-inert, LK.1); OUTER to heap rank 2 (GCL); held across
// the ENTIRE stop window; never held together with any api rank-1..3 lock.
//
// Deviation recorded (§A.3.1's "m_worldLock held for the window"): conductor
// tenure and window serialization are carried by the job-slot mutex per
// HBT4's reordering — m_worldLock is NOT held across the window (holding it
// would block every notifyVMStop park entry, deadlocking the predicate). The
// Mode-machine state m_worldLock guards is untouched by §A.3 windows: a §A.3
// stop sets NO client-visible GC stop state and never transitions
// VMManager::Mode (§A.3.2b; the keep-parked interplay below).
//
// SB1 contract: the stop word's SOLE accessors are the seq_cst pair below
// (U20 lint shape); the conductor's per-sample access loads execute inside
// the registry-lock hold behind a seq_cst fence — with the client's seq_cst
// F8 CAS / seq_cst RHA exchange on the other side, the C++20 SC-fence rule
// puts all four ops in one total order, which is exactly the SB1.4 proof
// (a fence-assisted relaxed sample is used because GCClient::Heap::
// hasHeapAccess() is a relaxed accessor frozen in Heap.h, outside this
// task's writable set; the proof obligation is discharged identically).
// ============================================================================
// Defined in heap/Heap.cpp (U-T5): per-thread park access pairing.
void gcClientWillParkForThreadGranularStop();
void gcClientDidResumeFromThreadGranularStop();
// Defined in heap/Heap.cpp (T1-gc-siblings-mark): sibling parallel-marking
// assist; true iff a marking phase was open and the sibling drained it.
bool gcSiblingAssistMarkingIfEnabled();
// Defined in heap/Heap.cpp (T5-rootscan-skip-coop-parked-suspend): publish /
// clear the cooperative root snapshot bracketing each pure-park span.
void gcClientPublishParkedRootSnapshot(CurrentThreadState*);
void gcClientClearParkedRootSnapshot();
// Defined in runtime/VMLite.cpp (U-T5): ANNEX ISB1.
void jsThreadsBumpStopGeneration();
void jsThreadsNVSExitInstructionSync();
static Lock s_jsThreadsJobSlotLock; // §A.3.3/HBT4 pending-job-slot mutex (§LK row 4b).
// Park-state stripes (T2 park-protocol rework). The old single
// s_jsThreadsParkLock / s_jsThreadsParkCondition pair funneled every NVS
// ticket, the conductor's predicate wait, AND every quiesce notification
// through ONE Lock and ONE Condition — two ParkingLot queue addresses in one
// hash bucket — and the 1ms-bounded waits re-acquired that one lock per
// timeout per parked thread. Two structural changes:
//
// - STRIPING: park state is sharded across jsThreadsParkStripeCount
// cache-line-separated { lock, condition, generation } stripes; each
// thread picks a stripe at first use (round-robin, stable thereafter),
// so waiter lock traffic and resume wakeup herds no longer funnel
// through one Lock / one ParkingLot bucket.
//
// - LOST-WAKEUP-PROOF UNTIMED WAITS: each stripe carries a generation
// counter, bumped ONLY under that stripe's lock by
// jsThreadsNotifyMutatorQuiesced. A waiter (a) samples its stripe's
// generation, (b) evaluates its predicate OUTSIDE the stripe lock
// (leaf discipline below), (c) re-checks the generation under the lock
// and only then blocks untimed. A notification racing the predicate
// sample either changed the generation (the waiter re-evaluates instead
// of blocking), or — because the bump serializes on the stripe lock the
// waiter holds through Condition::wait()'s queue registration — its
// notifyAll is issued after the waiter is registered and cannot be
// lost. This retires the bounded-1ms-poll backstop for the mutator
// tickets. The ONE remaining timed wait is the conductor's §A.3.2
// predicate wait (see jsThreadsThreadGranularStopTheWorldAndRun): it
// genuinely needs a poll, both to run the V4 watchdog
// (watchdogAssertStopProgress) periodically and to observe heap-access
// releases on paths that do not notify (e.g. a mutator releasing
// access on a plain exit-to-native path).
//
// TRUE LEAF (U20, preserved): nothing — not even the registry lock — is
// ever acquired while ANY stripe lock is held. Waiters evaluate their
// predicates (registry walks, m_worldLock samples) BEFORE taking their
// stripe lock; only the generation re-check and the block run under it.
// Notifiers bump the generation under the stripe lock but issue the
// notifyAll AFTER dropping it (no herd wakeup into a still-held lock).
struct alignas(128) JSThreadsParkStripe {
Lock lock;
Condition condition;
std::atomic<uint64_t> generation { 0 }; // Bumped ONLY under `lock`; sampled lock-free by waiters (seq_cst).
};
static constexpr unsigned jsThreadsParkStripeCount = 32; // Power of two (index mask below).
static JSThreadsParkStripe s_jsThreadsParkStripes[jsThreadsParkStripeCount];
static JSThreadsParkStripe& jsThreadsCurrentThreadParkStripe()
{
static std::atomic<unsigned> s_nextStripeIndex { 0 };
static thread_local unsigned stripeIndex = s_nextStripeIndex.fetch_add(1, std::memory_order_relaxed) & (jsThreadsParkStripeCount - 1);
return s_jsThreadsParkStripes[stripeIndex];
}
static std::atomic<VM*> s_jsThreadsStopWord { nullptr }; // SB1 stop word; seq_cst accessors below ONLY (U20).
static std::atomic<WTF::Thread*> s_jsThreadsConductorThread { nullptr }; // §A.3.3 tenure (thread-keyed, not VM-keyed).
static std::atomic<unsigned> s_jsThreadsWorldStoppedDepth { 0 }; // §J.8 witness: window open AND predicate satisfied.
static std::atomic<uint64_t> s_jsThreadsCompletedWindowCount { 0 }; // V4 watchdog progress token: bumped once per COMPLETED §A.3 window (resume path). Relaxed: heuristic re-arm input only, never a soundness input.
static thread_local unsigned t_jsThreadsConductorDepth { 0 }; // R1.h nesting on the conductor thread.
// The gilOff Mode-machine servicing-thread tenure (§A.3.8: the landed
// per-VM machine keyed m_targetVM on the VM, which is ambiguous with two
// same-VM observers — exactly the double-transition/assert hazard the
// handout cites at :218/:580). Keyed PER VM (review round: §A.3.8 makes each
// gilOff VM ONE counting unit with ITS OWN representative — a single
// process-wide slot would let the first-arriving thread of VM A hold the
// tenure while every thread of VM B parks uncounted, so B's stop never
// reaches m_numberOfStoppedVMs == m_numberOfActiveVMs and a two-gilOff-VM
// Mode stop deadlocks). Guarded by m_worldLock; entries are transient (the
// representative removes its own on exit; notifyVMDestruction sweeps).
static HashMap<VM*, WTF::Thread*>& gilOffServicingThreads() // WTF_REQUIRES_LOCK(m_worldLock) by convention.
{
static NeverDestroyed<HashMap<VM*, WTF::Thread*>> map;
return map.get();
}
// §A.3.2b(i) gate exemption: true while THIS thread is a gilOff VM's elected
// Mode-machine representative (between election and the post-service
// re-acquire) — the representative must re-acquire heap access to run the
// STW callback, and gating it would self-deadlock. Thread-local because the
// exemption is exact for the asking thread and needs no lock.
static thread_local bool t_gilOffModeStopServicer { false };
// U20: the ONLY loads/stores of the stop word (seq_cst, SB1 item 1/3).
static ALWAYS_INLINE VM* jsThreadsStopWordLoad()
{
return s_jsThreadsStopWord.load(std::memory_order_seq_cst);
}
static ALWAYS_INLINE void jsThreadsStopWordStore(VM* vm)
{
s_jsThreadsStopWord.store(vm, std::memory_order_seq_cst);
}
bool jsThreadsStopPendingFor(VM& vm)
{
return jsThreadsStopWordLoad() == &vm;
}
bool jsThreadsCurrentThreadIsStopConductor()
{
return s_jsThreadsConductorThread.load(std::memory_order_seq_cst) == &Thread::currentSingleton();
}
void jsThreadsNotifyMutatorQuiesced()
{
// Wake every stripe. The generation bump under each stripe's lock is the
// lost-wakeup fence (see the stripe banner); the notifyAll itself runs
// OUTSIDE the critical section so woken waiters never herd into a lock
// the notifier still holds, and a waiterless stripe's notifyAll costs
// one atomic load (Condition's hasWaiters fast path).
for (auto& stripe : s_jsThreadsParkStripes) {
{
Locker locker { stripe.lock };
stripe.generation.store(stripe.generation.load(std::memory_order_relaxed) + 1, std::memory_order_seq_cst);
}
stripe.condition.notifyAll();
}
}
// EXIT1.1/EXIT1.2: forEachEnteredThread — THE registry-walk helper; §A.3
// conductor code reaches lites ONLY through it (U20). The functor runs
// inside the registry-lock hold of the walk that found the lite and must not
// let any lite*/client* escape the hold (no caching across samples). The
// walk is allocation-free and acquires nothing.
//
// Entered predicate (EXIT1.4): registered AND state == Live (TEARDOWN or
// absent => EXITED, r28-r30; COLLECTED/DETACHED defensively excluded — they
// are never conductor-visible) AND clientHeap non-null (the write-once
// release-published client pointer; null => not-entered, EXIT1.4(b)). Every
// lite-state read is under the registry lock (r31).
template<typename Functor>
static void forEachEnteredThread(VM& vm, const Functor& functor)
{
auto& registry = VMLiteRegistry::singleton();
Locker locker { registry.lock };
// SB1 item 2: order this sample after the conductor's seq_cst stop-word
// store and against the clients' seq_cst CAS/exchange (SC-fence leg; see
// the banner). One fence per walk suffices — every load below is
// program-ordered after it.
std::atomic_thread_fence(std::memory_order_seq_cst);
for (VMLite* lite : registry.lites) {
if (lite->vm != &vm)
continue; // §A.1.3 filter.
if (lite->state != VMLite::State::Live)
continue; // EXIT1.4(a): counted EXITED before any client deref.
if (!lite->clientHeap)
continue; // EXIT1.4(b): not-entered / no-access.
if (functor(*lite) == IterationStatus::Done)
break;
}
}
static unsigned UNUSED_FUNCTION numberOfEnteredThreads(VM& vm)
{
unsigned count = 0;
forEachEnteredThread(vm, [&](VMLite&) {
++count;
return IterationStatus::Continue;
});
return count;
}
// §A.3.2 conductor predicate, one sample: every entered thread of the target
// VM — other than the conductor itself — is access-released (which subsumes
// parked: gilOff threads release their own client when parking, see
// gcClientWillParkForThreadGranularStop) or not-entered. The conductor's own
// lite is re-derived from TLS per sample, never cached across samples
// (EXIT1.2).
static bool allEnteredThreadsAreQuiescent(VM& vm)
{
VMLite* conductorLite = VMLite::currentIfExists();
bool quiescent = true;
forEachEnteredThread(vm, [&](VMLite& lite) {
if (&lite == conductorLite)
return IterationStatus::Continue; // HBT2.1: the conductor may retain/re-acquire access.
// SB1 item 2 sample (fence-assisted; live client deref is sound
// under the walk's lock hold per EXIT1.4(b)).
if (lite.clientHeap->hasHeapAccess()) {
quiescent = false;
return IterationStatus::Done;
}
return IterationStatus::Continue;
});
return quiescent;
}
// Mode-machine service gating (§A.3.8): a latched debugger STW callback must
// not run while any gilOff mutator other than the servicing thread still
// holds heap access. Cheap: gated on the process-level discriminator.
static bool gilOffMutatorsBlockModeStopService()
{
if (!VM::isGILOffProcess()) [[likely]]
return false;
GCClient::Heap* servicingClient = GCClient::Heap::currentThreadClient();
auto& registry = VMLiteRegistry::singleton();
Locker locker { registry.lock };
std::atomic_thread_fence(std::memory_order_seq_cst);
for (VMLite* lite : registry.lites) {
if (!lite->gilOff)
continue;
if (lite->state != VMLite::State::Live)
continue;
GCClient::Heap* client = lite->clientHeap;
if (!client || client == servicingClient)
continue;
if (client->hasHeapAccess())
return true;
}
return false;
}
void jsThreadsParkForStopWindow(VM& vm)
{
// §A.3.2 NVS ticket. Pre: the calling thread holds NO heap access (the
// §A.3.2b gate reverted it, or the caller released it). Tokens are KEPT
// while parked (§A.3.2b) — that is what makes the access-released
// exemption sound: re-running JS needs re-acquisition, which this
// window's stop word gates.
if (jsThreadsCurrentThreadIsStopConductor())
return; // HBT3.2: a conductor never parks on its own window.
auto& stripe = jsThreadsCurrentThreadParkStripe();
for (;;) {
// Generation-validated untimed wait (stripe banner): the resume path
// clears the word and THEN notifies (bump-under-lock), so a clear
// racing this sample either flips the predicate below or changes the
// generation observed under the stripe lock — never a lost wakeup.
uint64_t generation = stripe.generation.load(std::memory_order_seq_cst);
if (jsThreadsStopWordLoad() != &vm)
break;
Locker locker { stripe.lock };
if (stripe.generation.load(std::memory_order_seq_cst) != generation)
continue; // A notification raced the predicate sample; re-evaluate.
stripe.condition.wait(stripe.lock);
}
// R1.d/ISB1: leaving the NVS ticket executes an unconditional
// context-sync and refreshes the per-thread stop-generation copy.
jsThreadsNVSExitInstructionSync();
}
// SPEC-ungil §A.3.2b(i) (review round): the lite's stop bit — under the
// §A.2.1 interim seam, the VM trap word's NeedStopTheWorld bit — gates FRESH
// heap-access acquisition for MODE-MACHINE stops too, not just §A.3 windows.
// The §A.3.8 service-gating conjunct (gilOffMutatorsBlockModeStopService)
// samples sibling access states and is sound only if a sibling that released
// cannot silently re-acquire and run JS while the debugger STW callback is
// in flight. Consumed by GCClient::Heap::acquireHeapAccess (Heap.cpp seam).
//
// Exemptions: (a) the elected representative (it re-acquires to service);
// (b) the free-running RunOne target VM (landed RunOne semantics resume the
// target wholesale; its trap bit stays set by design); (c) Mode::RunAll
// (a residual or pre-Stopping-sliver bit — poll-site delivery and
// notifyVMActivation cover the sliver; the §A.3 conductor's post-window
// clear/re-check retires residuals).
//
// Cost: one fence-assisted relaxed trap-bit load on the gilOff AHA path; the
// m_worldLock-taking info() snapshot runs only with the bit set (i.e. only
// while some stop is actually in flight or RunOne is active — debugger
// modes, where "peek performance is not a concern" per notifyVMStop).
bool jsThreadsModeStopGatesCurrentThread(VM& vm)
{
if (t_gilOffModeStopServicer)
return false;
// HBT2.1/HBT3.2: an open §A.3 window's conductor may retain/re-acquire
// access inside its own window while holding the job-slot mutex (and
// GCL). Parking IT here would deadlock a Mode stop that arrived
// mid-window: every sibling is ticket-parked on the §A.3 word and none
// reaches the representative election until the window closes — so the
// conductor must finish its window first and parks for the Mode stop at
// its post-window re-acquire instead (tenure is already dropped there).
if (jsThreadsCurrentThreadIsStopConductor())
return false;
// SB1-shape Dekker leg: the fan side is fireTrap's seq_cst RMW followed
// by a fenced access sample; this side is the caller's seq_cst access
// CAS followed by this fenced bit load (the trap-bit accessor is a
// relaxed load frozen in VMTraps.h, outside this task's writable set —
// fence-assisted, same discharge as the banner's hasHeapAccess note).
std::atomic_thread_fence(std::memory_order_seq_cst);
if (!vm.traps().needHandling(VMTraps::NeedStopTheWorld)) [[likely]]
return false;
auto info = VMManager::info();
if (info.worldMode == VMManager::Mode::RunAll)
return false;
if (info.worldMode == VMManager::Mode::RunOne && info.targetVM == &vm)
return false;
return true;
}
void jsThreadsParkForModeStop(VM& vm)
{
// §A.3.2b(i) NVS ticket for Mode-machine stops. Pre: the calling thread
// holds NO heap access (the AHA gate reverted it). The predicate is
// evaluated OUTSIDE the stripe-lock hold (leaf discipline, see the
// stripe banner); the generation re-check under the lock replaces the
// old bounded-wait backstop. Every edge that can flip the gate false
// notifies: resumeTheWorld (trap-bit cancel + RunAll), the §A.3
// conductor's post-window clear/recheck/restore, notifyVMStop's
// Mode::RunOne transition and context-switch retarget, and the
// last-VM-destruction RunAll fix-up — all bump-then-notify, so the
// untimed wait cannot hang on a stale gate.
auto& stripe = jsThreadsCurrentThreadParkStripe();
for (;;) {
uint64_t generation = stripe.generation.load(std::memory_order_seq_cst);
if (!jsThreadsModeStopGatesCurrentThread(vm))
break;
Locker locker { stripe.lock };
if (stripe.generation.load(std::memory_order_seq_cst) != generation)
continue; // A notification raced the predicate sample; re-evaluate.
stripe.condition.wait(stripe.lock);
}
// R1.d/ISB1: same NVS-exit contract as the §A.3 ticket above.
jsThreadsNVSExitInstructionSync();
}
// §J.8 witness for patching asserts (replaces the stub depth counter's role
// post-ungil): true while a §A.3 window is open AND its predicate has been
// satisfied. OPEN (cross-file): JSThreadsSafepoint::worldIsStopped() gains
// this disjunct when the stub is deleted (bytecode/JSThreadsSafepoint.cpp is
// outside U-T5's writable set — see the task summary).
bool jsThreadsThreadGranularWorldIsStopped()
{
return s_jsThreadsWorldStoppedDepth.load(std::memory_order_relaxed);
}
// The real R1.a-i sequence (§A.3, HBT4 order). GIL-off ONLY: gilOn callers
// keep the JSThreadsSafepoint.cpp path. The §A.3.3 licensed reroute has
// LANDED: JSThreadsSafepoint::stopTheWorldAndRun routes here when
// vm.gilOff() (after its R1.h already-stopped inline branch, which now also
// consults jsThreadsThreadGranularWorldIsStopped() so nested fires inside an
// open window run inline), and JSThreadsSafepoint::worldIsStopped() gained
// the §J.8 disjunct. The stub and its entered-VM tripwire remain GIL-on-only.
void jsThreadsThreadGranularStopTheWorldAndRun(VM& vm, const ScopedLambda<void()>& work)
{
RELEASE_ASSERT(vm.gilOff());
#if ASSERT_ENABLED
{
// R1 contract: the requester is an entered mutator of this VM
// (token holder; GIL-off spawned threads hold no m_lock, §F.1, so
// currentThreadIsHoldingAPILock is NOT the right assert here).
VMLite* selfLite = VMLite::currentIfExists();
ASSERT(selfLite && selfLite->vm == &vm);
}
#endif
// R1.h: a nested request on the conductor thread inside its own open
// window runs inline — the world is already stopped for us.
if (t_jsThreadsConductorDepth) {
RELEASE_ASSERT(jsThreadsCurrentThreadIsStopConductor());
++t_jsThreadsConductorDepth;
work();
--t_jsThreadsConductorDepth;
WTF::crossModifyingCodeFence(); // Patcher-side fence for the nested patch (F5).
return;
}
// R1.i step 1 — KEPT FIRST (HBT4.1): release this thread's own client
// access before arbitration, so a losing requester parks access-released
// and the winner's predicate counts it.
GCClient::Heap* selfClient = GCClient::Heap::currentThreadClient();
bool releasedAccess = false;
if (selfClient && selfClient->hasHeapAccess()) {
selfClient->releaseHeapAccess();
releasedAccess = true;
}
// Watchdog budget (V4-stw-watchdog revision): requestStart is sampled
// BEFORE the arbitration park — reaching conductor tenure is part of
// reaching a stopped world, and the pre-tenure leg was previously an
// unbounded, unwatched block. But the budget is PROGRESS-AWARE, not one
// wall-clock window end-to-end: the arbitration leg re-arms whenever
// another conductor COMPLETES a window (a legitimate fire-storm queue
// serializing N full stop windows is progress, not a wedge), and each
// post-tenure leg (GCL bracket; §A.3.2 predicate wait) gets its own
// fresh 30s budget. A true wedge completes no windows and makes no
// per-leg progress, so it still fail-stops in <= 30s of NO progress —
// detection unchanged; only the queue-mistaken-for-wedge false positive
// is retired.
MonotonicTime requestStart = MonotonicTime::now();
const MonotonicTime originalRequestStart = requestStart;
const MonotonicTime bhRequestBegin = originalRequestStart; // BUGHUNT (NOT FOR LANDING): request-to-resume latency.
{
// HBT4 step 2: arbitration. Exactly one requesting THREAD is
// released as conductor; losers PARK here in bounded 1ms tryLock
// quanta (watchdog-covered, see above), access-released, then retry
// the whole sequence as later winners.
//
// V4-stw-watchdog (transition-vs-write, tier-forced): this tryLock
// poll has NO queue position — unlike Lock::lock()'s parked
// FIFO/handoff, a sleeping poller can lose every race to
// freshly-arriving requesters. Under a Class-A fire storm
// (tier-forced thresholds multiply transition fires; jettisoned
// code recompiles at warmup 10 and re-arms fresh watched sets) the
// job slot is held nearly continuously by a SUCCESSION of live,
// completing windows, so a starved loser's single end-to-end budget
// expires with the system fully live — the watchdog then fail-stops
// and its participant dump prints the CURRENT conductor's AB-21
// window access as "NON-QUIESCENT". Distinguish queue from wedge:
// a completed window is PROGRESS; re-arm the budget whenever the
// completion count advances. A true wedge completes no windows and
// still crashes at 30s of no progress, detection unchanged.
// Accepted trade: progress-based re-arm makes arbitration
// starvation formally unbounded (tryLock has no fairness); wedge
// detection, not fairness, is the watchdog's contract — the
// breadcrumb below keeps long queues observable.
uint64_t observedWindows = s_jsThreadsCompletedWindowCount.load(std::memory_order_relaxed);
unsigned loggedQueueBreadcrumbs = 0;
while (!s_jsThreadsJobSlotLock.tryLock()) {
uint64_t windows = s_jsThreadsCompletedWindowCount.load(std::memory_order_relaxed);
if (windows != observedWindows) {
observedWindows = windows;
requestStart = MonotonicTime::now(); // Progress: another conductor completed a window.
}
// Starvation observability: a re-armed loser can now queue far
// past one budget with zero output; log a breadcrumb at 60s and
// 120s of TOTAL queueing so a future fairness bug reads as a
// loud queue, not a silent harness timeout.
Seconds totalQueued = MonotonicTime::now() - originalRequestStart;
if ((loggedQueueBreadcrumbs == 0 && totalQueued > Seconds(60)) || (loggedQueueBreadcrumbs == 1 && totalQueued > Seconds(120))) {
++loggedQueueBreadcrumbs;
dataLogLn("JSThreads §A.3 arbitration: requester queued ", totalQueued.seconds(), "s total (budget re-armed on window completions; completed-window count now ", observedWindows, ") — live fire-storm queue, not a wedge.");
}
JSThreadsSafepoint::watchdogAssertStopProgress(requestStart, &vm);
// T6-arbitration-park-not-sleep: park keyed on the job-slot lock
// address instead of an unconditional 1ms sleep, so a queued
// requester wakes IMMEDIATELY when the previous conductor's
// window completes (the resume tail unparkOne()s this address
// right after the arbitration Locker releases). The 1ms timeout
// is kept as the watchdog backstop — same cadence for the
// progress-aware re-arm and the 60s/120s breadcrumb above — so
// a missed unpark (none expected; the validation predicate
// covers the bump-before-queue race) costs at most one quantum,
// identical to the prior sleep. Validation: park only while the
// completed-window token equals what we've already observed; a
// completion that races between tryLock() and queue insertion
// fails the predicate and we retry tryLock() without blocking.
// ParkingLot's internal queue lock orders the conductor's
// relaxed token bump before our predicate read (release/acquire
// on the bucket lock), so no lost wakeup. Flag-off: this whole
// function is §A.3 gilOff-only; line is unreachable with the
// GIL on.
ParkingLot::parkConditionally(
&s_jsThreadsJobSlotLock,
[observedWindows] { return s_jsThreadsCompletedWindowCount.load(std::memory_order_relaxed) == observedWindows; },
[] { },
MonotonicTime::now() + Seconds::fromMilliseconds(1));
}
Locker arbitration { AdoptLock, s_jsThreadsJobSlotLock };
// Per-leg budget: winning tenure IS progress. The GCL bracket and
// the §A.3.2 predicate wait each get a fresh 30s window (a wedge in
// any single leg still fail-stops in <= 30s of no progress; total
// request latency stays bounded by progress, not by one wall-clock
// budget that a legitimate fire-storm queue can exhaust).
requestStart = MonotonicTime::now();
// HBT4 step 3 (WINNER ONLY) — the LICENSED REORDER of the landed
// R1.i bracket: the GCL bracket comes strictly AFTER arbitration
// (the landed order "GCL then arbitrate" deadlocks: a loser blocked
// raw on GCL would violate HBT4.3 and could deadlock against a GC
// conductor queued behind the same lock). At most one thread — this
// winner — ever blocks in the GCL acquisition, and it blocks
// access-released and watchdog-covered; it queues behind any
// in-progress shared GC (§10C(b)/(e)).
JSC::Heap& server = vm.clientHeap.server();
Heap::JSThreadsStopScope stopScope(server, requestStart);
requestStart = MonotonicTime::now(); // Per-leg: predicate wait gets its own budget (GCL leg was covered by the value passed into stopScope).
// Fan (§A.2.3 / SB1 item 1): conductor tenure, then the seq_cst stop
// word, then the per-lite stop bits. Under the U-T2 interim seam the
// per-lite bits ALIAS the single VM-wide trap word (VMLite.cpp
// §A.2.1), so the fan is one requestStop(); the seq_cst stop WORD is
// the load-bearing half of the SB1 Dekker pair with re-acquirers.
s_jsThreadsConductorThread.store(&Thread::currentSingleton(), std::memory_order_seq_cst);
jsThreadsStopWordStore(&vm);
vm.requestStop(); // Poll-site delivery: running mutators trap to notifyVMStop.
WTF::storeLoadFence();
jsThreadsNotifyMutatorQuiesced(); // Wake ticket-parked threads to observe the word.
// §A.3.2 predicate wait: per-sample EXIT1.2 registry walks; the
// registry lock is dropped before every block/yield between samples,
// and the walk NEVER runs under a park-stripe lock (leaf discipline,
// see the stripe banner). This is the protocol's ONE remaining timed
// wait, by design (stripe banner): the conductor must run the V4
// watchdog periodically, and a mutator can release heap access on a
// path that never notifies (plain exit-to-native) — both need the
// poll. Quiesce notifications still wake it precisely via the
// generation re-check, so the timeout is latency-bounding only, not
// the wakeup mechanism. requestStart was re-sampled after the GCL
// bracket (per-leg budget, see the arbitration block above): this
// predicate wait has its own fresh 30s window, with NO progress
// re-arm — a conductor whose predicate cannot converge (bucket-iii
// lock-holding fire, unpolled access-holding native wait) still
// fail-stops in <= 30s.
auto& conductorStripe = jsThreadsCurrentThreadParkStripe();
for (;;) {
uint64_t generation = conductorStripe.generation.load(std::memory_order_seq_cst);
if (allEnteredThreadsAreQuiescent(vm))
break;
// §A.3.2 fan re-assertion (review round): under the §A.2.1 alias
// the per-lite stop bits ARE the single VM-wide trap word, and
// VMTraps' take rule clears NeedStopTheWorld when the FIRST
// trapping thread latches it — with no per-lite delivery record,
// a still-JS-spinning sibling would otherwise never trap after a
// sibling consumed the bit, and this predicate would hang until
// the watchdog fail-stops. Re-fire on every non-quiescent sample
// (idempotent seq_cst RMW, bounded by this loop). RETIRED when
// the per-lite trap words land (VMTraps.h activation checklist).
vm.requestStop();
WTF::storeLoadFence();
JSThreadsSafepoint::watchdogAssertStopProgress(requestStart, &vm); // Pass the target VM so a timeout names the non-quiescent lite(s).
Locker parkLocker { conductorStripe.lock };
if (conductorStripe.generation.load(std::memory_order_seq_cst) != generation)
continue; // A quiesce notification raced the predicate sample; re-sample now.
conductorStripe.condition.waitFor(conductorStripe.lock, Seconds::fromMilliseconds(1));
}
// AB-21 fix (GIL-removal review round): re-acquire the conductor's
// OWN client access for the window before running the fire bodies.
// Class-A fire bodies (WatchpointSet::fireAllSlow via
// drainClassAFireQueue, Debugger walk closures) take
// DeferGC/DeferGCForAWhile and run write barriers, whose per-client
// slots assert `client->hasHeapAccess() ||
// worldIsStoppedForAllClients()` (Heap::deferralDepthSlot, Heap.h) —
// the §A.3 thread-granular witness is invisible to them, so a
// no-access conductor aborted at the FIRST Class-A fire (the AB-21
// gilOff boot crash). Sound and non-blocking here: the AHA §A.3 and
// Mode-stop legs exempt the conductor (jsThreadsCurrentThreadIs-
// StopConductor — tenure was published above), GSP cannot be
// pending while the JSThreadsStopScope GCL bracket is held, and
// allEnteredThreadsAreQuiescent exempts the conductor's own lite
// (HBT2.1), so the satisfied predicate stays satisfied.
GCClient::Heap* windowClient = GCClient::Heap::currentThreadClient();
bool reacquiredForWindow = false;
if (windowClient && !windowClient->hasHeapAccess()) {
windowClient->acquireHeapAccess();
reacquiredForWindow = true;
}
// World stopped: run `work` on this stack. Default-conductor closure
// rules stand (R1.i): allocation-free, own client only. HBT2.2
// NO-GC-IN-WINDOW: bracket the window in heap I14's STW-forbidden
// counter so CSAC/SINFAC entries assert; GC initiation inside the
// window is FORBIDDEN — deferred-GC checks enqueue and re-run after
// resume.
s_jsThreadsWorldStoppedDepth.fetch_add(1, std::memory_order_relaxed);
++t_jsThreadsConductorDepth;
server.incrementSTWForbiddenScope();
work();
server.decrementSTWForbiddenScope();
--t_jsThreadsConductorDepth;
// AB-21 fix: drop the window-scoped access BEFORE resume publication
// so a sibling observing the cleared word never samples the
// conductor as an access-holding mutator mid-resume; the R1.i tail
// below re-acquires for the requester iff it held access on entry.
if (reacquiredForWindow)
windowClient->releaseHeapAccess();
// Resume (R1.i order): patcher-side data->ifetch publication first,
// then the ISB1.1 stop-generation bump INSIDE the window before
// resume, then drop the witness and clear the word (seq_cst — its
// synchronizes-with edge is what publishes the bump to gated
// re-acquirers), then wake every ticket.
WTF::crossModifyingCodeFence();
jsThreadsBumpStopGeneration();
s_jsThreadsWorldStoppedDepth.fetch_sub(1, std::memory_order_relaxed);
jsThreadsStopWordStore(nullptr);
s_jsThreadsConductorThread.store(nullptr, std::memory_order_seq_cst);
s_jsThreadsCompletedWindowCount.fetch_add(1, std::memory_order_relaxed); // V4 watchdog progress token (see the arbitration loop).
// BUGHUNT INSTRUMENTATION (stw-watchdog evidence pack; env-gated; NOT FOR LANDING):
// JSC_CLASSA_FIRE_STATS=1 also dumps the completed SectionA.3 window count and
// request-to-resume latency aggregates at exit.
if (getenv("JSC_CLASSA_FIRE_STATS")) [[unlikely]] {
static std::atomic<double> bhTotalMs { 0 };
static std::atomic<double> bhMaxMs { 0 };
double ms = (MonotonicTime::now() - bhRequestBegin).milliseconds();
double cur = bhTotalMs.load(std::memory_order_relaxed);
while (!bhTotalMs.compare_exchange_weak(cur, cur + ms)) { }
double curMax = bhMaxMs.load(std::memory_order_relaxed);
while (ms > curMax && !bhMaxMs.compare_exchange_weak(curMax, ms)) { }
static std::once_flag bhOnce;
std::call_once(bhOnce, [] {
std::atexit([] {
dataLogLn("BUGHUNT-A3-WINDOWS completed=", s_jsThreadsCompletedWindowCount.load(std::memory_order_relaxed),
" totalRequestToResumeMs=", bhTotalMs.load(std::memory_order_relaxed),
" maxMs=", bhMaxMs.load(std::memory_order_relaxed));
});
});
}
// Retire this window's stop bits (review round): leaving the
// NeedStopTheWorld trap + entry-scope-service bits set forever would
// (a) route EVERY later VMEntryScope entry/exit of every thread
// through the entry-scope service and its m_worldLock takes — a
// permanent serialization point — and (b) wedge the §A.3.2b(i)
// Mode-stop AHA gate on a stale bit. The bit may be co-owned by an
// in-flight Mode-machine stop, so the clear is CLEAR-THEN-RECHECK-
// THEN-RESTORE rather than a raw cancel: requestStopAllInternal
// publishes Mode::Stopping and (re)fires the trap bit under ONE
// m_worldLock hold, so the info() lock acquisition below observes a
// non-RunAll mode for any fire our cancel could have raced with, and
// the restore re-delivers it (requestStop is idempotent; a duplicate
// delivery is one benign notifyVMStop trip). RunOne deliberately
// keeps its bits (the restore re-arms them). A queued §A.3 loser
// re-fires its own fan as the next winner.
vm.cancelStop();
if (VMManager::info().worldMode != VMManager::Mode::RunAll)
vm.requestStop();
jsThreadsNotifyMutatorQuiesced();
} // ~JSThreadsStopScope: drop the GCL bracket (resume order), then...
// T6-arbitration-park-not-sleep: the arbitration Locker has now released
// s_jsThreadsJobSlotLock — wake ONE queued §A.3 requester parked on its
// address so its next tryLock() succeeds without paying the 1ms backstop.
// s_jsThreadsCompletedWindowCount was bumped inside the window (the
// parker's validation predicate), and ParkingLot's internal bucket lock
// orders that relaxed bump before the woken thread's predicate re-check,
// so there is no lost wakeup. unparkOne (not All) is the T5 fair-handoff
// pattern (see wakeOneSyncHoldParker): each completing conductor releases
// exactly one successor; the rest stay parked until THEIR predecessor
// completes. Flag-off: this function is gilOff-only.
ParkingLot::unparkOne(&s_jsThreadsJobSlotLock);
if (releasedAccess)
selfClient->acquireHeapAccess(); // ...re-acquire access LAST (R1.i).
}
void VMManager::incrementActiveVMs(VM& vm) WTF_REQUIRES_LOCK(m_worldLock)
{
RELEASE_ASSERT(m_worldMode != Mode::RunAll);
if (!vm.traps().m_hasBeenCountedAsActive) {
m_numberOfActiveVMs++;
vm.traps().m_hasBeenCountedAsActive = true;
}
}
void VMManager::decrementActiveVMs(VM& vm) WTF_REQUIRES_LOCK(m_worldLock)
{
// UNGIL §A.3.8 (U-T5): the gilOff VM is ONE counting unit, represented
// by its gilOffServicingThreads() entry (see notifyVMStop). A SIBLING
// thread of that VM deactivating mid-stop must neither decrement the
// VM's count nor trigger the RunOne resume-all arm (the :218
// m_targetVM==&vm tenure assert is VM-keyed and ambiguous with two
// same-VM observers) — the VM stays active while any thread is still
// entered. GIL-on: branch dead.
if (vm.gilOff() && m_worldMode != Mode::RunAll) [[unlikely]] {
bool currentThreadIsServicing = gilOffServicingThreads().get(&vm) == &Thread::currentSingleton();
if (!currentThreadIsServicing && vm.isEntered())
return;
}
// We only need to track m_numberOfActiveVMs changes if we're in RunOne
// mode. If we're running because the world was resumed with RunAll,
// then m_numberOfActiveVMs is invalid, and resumeTheWorld() would set
// it to a token value of invalidNumberOfActiveVMs (to aid debugging).
if (m_worldMode == Mode::RunAll) {
RELEASE_ASSERT(m_numberOfActiveVMs == invalidNumberOfActiveVMs);
RELEASE_ASSERT(!vm.traps().m_hasBeenCountedAsActive);
} else if (vm.traps().m_hasBeenCountedAsActive) {
m_numberOfActiveVMs--;
vm.traps().m_hasBeenCountedAsActive = false;
}
auto shouldResumeAll = [&] WTF_REQUIRES_LOCK(m_worldLock) {
if (m_worldMode != Mode::RunAll && !m_numberOfActiveVMs)
return true;
if (m_worldMode == Mode::RunOne) {
RELEASE_ASSERT(m_targetVM == &vm);
return true;
}
return false;
};
if (shouldResumeAll()) {
if (m_targetVM) {
// There's a designated targetVM thread to continue in, but we don't have the
// ability to just wake the desired one up. So, wake up all the threads and let
// them sort themselves out.
//
// But if the targetVM thread is this thread, then pass the control to another
// thread, any thread. That's because this thread is dying imminently.
if (m_targetVM == &vm) {
m_targetVM = nullptr;
m_useRunOneMode = false;
}
m_worldConditionVariable.notifyAll();
} else {
// There's no designated targetVM thread. So, just waking up any one thread will do.
m_worldConditionVariable.notifyOne();
}
}
}
CONCURRENT_SAFE void VMManager::requestStopAllInternal(StopReason reason)
{
// StopReason is synonymous with "StopRequest".
// From the client's perspective, it is the reason for a stop request.
// From the VMManager's perspective, it is the type of stop request.
auto requestBits = static_cast<StopRequestBits>(reason);
m_pendingStopRequestBits.exchangeOr(requestBits);
{
Locker lock { m_worldLock };
if (m_worldMode >= Mode::Stopping) {
// THREADS-INTEGRATE(heap) manifest 5g(ii): a GC stop
// requested while another stop is already in progress must
// still (1) trap entered VMs — a RunOne targetVM keeps
// executing through an in-progress debugger stop and would
// otherwise never reach a poll — and (2) wake parked VMs so
// their wait loops observe the new GC bit and run the 5g(i)
// park hook (release heap access). Without this, a GC
// requested during a non-GC stop hangs the §10.4 barrier.
if (reason == StopReason::GC) [[unlikely]] {
iterateVMs(scopedLambda<IteratorCallback>([&] (VM& vm) {
if (vm.isEntered()) {
vm.requestStop();
WTF::storeLoadFence();
}
return IterationStatus::Continue;
}));
m_worldConditionVariable.notifyAll();
}
return;
}
if (m_worldMode == Mode::RunAll) {
// RunOne mode allows execution of 1 VM without resumeTheWorld(). We did not clear
// the m_hasBeenCountedAsActive flags on each VM on resuming with RunOne. As a
// result, m_numberOfActiveVMs is still valid in RunOne mode. We don't want
// to reset m_numberOfActiveVMs to 0 here because we won't be re-calculating
// it on stop like we do for RunAll mode.
//
// For RunAll mode, do want to reset m_numberOfActiveVMs, and incrementActiveVMs()
// below will re-calculate the current true value of m_numberOfActiveVMs.
m_numberOfActiveVMs = 0;
}
// INVARIANT (load-bearing for the §A.3 conductor's resume recheck,
// jsThreadsThreadGranularStopTheWorldAndRun cancelStop/recheck/
// restore): the Mode::Stopping publication below and the per-VM
// requestStop() fan in the iterateVMs loop further down MUST both
// happen under THIS single m_worldLock hold. The conductor's
// post-cancel VMManager::info() read acquires m_worldLock, so any