diff --git a/vm/ByteCodeTranslator/src/cn1_globals.h b/vm/ByteCodeTranslator/src/cn1_globals.h index 79072bfc220..d2ba08f9cba 100644 --- a/vm/ByteCodeTranslator/src/cn1_globals.h +++ b/vm/ByteCodeTranslator/src/cn1_globals.h @@ -93,6 +93,68 @@ #endif #endif +// ========================================================================= +// UNCOOPERATIVE-MUTATOR ESCALATION (issue #5537) +// ========================================================================= +// The mark phase stops each lightweight thread by setting threadBlockedByGC and +// then spinning on threadActive until the thread parks itself. That handshake is +// purely COOPERATIVE, and ParparVM emits no safepoint polls in generated code -- +// not on method entry, not on loop back-edges (grep the translator for +// threadBlockedByGC: no hits). Every safepoint lives inside a runtime function: +// the codenameOneGcMalloc handshake, cn1BibopMaybeGc (reached only once per BiBOP +// PAGE, not per object), contended monitorEnter, Thread.sleep/Object.wait and the +// CN1_YIELD_THREAD native bracket. +// +// So a Java loop that allocates nothing new and enters no contended monitor never +// reaches a safepoint, and the collector's spin never ends. That is not a slow GC, +// it is a whole-VM freeze: every other thread parks at its next allocation waiting +// for a cycle that can never start. Issue #5537 caught it in the debugger with the +// collector 10 minutes 9 seconds into that spin (totalwait = 609,491,500us) while a +// game-tree search thread ran a compute-only evaluation loop. +// +// The escalation bounds the spin and then FREEZES the thread with the same SIGUSR2 +// stop the collector already uses for genuine native threads (cn1GcSignalStopOne), +// which needs no cooperation at all. Only available where that machinery is +// compiled: conservative roots on, and not Windows (no POSIX signals -- there the +// spin stays unbounded, exactly as today, because proceeding without stopping the +// thread would miss its roots and free live objects). +// +// The proper long-term answer is a safepoint poll on loop back-edges in the +// translator, which costs throughput in every loop the VM ever runs; this makes the +// pathological case survivable without paying that everywhere. +// +// -DCN1_GC_NO_FORCE_STOP restores the unbounded cooperative spin. It is the ablation arm +// GcUncooperativeThreadIntegrationTest builds to prove the gate can fail -- without a +// build that still wedges, an assertion that the VM does not wedge proves nothing. +// +// ALSO OFF UNDER -DCN1_DISABLE_SATB, which is the interesting one. A freeze is only +// releasable early -- before the mark drain -- because the SATB deletion barrier covers a +// mutator released mid-mark, exactly as it already covers native threads, which are never +// blocked at all. With the barrier compiled out that argument is gone, and the two ways to +// keep the escalation would both be worse than not having it: releasing early would let +// the resumed thread move a child out of a captured root into a local no snapshot contains +// and have the sweep take it, while holding the freeze through the drain puts markStatics +// (force-marking, which mallocs through the force-visited table) and gcMarkDrainParallel +// (lazy pthread_create) inside a window where the frozen thread may own the allocator or +// pthread lock -- a wedge in the middle of the fix for a wedge. The frozen window has to +// stay small and enumerable, and a full parallel drain is neither. So this ablation keeps +// master's unbounded cooperative wait, which is the behaviour it is there to measure +// against anyway. +#if defined(CN1_CONSERVATIVE_GC_ROOTS) && !defined(_WIN32) \ + && !defined(CN1_GC_NO_FORCE_STOP) && !defined(CN1_DISABLE_SATB) +#define CN1_GC_CAN_FORCE_STOP 1 +#endif + +// How long the cooperative safepoint handshake may spin before escalating, in +// microseconds. A thread that is going to park does so in microseconds, so this is +// pure headroom -- it only has to exceed the longest legitimate gap between a +// mutator's safepoints, which is one BiBOP page (64KB) of allocation or one +// unbracketed native call. Raising it lengthens the freeze in the pathological case +// and buys nothing in the normal one. +#ifndef CN1_GC_SAFEPOINT_WAIT_MAX_US +#define CN1_GC_SAFEPOINT_WAIT_MAX_US 250000 +#endif + //#define DEBUG_GC_ALLOCATIONS #define NUMBER_OF_SUPPORTED_THREADS 1024 @@ -1231,9 +1293,24 @@ struct ThreadLocalData { volatile sig_atomic_t gcSigRelease; // GC publishes highest released gen (monotonic) volatile sig_atomic_t gcSigStopGen; // generation counter (GC thread writes only) void* volatile gcSigStackPointer; // SP captured inside the signal handler - void* volatile gcSigStackBase; // [sp,base) high bound (filled by GC/handler) + // [sp,base) high bound and stack size, resolved BEFORE a forced freeze and reused + // while it is held. cn1GcStackBase must not be called under one: it is two plain + // accessors on Apple, but on Linux it is pthread_getattr_np, which mallocs (and + // reads /proc/self/maps for the initial thread), so calling it while the target is + // stopped can wait on an allocator lock the target owns. Written by the GC thread in + // cn1GcMarkForceStopUncooperative, read by cn1GcScanThreadNativeStack. + void* volatile gcSigStackBase; + size_t gcSigStackSize; char gcSigRegs[4096]; // raw copy of the interrupted ucontext (GPRs) volatile sig_atomic_t gcSigRegsLen; // valid bytes in gcSigRegs + // Set while the MARK LOOP owns a signal freeze it took because this thread would + // not reach a safepoint (see CN1_GC_CAN_FORCE_STOP). It tells + // cn1GcScanThreadNativeStack to reuse that freeze rather than take its own: a + // second SIGUSR2 aimed at a thread already spinning inside the handler stays + // pending until the handler returns, so the nested stop would spin out its whole + // timeout and then report failure on a thread that is in fact frozen. GC-thread + // owned, cleared by cn1GcMarkReleaseForced. + JAVA_BOOLEAN gcMarkForcedStop; #endif #ifdef CN1_GC_CONFORM // Monotonic ms at which this thread was registered. The duty denominator is the diff --git a/vm/ByteCodeTranslator/src/cn1_globals.m b/vm/ByteCodeTranslator/src/cn1_globals.m index bfcd4be756b..da921e8b653 100644 --- a/vm/ByteCodeTranslator/src/cn1_globals.m +++ b/vm/ByteCodeTranslator/src/cn1_globals.m @@ -1448,12 +1448,52 @@ void placeObjectInHeapCollection(JAVA_OBJECT obj) { static long gcAdoptTop = 0, gcAdoptCap = 0; #endif +// Nonzero while the collector is holding a thread SIGNAL-FROZEN (either the escalation in +// codenameOneGCMark or the native-thread stop inside cn1GcScanThreadNativeStack). A frozen +// thread halts at an arbitrary instruction and may own the libc allocator lock, so nothing +// the collector runs in that window may allocate -- and root marking DOES allocate, through +// cn1MatureObject's adoption buffer. Written only by the GC thread, and only while root +// scanning, which is serial; the parallel drain runs after every freeze is released. +static _Atomic int cn1GcFreezeHeld = 0; + +#ifndef CN1_DISABLE_BIBOP +// Grow the adoption buffer to `headroom` free slots, called BEFORE a freeze is taken so the +// growth inside cn1MatureObject is unlikely to be needed while one is held. Best effort: +// failing to grow only makes the decline in cn1MatureObject more likely, never unsafe. +static void cn1GcAdoptReserve(long headroom) { + pthread_mutex_lock(&gcAdoptMutex); + if(gcAdoptTop + headroom > gcAdoptCap) { + long ncap = gcAdoptCap ? gcAdoptCap : 4096; + while(ncap < gcAdoptTop + headroom) { ncap *= 2; } + JAVA_OBJECT* n = (JAVA_OBJECT*)realloc(gcAdoptStack, (size_t)ncap * sizeof(JAVA_OBJECT)); + if(n != 0) { gcAdoptStack = n; gcAdoptCap = ncap; } + } + pthread_mutex_unlock(&gcAdoptMutex); +} +#endif + static void cn1MatureObject(JAVA_OBJECT obj) { #ifndef CN1_DISABLE_BIBOP // Claim the object for adoption exactly ONCE with a CAS -3 -> -4. Under parallel // markers two threads can both reach the same object; the CAS loser must not // double-buffer/double-register. The -4 flag takes effect immediately so the cascade, // the mark-stamp and the sweep-skip all see it during THIS mark. + // DECLINE BEFORE THE CLAIM if a signal freeze is held and the buffer below would have + // to grow: that growth is a realloc, and the frozen thread may own the allocator lock, + // which would hang the collector on the thread it just stopped and never reach the + // release. Checked before the CAS on purpose -- claiming and then bailing is what the + // OOM path below does, and it leaves the object flagged -4 and unregistered forever, + // i.e. leaked, because the CAS can never fire again. Declining leaves it at -3: it is + // still marked and traced this cycle (the push below is unconditional), it simply + // graduates in a later one. + // + // The unlocked read of gcAdoptTop/gcAdoptCap is exact where it matters: the flag is + // only ever set while the collector is root-scanning, which is serial on the GC + // thread. Ordered flag-last so the common path pays nothing but a predictable branch. + if(gcAdoptTop >= gcAdoptCap + && atomic_load_explicit(&cn1GcFreezeHeld, memory_order_relaxed) != 0) { + return; + } int expected = CN1_BIBOP_HEAP_POS; if(!__atomic_compare_exchange_n(&obj->__heapPosition, &expected, CN1_BIBOP_ADOPTED, 0, __ATOMIC_RELAXED, __ATOMIC_RELAXED)) { @@ -1605,6 +1645,12 @@ static void cn1DrainDeadThreadPending() { static void cn1GcScanOwnStack(CODENAME_ONE_THREAD_STATE); static void cn1GcSignalStopThreads(struct ThreadLocalData* self); static void cn1GcSignalReleaseThreads(struct ThreadLocalData* self); +#ifdef CN1_GC_CAN_FORCE_STOP +// Escalation for a lightweight thread that will not reach a safepoint (issue #5537). +// See the definitions next to cn1GcSignalStopOne for the contract. +static JAVA_BOOLEAN cn1GcMarkForceStopUncooperative(struct ThreadLocalData* t); +static void cn1GcMarkReleaseForced(struct ThreadLocalData* t); +#endif void cn1GcBuildRootSnapshots(void); JAVA_OBJECT cn1ConservativeResolve(void* w); #ifdef CN1_CONSERVATIVE_GC_SELFCHECK @@ -2223,6 +2269,56 @@ void codenameOneGCMark() { } if(t != d) { struct elementStruct* objects = t->threadObjectStack; + // TRUE once the cooperative handshake below gave up and froze this + // thread with a signal instead. It changes what the rest of this + // iteration may touch -- see each use. + // + // WHAT A HELD FREEZE FORBIDS. The thread is stopped at an arbitrary + // instruction, so it may own ANY lock a mutator can hold -- the libc + // allocator's, stdio's, os_log's, criticalSection. Nothing between the + // freeze and cn1GcMarkReleaseForced may block on one of those, or the + // collector waits for a thread that cannot run until the collector + // releases it: the same permanent wedge this whole change exists to + // remove, just rarer and harder to place. Note this is not only about + // how long a mutator holds such a lock -- there is a window between the + // last threadActive read and the signal landing in which the target can + // enter any of that code, so "it would have to be unlucky" is not an + // argument. Every step below is therefore classified: + // + // pending migration ....... TAKES criticalSection -> skipped entirely + // aggressive-allocator hold allocates + logs -> skipped entirely + // root snapshot rebuild ... reallocs -> skipped (built pre-freeze) + // precise stack scan ...... resolve + mark bit -> SAFE, lock-free + // native stack scan ....... resolve + mark bit -> SAFE, lock-free + // stack-bounds lookup ..... mallocs on Linux -> done pre-freeze + // this escalation's log ... stdio / os_log -> DEFERRED past release + // + // The mark worklist mutex is fine to take: only collector threads ever + // hold it, so a frozen mutator can never be the owner. + // + // Note what is NOT on the list: markStatics and gcMarkDrainParallel, which + // run after the release. Both allocate -- markStatics force-marks, which + // reaches the force-visited table's malloc, and the first parallel drain + // creates its workers with pthread_create -- so the release must stay ahead + // of them. That is also why the escalation is compiled out when the SATB + // barrier is (see CN1_GC_CAN_FORCE_STOP): without the barrier the only way + // to keep it would be to hold the freeze across exactly those two. + // + // And a thread inside its own nursery minor collection is never frozen at + // all: cn1MarkForceStopUncooperative declines it, because the root scans + // mark through the TARGET's thread state and nurseryPromoting makes + // gcMarkObject return without marking anything. + // + // The stack-bounds entry is the one to learn from: cn1GcStackBase is two + // plain accessors on Apple and pthread_getattr_np on Linux, and only the + // Linux spelling allocates. Checking the Apple one and calling the + // function safe is how it got onto this list as SAFE the first time. A + // cross-platform helper has to be classified on its WORST platform. + JAVA_BOOLEAN forcedStop = JAVA_FALSE; + // Deferred report for the escalation (see above). Zero means nothing to + // report; the values are captured under the freeze and printed after it. + long long forcedStopWaitUs = 0; + long forcedStopSeq = 0; #ifdef CN1_CONSERVATIVE_GC_ROOTS // PHASE 3b: demand a FRESH native-stack capture this round. Only a @@ -2239,8 +2335,19 @@ void codenameOneGCMark() { // we don't have much control and who barely call into Java anyway if(t->lightweightThread) { t->threadBlockedByGC = JAVA_TRUE; - int totalwait = 0; + // 64-bit: at 500us a spin, an int overflowed after ~36 minutes of + // waiting, and signed overflow is undefined -- the one input that + // can reach it is precisely the wedge this loop is trying to report. + long long totalwait = 0; long now = time(0); + long lastReport = 0; +#ifdef CN1_GC_CAN_FORCE_STOP + // Next totalwait at which to try the escalation. A running total + // rather than a modulo: CN1_GC_SAFEPOINT_WAIT_MAX_US is overridable, + // and `totalwait % bound == 0` silently never fires for any bound that + // is not a multiple of the 500us step. + long long nextEscalation = (long long)CN1_GC_SAFEPOINT_WAIT_MAX_US; +#endif #ifdef CN1_GC_CONFORM // Opened and closed around the safepoint wait ALONE. Closing it later // -- after the migration and the stack scans -- would fold their cost @@ -2252,16 +2359,85 @@ void codenameOneGCMark() { while(t->threadActive) { usleep(500); totalwait += 500; - if((totalwait%10000)==0) - { long later = time(0)-now; - if(later>10000) - { + // REPORTING, fixed. time(0) is in SECONDS; this compared the + // elapsed value against 10000 and then printed it divided by + // 1000, so the warning first became eligible after 2.8 HOURS and + // would have understated what it found by a factor of 1000. The + // collector in issue #5537 sat here for 10 minutes 9 seconds + // (totalwait 609,491,500us, read out of the debugger) and never + // printed a line -- the one diagnostic aimed at this failure was + // silent for the whole of it. Report from 10 seconds on, once + // every 10, naming the thread while it is still stuck. Kept live + // even where the escalation below exists, because that escalation + // can itself fail and this is then the only thing that says so. + if((totalwait % 100000) == 0) { + long later = time(0) - now; + if(later >= 10 && later - lastReport >= 10) { + lastReport = later; #if defined(__OBJC__) - NSLog(@"GC trapped for %d seconds waiting for thread %d in slot %d (%d)", - (int)(later/1000),(int)t->threadId,iter,t->threadKilled); + NSLog(@"[GC] trapped for %ld seconds waiting for thread %d in slot %d (killed=%d)", + later, (int)t->threadId, iter, (int)t->threadKilled); +#else + fprintf(stderr, "[GC] trapped for %ld seconds waiting for thread %d in slot %d (killed=%d)\n", + later, (int)t->threadId, iter, (int)t->threadKilled); #endif } } +#ifdef CN1_GC_CAN_FORCE_STOP + // ESCALATION. Past the bound this thread is not going to park -- + // typically because it is running generated code that reaches no + // safepoint at all (see CN1_GC_CAN_FORCE_STOP in cn1_globals.h). + // Freeze it with the signal stop instead, which needs no + // cooperation. RETRIED on the same cadence rather than attempted + // once: cn1GcSignalStopOne can time out on a descheduled handler, + // and one transient failure must not sentence the collector to the + // unbounded wait for the rest of the cycle. Each failed attempt + // already costs about its own timeout, so the retry rate is + // self-limiting. gcPthreadValid is the one PERMANENT reason the + // stop cannot work, and retrying past it would rebuild the root + // snapshot every 250ms for the rest of a wait that is already + // never going to end. + if(totalwait >= nextEscalation && t->gcPthreadValid) { + nextEscalation += (long long)CN1_GC_SAFEPOINT_WAIT_MAX_US; + // The snapshot rebuild MUST happen here, BEFORE the freeze: it + // reallocs, and a thread frozen mid-malloc holds the libc + // allocator lock. Everything this iteration does while the + // freeze is held is malloc-free for the same reason (the mark + // worklist is a fixed-size array; the force-visited side table + // is only touched on the force path, which no root scan takes). + cn1GcBuildRootSnapshots(); + forcedStop = cn1GcMarkForceStopUncooperative(t); + if(forcedStop) { + // Reported on the 1st, 2nd, 4th, 8th ... escalation of the + // process. Self-limiting without a rate-limit clock, and + // the running count still tells you the order of magnitude + // -- one escalation is a thread that happened to sit in a + // long native, thousands is a program with a compute loop + // in it, and that difference is what a reader needs. Not + // behind an env var: a collector that had to shoot a + // mutator to make progress is abnormal, and the reason + // issue #5537 took so long to place is that this loop had + // nothing to say for itself. + // + // CAPTURED here, PRINTED after the release. NSLog and + // fprintf both take locks a frozen mutator can be holding + // (os_log's, stdio's, and malloc's underneath either), so + // logging here would deadlock the collector against the + // very thread it just stopped. Counting is two integer + // stores and is safe. + static long cn1GcForcedStops = 0; // GC thread only + cn1GcForcedStops++; + if((cn1GcForcedStops & (cn1GcForcedStops - 1)) == 0) { + forcedStopSeq = cn1GcForcedStops; + forcedStopWaitUs = totalwait; + } + // threadActive is still TRUE and stays that way -- the + // thread is frozen, not parked. Everything below reads + // forcedStop rather than threadActive for that reason. + break; + } + } +#endif } #ifdef CN1_GC_CONFORM cn1GcWaitNs += cn1GcNowNs() - __wt0; @@ -2282,29 +2458,56 @@ void codenameOneGCMark() { #ifdef CN1_GC_CONFORM long long __mg0 = cn1GcNowNs(); #endif - lockCriticalSection(); - if(allThreads[iter] == t) { - if (!t->lightweightThread) { - // For native threads, we need to actually lock them while we traverse the - // heap allocations because we can't use the usual locking mechanisms on - // them. - lockThreadHeapMutex(); - } - for(int heapTrav = 0 ; heapTrav < t->heapAllocationSize ; heapTrav++) { - JAVA_OBJECT obj = (JAVA_OBJECT)t->pendingHeapAllocations[heapTrav]; - if(obj) { - t->pendingHeapAllocations[heapTrav] = 0; - placeObjectInHeapCollection(obj); -#ifdef CN1_GC_CONFORM - cn1GcMigrated++; -#endif + // A force-stopped thread skips this ENTIRELY -- the lock included, not + // just the body. Two independent reasons, and the first one is fatal: + // + // 1. lockCriticalSection() would BLOCK. The frozen thread is stopped at an + // arbitrary instruction and may hold this very mutex (markDeadThread, + // monitorEnter's monitor-creation branch, placeObjectInHeapCollection + // all take it), and it cannot release it until this cycle releases the + // freeze -- which happens after this point. Guarding only the body, + // which is what this did first, still takes the lock and still + // deadlocks. There is also no "it is only held briefly" defence: the + // target can enter that code in the window between the last + // threadActive read and the signal landing. + // 2. Migrating would be wrong anyway. The append is `pending[size] = o; + // size++`, so a thread frozen between those two stores has an object in + // the table that the count does not cover yet: migrating [0,size), + // zeroing those slots and resetting size to 0 would leave that object + // referenced by nothing the collector ever walks, and the thread's own + // `size++` on resume would hand the next allocation a slot the + // migration had already emptied. + // + // The table is not a root source -- it only decides which objects the + // SWEEP may consider -- so leaving it intact for one cycle costs a + // deferred reclaim and nothing else. It cannot grow without bound either: + // a table over its threshold parks its own thread at a safepoint, which is + // the cooperative stop this escalation was standing in for. + if(!forcedStop) { + lockCriticalSection(); + if(allThreads[iter] == t) { + if (!t->lightweightThread) { + // For native threads, we need to actually lock them while we traverse the + // heap allocations because we can't use the usual locking mechanisms on + // them. + lockThreadHeapMutex(); + } + for(int heapTrav = 0 ; heapTrav < t->heapAllocationSize ; heapTrav++) { + JAVA_OBJECT obj = (JAVA_OBJECT)t->pendingHeapAllocations[heapTrav]; + if(obj) { + t->pendingHeapAllocations[heapTrav] = 0; + placeObjectInHeapCollection(obj); + #ifdef CN1_GC_CONFORM + cn1GcMigrated++; + #endif + } + } + if (!t->lightweightThread) { + unlockThreadHeapMutex(); } } - if (!t->lightweightThread) { - unlockThreadHeapMutex(); - } + unlockCriticalSection(); } - unlockCriticalSection(); #ifdef CN1_GC_CONFORM cn1GcMigrateNs += cn1GcNowNs() - __mg0; #endif @@ -2313,11 +2516,20 @@ void codenameOneGCMark() { JAVA_INT allocSize = t->heapAllocationSize; JAVA_BOOLEAN agressiveAllocator = JAVA_FALSE; + // Skipped under a held freeze, with the diagnostic below it. get_free_memory() + // and NSLog both take locks the stopped thread may own, and the EDT is a + // perfectly ordinary candidate for the escalation -- a long computation on + // the event thread is exactly the shape issue #5537 reported. The hold + // decision is also meaningless here: allocSize is read from a table this + // cycle deliberately did not migrate, so it describes the previous cycle's + // backlog rather than what this thread just produced. #ifndef CN1_NO_AGGRESSIVE_HOLD - if (isEdt(t->threadId) && !lowMemoryMode) { - agressiveAllocator = allocSize > CN1_AGRESSIVE_ALLOCATOR_THREAD_HEAP_ALLOCATIONS_THRESHOLD_EDT; - } else { - agressiveAllocator = allocSize > CN1_AGRESSIVE_ALLOCATOR_THREAD_HEAP_ALLOCATIONS_THRESHOLD; + if(!forcedStop) { + if (isEdt(t->threadId) && !lowMemoryMode) { + agressiveAllocator = allocSize > CN1_AGRESSIVE_ALLOCATOR_THREAD_HEAP_ALLOCATIONS_THRESHOLD_EDT; + } else { + agressiveAllocator = allocSize > CN1_AGRESSIVE_ALLOCATOR_THREAD_HEAP_ALLOCATIONS_THRESHOLD; + } } #endif if (CN1_EDT_THREAD_ID == t->threadId && agressiveAllocator) { @@ -2328,14 +2540,21 @@ void codenameOneGCMark() { } - t->heapAllocationSize = 0; + if(!forcedStop) { + t->heapAllocationSize = 0; + } int stackSize = t->threadObjectStackOffset; #ifdef CN1_CONSERVATIVE_GC_ROOTS // Refresh the page/extent snapshot for the VALIDATED precise scan // below (also rebuilt in cn1GcScanThreadNativeStack before any // signal-stop; building here first only makes it fresher). - cn1GcBuildRootSnapshots(); + // NOT while a forced stop is held: this reallocs, and the frozen thread + // may hold the allocator lock. The escalation built the snapshot before + // it froze the thread, so the one this would refresh already exists. + if(!forcedStop) { + cn1GcBuildRootSnapshots(); + } #endif #ifdef CN1_GC_VERIFY { extern const char* cn1GcMarkPhase; cn1GcMarkPhase = "precise-thread-stack"; } @@ -2403,6 +2622,44 @@ void codenameOneGCMark() { #ifdef CN1_GC_CONFORM cn1GcStackNs += cn1GcNowNs(); #endif +#ifdef CN1_GC_CAN_FORCE_STOP + // Released as soon as this thread's roots are captured, which is HERE and + // not at the threadBlockedByGC clear below. A cooperatively parked thread + // waits out the mark drain in a usleep; a signal-frozen one waits in an + // async-signal-safe BUSY spin, so holding it across the drain would burn a + // core for the length of a full mark -- and would drag markStatics and + // gcMarkDrainParallel, neither of which is allocation-free, inside a window + // where this thread may own the allocator lock. + // + // What the drain-before-unblock protects -- snapshot-at-the-beginning -- is + // supplied for a released mutator by the SATB deletion barrier, which is + // armed for the whole mark and is already the only thing keeping genuine + // native threads honest; they are never blocked at all, and + // cn1GcScanThreadNativeStack releases its own signal stops at exactly this + // point for the same reason. That dependency is enforced rather than + // assumed: CN1_GC_CAN_FORCE_STOP is not defined when the barrier is + // compiled out (see cn1_globals.h), so this release can never run without + // it. threadBlockedByGC stays set, so if this thread does reach a safepoint + // it still parks. + if(forcedStop) { + cn1GcMarkReleaseForced(t); + forcedStop = JAVA_FALSE; + } + // Only now, with the thread running again, is it safe to take a logging + // lock: while frozen the target could have owned os_log's, stdio's or + // malloc's, and printing would have hung the collector on the thread it had + // just stopped. Captured at the escalation, printed here. + if(forcedStopSeq != 0) { +#if defined(__OBJC__) + NSLog(@"[GC] force-stopped thread %d after %lldus at a safepoint it never reached (%ld so far)", + (int)t->threadId, forcedStopWaitUs, forcedStopSeq); +#else + fprintf(stderr, "[GC] force-stopped thread %d after %lldus at a safepoint it never reached (%ld so far)\n", + (int)t->threadId, forcedStopWaitUs, forcedStopSeq); +#endif + forcedStopSeq = 0; + } +#endif #ifdef CN1_CONSERVATIVE_GC_SELFCHECK cn1GcSelfCheckThreadStack(t, stackSize); #endif @@ -8219,10 +8476,113 @@ static void cn1GcSignalReleaseOne(struct ThreadLocalData* t) { #endif } +#ifdef CN1_GC_CAN_FORCE_STOP +// Freeze a lightweight thread that would not reach a safepoint within +// CN1_GC_SAFEPOINT_WAIT_MAX_US (issue #5537 -- see CN1_GC_CAN_FORCE_STOP in +// cn1_globals.h for why such a thread exists at all). Returns JAVA_TRUE once the thread +// is parked inside the SIGUSR2 handler, and the CALLER then owns that freeze until +// cn1GcMarkReleaseForced -- including the obligation to run nothing that allocates while +// it is held, because the thread can have been frozen mid-malloc. The root snapshots must +// already be built for the same reason. +static JAVA_BOOLEAN cn1GcMarkForceStopUncooperative(struct ThreadLocalData* t) { + // Stack bounds FIRST, while the target is still running. cn1GcStackBase is two plain + // accessors on Apple and pthread_getattr_np on Linux, and the Linux one mallocs (plus + // reads /proc/self/maps for the initial thread) -- so resolving it under the freeze + // can block on an allocator lock the frozen thread owns. Reasoning about the Apple + // spelling and letting the conclusion cover both is how this got classified safe the + // first time. The bounds do not change for a live pthread, so resolving here and + // reusing them for the whole freeze loses nothing. + if(!t->gcPthreadValid) { + return JAVA_FALSE; + } + size_t ssz = 0; + char* base = cn1GcStackBase(t->gcPthread, &ssz); + if(base == 0 || ssz == 0) { + // No bounds means the conservative scan could not read this thread's native stack + // even once it was stopped, so freezing it would buy nothing and skip its roots. + // Report failure and let the caller fall back to waiting. + return JAVA_FALSE; + } + t->gcSigStackBase = base; + t->gcSigStackSize = ssz; +#ifndef CN1_DISABLE_BIBOP + // Give the adoption buffer headroom while allocating is still legal, so the root scans + // under the freeze are unlikely to need cn1MatureObject's realloc (which they must + // decline). Sized well above one thread's plausible adoption count per cycle. + cn1GcAdoptReserve(16384); +#endif + if(cn1GcSignalStopOne(t) == 0) { + // Timed out. cn1GcSignalStopOne has already pre-released the generation, so + // nothing is left stranded. + return JAVA_FALSE; + } +#ifdef CN1_NURSERY + // DECLINE a thread caught inside its own minor collection. cn1NurseryWriteBarrier + // raises nurseryPromoting and deliberately leaves threadActive TRUE for the duration, + // so it is a prime candidate for this escalation -- and the root scans below call + // gcMarkObject(t, ...) with the TARGET's thread state, whose first act under that flag + // is to promote-or-return WITHOUT marking. Freezing here would therefore hand the + // sweep a thread whose roots were all silently skipped, and mature objects live only + // from this thread would be reclaimed under it. + // + // Checked AFTER the stop, not before: read while the thread is running, the flag can + // be raised in the window between the read and the signal landing. A frozen thread's + // flag cannot change, so this is exact. Released and reported as a failure so the + // caller's retry simply tries again after the next interval, by which time the minor + // collection is normally over -- and the cooperative wait is still in force meanwhile. + if(t->nurseryPromoting) { + cn1GcSignalReleaseOne(t); // before any bookkeeping, so there is none to unwind + return JAVA_FALSE; + } +#endif + atomic_fetch_add_explicit(&cn1GcFreezeHeld, 1, memory_order_relaxed); + t->gcMarkForcedStop = JAVA_TRUE; + return JAVA_TRUE; +} + +static void cn1GcMarkReleaseForced(struct ThreadLocalData* t) { + if(!t->gcMarkForcedStop) { + return; + } + t->gcMarkForcedStop = JAVA_FALSE; + cn1GcSignalReleaseOne(t); + atomic_fetch_sub_explicit(&cn1GcFreezeHeld, 1, memory_order_relaxed); +} +#endif + // Scan ONE thread's native C stack [sp, base) + its register snapshot, marking every // resolved live object. threadStateData = the GC thread; t = the thread being scanned. static void cn1GcScanThreadNativeStack(CODENAME_ONE_THREAD_STATE, struct ThreadLocalData* t) { if(!t->gcPthreadValid) return; + +#ifdef CN1_GC_CAN_FORCE_STOP + // FIRST, ahead of cn1GcStackBase: the mark loop may already be holding a signal + // freeze on this thread, and cn1GcStackBase is pthread_getattr_np on Linux, which + // mallocs -- calling it under the freeze can block on an allocator lock the frozen + // thread owns. cn1GcMarkForceStopUncooperative resolved the bounds before it froze + // the thread precisely so this path never has to. + if(t->gcMarkForcedStop) { + // Reuse the mark loop's capture: do NOT stop again -- a second SIGUSR2 to a + // thread spinning inside the handler stays pending until the handler returns, so + // the nested stop would spin out its full timeout and then report failure on a + // thread that is demonstrably stopped -- and do NOT release, because the precise + // object-stack scan the caller runs alongside this needs the same freeze. No + // cn1GcBuildRootSnapshots either: it reallocs, and the caller built the snapshot + // before freezing for the same reason. + char* fbase = (char*)t->gcSigStackBase; + size_t fssz = t->gcSigStackSize; + char* fsp = (char*)t->gcSigStackPointer; + if(fbase != 0 && fssz != 0 && fsp != 0 + && fsp >= fbase - (long)fssz && fsp < fbase) { + cn1ConservativeMarkRange(threadStateData, fsp, fbase); + } + if(t->gcSigRegsLen > 0) { + cn1ConservativeMarkRange(threadStateData, t->gcSigRegs, t->gcSigRegs + t->gcSigRegsLen); + } + return; + } +#endif + size_t ssz = 0; char* base = cn1GcStackBase(t->gcPthread, &ssz); if(base == 0 || ssz == 0) return; @@ -8248,6 +8608,13 @@ static void cn1GcScanThreadNativeStack(CODENAME_ONE_THREAD_STATE, struct ThreadL // SIGNAL path: stop, scan, release. Used for native threads, for the forced // CN1_GC_SIGNAL_STOP=1 validation mode, or as a fallback for a stale capture. + // Same allocation ban as the escalation, and this path had it first: master already + // marked between the stop and the release here, so cn1MatureObject's realloc could + // already hang the collector against a frozen NATIVE thread. Counted rather than a + // boolean because the two freeze sites are independent. +#ifndef CN1_DISABLE_BIBOP + cn1GcAdoptReserve(16384); +#endif char* sp = cn1GcSignalStopOne(t); if(sp == 0) { // Could not stop the thread. If it is lightweight and cooperatively captured we @@ -8261,8 +8628,10 @@ static void cn1GcScanThreadNativeStack(CODENAME_ONE_THREAD_STATE, struct ThreadL (char*)&t->gcRegisterSnapshot + sizeof(t->gcRegisterSnapshot)); } } - return; + return; // nothing was frozen, so the counter was never raised } + // Raised only on the success path, so the sub below always pairs with an add. + atomic_fetch_add_explicit(&cn1GcFreezeHeld, 1, memory_order_relaxed); if(sp >= base - (long)ssz && sp < base) { cn1ConservativeMarkRange(threadStateData, sp, base); } @@ -8270,6 +8639,7 @@ static void cn1GcScanThreadNativeStack(CODENAME_ONE_THREAD_STATE, struct ThreadL cn1ConservativeMarkRange(threadStateData, t->gcSigRegs, t->gcSigRegs + t->gcSigRegsLen); } cn1GcSignalReleaseOne(t); + atomic_fetch_sub_explicit(&cn1GcFreezeHeld, 1, memory_order_relaxed); } // Scan the GC thread's OWN native stack (a root could be live only in a GC-thread C diff --git a/vm/ByteCodeTranslator/src/nativeMethods.m b/vm/ByteCodeTranslator/src/nativeMethods.m index 35354f8196d..2d50728d0c8 100644 --- a/vm/ByteCodeTranslator/src/nativeMethods.m +++ b/vm/ByteCodeTranslator/src/nativeMethods.m @@ -1830,7 +1830,15 @@ JAVA_INT java_lang_Object_hashCode___R_int(CODENAME_ONE_THREAD_STATE, JAVA_OBJEC i->gcSigRelease = 0; i->gcSigStopGen = 0; i->gcSigStackPointer = 0; + i->gcSigStackBase = 0; + i->gcSigStackSize = 0; i->gcSigRegsLen = 0; + // ThreadLocalData is malloc'd, NOT zeroed (see the notes on nativeStackLimit and + // bibopBytesLocal above). Garbage-nonzero here would tell + // cn1GcScanThreadNativeStack that the mark loop already froze this thread, so it + // would scan a RUNNING thread's stack from a garbage SP and never signal-stop it + // -- missed roots, then a use-after-free on whatever the sweep took. + i->gcMarkForcedStop = JAVA_FALSE; cn1TlsSelf = i; #endif pthread_setspecific(threadIdKey, i); diff --git a/vm/CLAUDE.md b/vm/CLAUDE.md index 2cb98ab271b..59eb9d0396f 100644 --- a/vm/CLAUDE.md +++ b/vm/CLAUDE.md @@ -341,6 +341,93 @@ If this is ever revisited, the thing to build FIRST is a way to drive an allocat residual window on purpose -- the phases after the grace walk and before `gcSatbActive` is cleared -- because without that, no version of this change can be validated. +### There are no safepoint polls in generated code + +The mark phase stops each lightweight thread cooperatively: it raises `threadBlockedByGC` +and spins on `while(t->threadActive)` until the thread parks itself. **Nothing in the +translator emits a safepoint poll** -- not on method entry, not on loop back-edges; grep +`vm/ByteCodeTranslator/src/com/codename1/tools/translator` for `threadBlockedByGC` and +there are no hits. Every safepoint in this VM lives inside a *runtime function*: + +- `codenameOneGcMalloc`'s handshake (legacy allocations), +- `cn1BibopMaybeGc`, reached **once per 64KB PAGE**, not per object -- a thread bumping + inside a page it already holds passes no safepoint, +- contended `monitorEnter`, +- `Thread.sleep` / `Object.wait`, and the `CN1_YIELD_THREAD` native bracket. + +`monitorEnter`'s **first-creation** branch is the odd one out and is worth fixing on its +own: it `pthread_mutex_lock`s with `threadActive` still TRUE, where the contended branch +right below it parks first. A thread that publishes the monitor into the side table, drops +the critical section and is then beaten to the mutex by a second thread -- which parks and +holds it across the GC handshake -- blocks there while still counted active, which is a +three-way deadlock (collector waits for it, it waits for the mutex, the holder waits for the +collector). The escalation below rescues that on POSIX and does not on Windows. Left alone +here deliberately: it is a different bug on a hot path and wants its own change and gate. + +So a Java loop that allocates nothing new and enters no contended monitor reaches **no +safepoint at all**, and that spin never ends. It is not a slow GC, it is a whole-VM freeze: +every other thread parks at its next allocation waiting for a cycle that can never start. +Issue #5537's reporter caught it in the debugger with the collector `totalwait = +609491500` microseconds -- **10 minutes 9 seconds** -- into that spin while a game-tree +search ran a compute-only evaluation loop. + +`CN1_GC_SAFEPOINT_WAIT_MAX_US` (250ms) bounds the spin, and past it the collector freezes +the thread with the **same SIGUSR2 stop it already uses for genuine native threads** +(`cn1GcSignalStopOne`), which needs no cooperation. Measured by +`GcUncooperativeThreadIntegrationTest`, one 6s compute-only spin against a churning +allocator: **maxStall 6186ms of a 6187ms spin without it, 339ms of 5948ms with it**. The +ablation arm is `-DCN1_GC_NO_FORCE_STOP`, and the gate requires it to reproduce the wedge. + +Four things the escalation has to respect, all of them consequences of a thread being +frozen wherever it happened to be rather than at a point it chose: + +- **Nothing may allocate while the freeze is held.** The thread can be frozen mid-`malloc` + holding the libc allocator lock. `cn1GcBuildRootSnapshots` reallocs, so it runs BEFORE + the freeze and is skipped at both of its usual call sites for the rest of that thread's + iteration. The scans themselves are malloc-free (fixed-size mark worklist; the + force-visited side table is only touched on the `force` path, which no root scan takes). +- **The pending-allocation table is not migrated.** The append is `pending[size] = o; + size++`, so a thread frozen between the two stores has an object the count does not + cover; migrating and resetting `size` to 0 would orphan it and then hand its slot back + out. The table is not a root source -- it only decides what the SWEEP may consider -- so + skipping it for a cycle costs a deferred reclaim and nothing else. +- **The freeze is released as soon as the roots are captured**, not at the + `threadBlockedByGC` clear. A parked thread waits in `usleep`; a signal-frozen one waits + in an async-signal-safe BUSY spin, so holding it across the mark drain would burn a core. + SATB is what makes an early release safe, and it is already the only thing keeping + genuine native threads honest. +- **Do not signal a thread that is already frozen.** A second SIGUSR2 aimed at a thread + spinning inside the handler stays pending until the handler returns, so a nested stop + spins out its whole timeout and then reports failure on a thread that is demonstrably + stopped. `gcMarkForcedStop` tells `cn1GcScanThreadNativeStack` to reuse the capture. + +Two configurations deliberately do NOT get the escalation, and both are enforced in the +`CN1_GC_CAN_FORCE_STOP` guard rather than argued at the use site. **Windows**, which has no +POSIX signals -- proceeding without stopping the thread would miss its roots and free live +objects, which is worse than a hang. And **`-DCN1_DISABLE_SATB`**, because the early +release is what keeps the frozen window small, and only the barrier makes an early release +sound; holding the freeze through the drain instead drags `markStatics` (force-marking, +which mallocs through the force-visited table) and `gcMarkDrainParallel` (lazy +`pthread_create`) inside it, which is a wedge in the middle of the fix for a wedge. + +A thread inside its own **nursery minor collection** is never frozen either. +`cn1NurseryWriteBarrier` raises `nurseryPromoting` and leaves `threadActive` TRUE for the +duration, so it is a prime escalation candidate -- and the root scans mark through the +TARGET's thread state, where that flag makes `gcMarkObject` promote-or-return without +marking anything. Freezing one would hand the sweep a thread whose roots were all silently +skipped. The check runs AFTER the stop, because a flag read while the thread is still +running can be raised in the window before the signal lands. +The proper long-term answer is a back-edge poll in the translator; it costs throughput in +every loop the VM ever runs, and this makes the pathological case survivable without paying +that everywhere. + +**The diagnostic aimed at exactly this was dead for its whole life.** The spin's warning +computed `long later = time(0) - now` -- SECONDS -- then tested `later > 10000` and printed +`later / 1000` as "seconds". It first became eligible after 2.8 hours and would have +understated by 1000x, so the ten-minute freeze above printed nothing and had to be read out +of a debugger. `totalwait` was also an `int`, which is signed overflow at ~36 minutes of +waiting. If an instrument has never been seen firing, assume it does not. + ### Never call into Java from a parked thread `java_lang_System_gc__` enters `synchronized(LOCK)`, and `monitorEnter` is a GC safepoint. diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/GcUncooperativeThreadIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/GcUncooperativeThreadIntegrationTest.java new file mode 100644 index 00000000000..6ab368bb793 --- /dev/null +++ b/vm/tests/src/test/java/com/codename1/tools/translator/GcUncooperativeThreadIntegrationTest.java @@ -0,0 +1,415 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.tools.translator; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * The uncooperative-mutator gate for the collector (issue #5537). + * + *
The mark phase stops each lightweight thread by raising {@code threadBlockedByGC} and + * spinning on {@code threadActive}, and the translator emits no safepoint polls in + * generated code -- not on method entry, not on loop back-edges. Every safepoint lives + * inside a runtime function, so a Java loop that allocates nothing and enters no contended + * monitor reaches none of them and that spin never ends. The reporter's debugger caught it + * at {@code totalwait = 609491500} microseconds -- ten minutes with the whole VM stopped + * behind one compute loop, because every other thread parks at its next allocation waiting + * for a cycle that can never start.
+ * + *{@code cn1_globals.m} now bounds that spin at {@code CN1_GC_SAFEPOINT_WAIT_MAX_US} and + * then freezes the thread with the same SIGUSR2 stop the collector already uses for genuine + * native threads. This gate asserts the outcome and the mechanism, and then rebuilds the + * same translated project with the escalation compiled out ({@code -DCN1_GC_NO_FORCE_STOP}) + * and requires both to fail -- a gate that has never been watched failing proves + * nothing.
+ * + *Every threshold is a RATIO of the workload's own two measurements rather than a + * wall-clock constant, because the failure is "a mutator was stalled for as long as the + * spinner ran" and that duration is whatever the machine makes it. See + * {@code GcUncooperativeThreadApp} for how the two are produced.
+ */ +@Tag("benchmark") +class GcUncooperativeThreadIntegrationTest { + + /** + * Share of the spin a mutator may be stalled for when the escalation works. The stall + * it should actually see is one safepoint bound (CN1_GC_SAFEPOINT_WAIT_MAX_US, 250ms) + * plus a mark, repeated per cycle -- so this is loose on purpose. The two regimes are + * "a fraction of a second" and "the entire spin", and a threshold tight enough to + * flake would be measuring the machine rather than the bug. + * + *Measured on this arm: 0.06 alone, 0.13 with the whole benchmark suite running in + * parallel on the same host. The ablation arm measures 0.98-1.00 in both, so the two + * are separated by roughly an order of magnitude and the margin here is deliberate + * headroom for a loaded runner rather than a number tuned against one.
+ */ + private static final double MAX_STALL_SHARE_FIXED = 0.25; + + /** + * Share of the spin the ABLATION arm must exceed. A wedged VM stalls the allocator + * from whenever the first cycle starts until the spinner ends, so the only thing that + * keeps this below 1.0 is how long the workload takes to get going -- measured 0.98 + * and 1.00. Set at half that, because this half of the gate only has to establish + * that the wedge still reproduces, not to measure it. + */ + private static final double MIN_STALL_SHARE_FAULTED = 0.5; + + /** + * Floor on the spin itself. Below this the ratios above stop separating anything: a + * spin shorter than a collection cycle cannot express a stall that lasts one. The + * fixture calibrates for 6s, so this only catches a machine or a build on which the + * calibration collapsed. + */ + private static final long MIN_SPIN_MS = 2000; + + /** + * Ceiling on one translated run. The ablation arm's spinner is bounded by an iteration + * count, so even a fully wedged VM finishes -- but reading the child to EOF on this + * thread would still hang the fork if that ever stopped being true. + */ + private static final long VM_RUN_TIMEOUT_SECONDS = 600; + + /** What the collector prints when it has to shoot a mutator. */ + private static final String FORCE_STOP_MARKER = "force-stopped thread"; + + @Test + void aComputeOnlyThreadDoesNotStallEverybodyElse() throws Exception { + Parser.cleanup(); + ListParparVM's mark phase stops each lightweight thread cooperatively -- it raises + * {@code threadBlockedByGC} and then spins on {@code threadActive} until the thread parks + * itself -- and the translator emits no safepoint polls in generated code, neither on + * method entry nor on loop back-edges. Every safepoint lives inside a runtime function: + * the allocator handshakes, contended {@code monitorEnter}, {@code Thread.sleep}, and the + * native-call bracket. A Java loop that allocates nothing and enters no contended monitor + * therefore reaches no safepoint at all, and the collector's spin never ends -- taking + * every other thread with it, because they park at their next allocation waiting for a + * cycle that can never start. The reporter's debugger caught the collector ten minutes + * into that spin while a game-tree search ran a compute-only evaluation loop.
+ * + *The shape here is that reduced to its two essential threads:
+ * + *Both figures are printed, and the gate compares them against each other rather than + * against a wall-clock constant: {@code MAXSTALL} is a small fraction of {@code SPINMS} + * when the collector can force the spinner to stop, and essentially all of it when it + * cannot. Self-calibrating, so one pair of thresholds holds on a fast developer machine + * and a slow CI runner alike.
+ * + *The spin length is calibrated at runtime for the same reason -- a fixed iteration + * count is seconds on one machine and milliseconds on another, and a spin shorter than a + * collection cycle cannot express the failure. Calibration runs BEFORE the allocator + * starts, so it measures the machine rather than the contention.
+ * + *Note that the allocator cannot escape the wedge by finishing early: once the + * collector is stuck on the spinner, the allocator blocks at its next page acquisition and + * stays there until the spinner ends. Its chunk count therefore only has to be enough to + * still be running when the spin begins.
+ * + *Declares no natives and uses no Codename One API, so a stock JVM runs it unchanged + * and {@code RESULT} can be compared across the two. {@code RESULT} deliberately excludes + * everything derived from the calibration, which is wall-clock dependent and so differs + * between any two runs. On a stock JVM the spinner is stoppable by construction (HotSpot + * polls at loop back-edges), which is the behaviour this gate asks ParparVM to + * approximate.
+ */ +public class GcUncooperativeThreadApp { + + /** How long the uninterruptible spin should last. Comfortably longer than a collection + * cycle on any runner, so a wedge is unmistakable next to a legitimate GC pause. */ + private static final long TARGET_SPIN_MS = 6000; + + /** Iterations used to measure the machine before sizing the real spin. Large enough to + * take milliseconds rather than to be swallowed by clock granularity. */ + private static final long CALIBRATE_ITERS = 20000000L; + + /** Floor on the calibrated spin, in case the calibration lands inside one clock tick. */ + private static final long MIN_SPIN_ITERS = 200000000L; + + /** Objects per timed chunk on the allocator. A chunk has to cross several BiBOP page + * acquisitions -- the page acquire is where the allocator's own safepoint lives, so a + * chunk that fitted inside one page would never park and would time nothing. */ + private static final int CHUNK_OBJECTS = 20000; + + /** Timed chunks. The measurement is a MAXIMUM, so this only has to keep the allocator + * running into the spin; see the class comment on why it cannot finish during one. */ + private static final int CHUNKS = 1500; + + /** Retained set, so the collector has something real to trace and cycles cost time. */ + private static final int LIVE_SET = 4000; + + /** A small reference-carrying object: reaches the mark worklist, unlike a leaf. */ + private static final class Node { + Node next; + long a; + long b; + + Node(Node next, long a) { + this.next = next; + this.a = a; + this.b = a * 31; + } + } + + /** Iterations for the spin, published before the spinner starts. */ + private static long spinIterations; + + /** Written by the spinner, read only after join. */ + private static volatile long spinResult; + private static volatile long spinMillis; + + /** Held so the retained set cannot be optimized into nothing. */ + private static Node[] liveSet; + + /** + * The unstoppable loop. Pure long arithmetic on locals: no allocation (so no allocator + * handshake), no monitor, no native call, and on ParparVM no back-edge poll either. + * Deliberately NOT split into chunks with a check between them -- a chunk boundary that + * touches the runtime is a safepoint, and the point of this method is to have none. + */ + private static long burn(long iterations) { + long a = 1; + for (long i = 0; i < iterations; i++) { + a = a * 31 + i; + a ^= (a >>> 7); + } + return a; + } + + private static final class Spinner implements Runnable { + public void run() { + long t0 = System.currentTimeMillis(); + long r = burn(spinIterations); + spinMillis = System.currentTimeMillis() - t0; + spinResult = r; + } + } + + public static void main(String[] args) throws Exception { + // Retained set first: a collection that finds nothing to do finishes instantly and + // would not hold the allocator long enough for the comparison to mean anything. + liveSet = new Node[LIVE_SET]; + for (int i = 0; i < LIVE_SET; i++) { + liveSet[i] = new Node(i > 0 ? liveSet[i - 1] : null, i); + } + + // Size the spin against THIS machine, before any contention exists. + long calStart = System.currentTimeMillis(); + long calAcc = burn(CALIBRATE_ITERS); + long calMs = System.currentTimeMillis() - calStart; + long iters = calMs <= 0 ? MIN_SPIN_ITERS : (CALIBRATE_ITERS * TARGET_SPIN_MS) / calMs; + if (iters < MIN_SPIN_ITERS) { + iters = MIN_SPIN_ITERS; + } + spinIterations = iters; + + Thread spinner = new Thread(new Spinner()); + spinner.start(); + + // The allocator. Every chunk is timed; the maximum is the report. + long maxStallMs = 0; + long checksum = 0; + for (int chunk = 0; chunk < CHUNKS; chunk++) { + long t0 = System.currentTimeMillis(); + Node head = null; + for (int i = 0; i < CHUNK_OBJECTS; i++) { + head = new Node(head, chunk * 31L + i); + } + checksum += head.b; + // Keep the retained set churning so marking has real work every cycle. + liveSet[chunk % LIVE_SET] = new Node(null, chunk); + long dt = System.currentTimeMillis() - t0; + if (dt > maxStallMs) { + maxStallMs = dt; + } + } + + spinner.join(); + + System.out.println("SPINMS=" + spinMillis); + System.out.println("MAXSTALL=" + maxStallMs); + System.out.println("SPINACC=" + spinResult); + System.out.println("RESULT=" + (checksum ^ calAcc ^ liveSet[0].b)); + System.out.println("GC_UNCOOPERATIVE_DONE"); + } +}