Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1210c8a
Answer the collector's demand signal instead of idling through it (is…
shai-almog Aug 26, 2026
b6ed99f
Stop the collector paying for work that finds nothing (issue #5537)
shai-almog Aug 26, 2026
18a2def
Fix a deadlock the latency work exposed, and close the bulk-copy barr…
shai-almog Aug 27, 2026
35b6d38
Address the two review findings, and stop asserting the runner (issue…
shai-almog Aug 28, 2026
5321f73
Retry a failed allocation by looping, not by recursing (issue #5537)
shai-almog Aug 28, 2026
80d819f
Bind the new native for JavaScript, and stop gating "a collection is …
shai-almog Aug 28, 2026
dcd5e55
Fix three more review findings, one of which was in the measurement i…
shai-almog Aug 28, 2026
176d741
Never time out the GC handshake (issue #5537)
shai-almog Aug 28, 2026
7890725
Invalidate the JavaAPI cache on a deleted source too (issue #5537)
shai-almog Aug 28, 2026
0acda2f
Back off between failed allocations, and make that path testable (iss…
shai-almog Aug 28, 2026
1c99355
Stop the simulated-failure budget from refilling itself (issue #5537)
shai-almog Aug 28, 2026
9da3fef
Take the SATB mutex once per chunk on bulk copies, not once per refer…
shai-almog Aug 28, 2026
e5c870f
Correct the duty-cycle figures the collector was inflating
shai-almog Aug 28, 2026
76126e2
Stop the duty figure inflating itself, and shut the bulk SATB path do…
shai-almog Aug 28, 2026
a4fa589
Register the whole bulk barrier, not each range inside it
shai-almog Aug 28, 2026
d14a4b9
Never trace a new SATB discovery with the barrier already down
shai-almog Aug 28, 2026
efa53ab
Record the trial clear and what satbReopens is for (issue #5537)
shai-almog Aug 28, 2026
1fdd221
Count the same threads in the duty numerator that the denominator counts
shai-almog Aug 28, 2026
926dc96
Make an empty catch the only way out of SATB termination
shai-almog Aug 28, 2026
06d7afc
Bound the lost-request window, and divide duty by real thread-time
shai-almog Aug 28, 2026
0b474db
Make the bulk barrier's fast-path check safe across the trial clear
shai-almog Aug 28, 2026
2249bed
Handshake bulk copies with mark STARTUP, and make thread-time exact
shai-almog Aug 28, 2026
becc8db
Hold the bulk registration across the copy, not just the logging
shai-almog Aug 28, 2026
483f93b
Correct the cost claim for the spanning bulk registration
shai-almog Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion vm/ByteCodeTranslator/src/cn1_globals.h
Original file line number Diff line number Diff line change
Expand Up @@ -1030,6 +1030,11 @@ static inline JAVA_BOOLEAN cn1InNursery(void* p) {
// complete snapshot + incremental barrier. Off-mark: one predicted-not-taken flag load.
extern volatile int gcSatbActive;
extern void cn1SatbEnqueue(JAVA_OBJECT old);
extern volatile int gcSatbTerminating;
extern JAVA_BOOLEAN cn1SatbBulkBegin(void);
extern void cn1SatbEnqueueRangeLocked(JAVA_ARRAY_OBJECT* refs, int count);
extern void cn1SatbBulkEnd(void);
extern void cn1SatbBulkQuiesce(void);
#if defined(CN1_DISABLE_SATB)
#define CN1_WRITE_BARRIER(target, value) do { } while(0)
#else
Expand Down Expand Up @@ -1230,6 +1235,14 @@ struct ThreadLocalData {
char gcSigRegs[4096]; // raw copy of the interrupted ucontext (GPRs)
volatile sig_atomic_t gcSigRegsLen; // valid bytes in gcSigRegs
#endif
#ifdef CN1_GC_CONFORM
// Monotonic ms at which this thread was registered. The duty denominator is the
// integral of the live MUTATOR count over time, and sampling that population at slice
// boundaries misses any thread that both starts and exits inside one slice -- its
// stalls stay in the numerator while its lifetime is never counted. Stamping here and
// banking the lifetime in markDeadThread makes the integral exact instead of sampled.
long long gcThreadStartMs;
#endif
};

//#define BLOCK_FOR_GC() while(threadStateData->threadBlockedByGC) { usleep(500); }
Expand Down Expand Up @@ -1786,7 +1799,7 @@ static inline JAVA_OBJECT cn1BibopFastAllocNoZero(CODENAME_ONE_THREAD_STATE, int
* signal-stop this just makes the cheaper cooperative path usable; a no-op when conservative
* roots are off. */
#define CN1_YIELD_THREAD do { struct ThreadLocalData* __cn1yts = getThreadLocalData(); CN1_GC_PARK_CAPTURE(__cn1yts); __cn1yts->threadActive = JAVA_FALSE; } while(0)
#define CN1_RESUME_THREAD do { struct ThreadLocalData* __cn1rts = getThreadLocalData(); while (__cn1rts->threadBlockedByGC){ usleep((JAVA_INT)1000);} __cn1rts->threadActive = JAVA_TRUE; __cn1rts->gcParkCaptured = JAVA_FALSE; } while(0)
#define CN1_RESUME_THREAD do { struct ThreadLocalData* __cn1rts = getThreadLocalData(); CN1_STALL_T0(__cn1rt0); while (__cn1rts->threadBlockedByGC){ usleep((JAVA_INT)1000);} __cn1rts->threadActive = JAVA_TRUE; __cn1rts->gcParkCaptured = JAVA_FALSE; CN1_STALL_ADD(__cn1rt0, CN1_STALL_NATIVE_RESUME, __cn1rts); } while(0)

extern struct ThreadLocalData* getThreadLocalData();

Expand Down Expand Up @@ -2661,6 +2674,20 @@ extern __thread struct ThreadLocalData* cn1TlsSelf;
#define CN1_GC_PARK_CAPTURE(ts) do {} while(0)
#endif

// Bracket one mutator park so its duration is charged to a cause. Both halves compile to
// nothing without -DCN1_GC_CONFORM -- including the timestamp variable, which is why the
// name is a macro argument rather than a fixed identifier: several park sites sit in one
// scope in codenameOneGcMalloc and a fixed name would not survive there.
#ifdef CN1_GC_CONFORM
extern long long cn1StallNowNs(void);
extern void cn1StallRecord(int cause, long long ns, struct ThreadLocalData* ts);
#define CN1_STALL_T0(v) long long v = cn1StallNowNs()
#define CN1_STALL_ADD(v, cause, ts) cn1StallRecord((cause), cn1StallNowNs() - (v), (ts))
#else
#define CN1_STALL_T0(v) ((void)0)
#define CN1_STALL_ADD(v, cause, ts) ((void)0)
#endif

typedef JAVA_OBJECT (*newInstanceFunctionPointer)(CODENAME_ONE_THREAD_STATE);
typedef JAVA_OBJECT (*enumValueOfFunctionPointer)(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT);

Expand Down
1,869 changes: 1,784 additions & 85 deletions vm/ByteCodeTranslator/src/cn1_globals.m

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ enum NativeCategory {
"cn1_java_lang_Integer_cn1Value_R_int",
"cn1_java_lang_Integer_valueOf_int_R_java_lang_Integer",
"cn1_java_lang_System_isHighFrequencyGC_R_boolean",
"cn1_java_lang_System_gcIdleWaitMillis_R_int",
"cn1_java_lang_Thread_currentThread_R_java_lang_Thread",
"cn1_java_lang_Thread_getNativeThreadId_R_long",
"cn1_java_lang_Thread_interrupt0",
Expand Down
6 changes: 6 additions & 0 deletions vm/ByteCodeTranslator/src/javascript/parparvm_runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -5381,6 +5381,12 @@ bindNative(["cn1_java_lang_System_arraycopy_java_lang_Object_int_java_lang_Objec
bindNative(["cn1_java_lang_System_gcLight", "cn1_java_lang_System_gcLight__"], function() { return null; });
bindNative(["cn1_java_lang_System_gcMarkSweep", "cn1_java_lang_System_gcMarkSweep__"], function() { return null; });
bindNative(["cn1_java_lang_System_isHighFrequencyGC_R_boolean", "cn1_java_lang_System_isHighFrequencyGC___R_boolean"], function() { return 0; });
// The GC thread's idle decision. gcMarkSweep above is a no-op here and isHighFrequencyGC
// answers 0, so the collector loop has nothing to do; 30000 is the wait the Java code chose
// for exactly that case before the decision moved into a native, which keeps this port's
// GC thread as idle as it has always been rather than spinning it every 200ms over a
// collector that does nothing.
bindNative(["cn1_java_lang_System_gcIdleWaitMillis_R_int", "cn1_java_lang_System_gcIdleWaitMillis___R_int"], function() { return 30000; });
// Tagged-immediate Integer natives (C-side poor-man's-Valhalla). The JS port
// has no tagged pointers: cn1Value reads the heap field, valueOf delegates to
// the pure-Java cache twin (valueOfHeap).
Expand Down
73 changes: 64 additions & 9 deletions vm/ByteCodeTranslator/src/nativeMethods.m
Original file line number Diff line number Diff line change
Expand Up @@ -967,14 +967,54 @@ JAVA_VOID java_lang_System_arraycopy___java_lang_Object_int_java_lang_Object_int
}
struct clazz* cls = (*srcArr).__codenameOneParentClsReference;
int byteSize = byteSizeForArray(cls);
// SATB deletion barrier: an object arraycopy overwrites dst[dstOffset..+length)
// with a bulk memmove that bypasses the per-element setter, so preserve those
// overwritten references for the current mark cycle. No-op (one flag load) off-GC.
if(__builtin_expect(gcSatbActive, 0) && !cls->primitiveType) {
JAVA_ARRAY_OBJECT* dstData = (JAVA_ARRAY_OBJECT*)(*dstArr).data;
for(int i = 0 ; i < length ; i++) {
JAVA_OBJECT o = dstData[dstOffset + i];
if(o != JAVA_NULL && !CN1_IS_TAGGED(o)) cn1SatbEnqueue(o);
// SATB barrier, BOTH halves: an object arraycopy replaces dst[dstOffset..+length)
// with a bulk memmove that bypasses the per-element setter, so neither half fires on
// its own. No-op (one flag load) off-GC.
//
// The DELETION half preserves the references being overwritten, for the usual
// snapshot reason.
//
// The INSERTION half preserves the references being written IN, and it was missing.
// That half exists (see CN1_WRITE_BARRIER in cn1_globals.h) specifically to keep an
// object alive when "the container it is stored into is a fresh grace object not yet
// reachable" -- so copying into a freshly allocated Object[] during a mark, and then
// dropping the source, left the copied-in objects unmarked. The BiBOP grace pass
// covers most of that window by walking fresh slots, but not a destination allocated
// after the walk has passed its page, and not the belt/fixpoint phases that run after
// it. The result is a live object swept with a surviving reference to it -- the
// container->content class of crash the insertion half was added for.
//
// Both reads happen BEFORE the memmove, which is also what makes this correct for the
// overlapping src/dst that arraycopy is contractually required to support.
// ONE registration around BOTH halves. Bracketing each range separately would let the
// in-flight count fall to zero between them, and the collector can clear gcSatbActive,
// see zero and finish its final drain in that gap -- after which the insertion half
// logs nothing while the memmove below publishes those references regardless. See
// cn1SatbBulkBegin.
// NO FLAG PRECHECK. Sampling gcSatbActive out here is unsafe at BOTH ends of a mark:
// termination clears and re-raises it during the trial-clear protocol, and startup arms
// it without waiting for a copy that already looked. Either way the copy skips the
// barrier and then publishes into a live mark. Registering first and reading the flags
// while registered is the whole point of the protocol, and it is what lets
// codenameOneGCMark and mark termination both wait for an in-flight copy.
//
// The cost lands only on OBJECT arrays: cls->primitiveType is a load and a branch, and
// it rejects the byte[]/char[] copies that dominate arraycopy traffic before any atomic
// is executed.
// The bracket spans the memmove below, not just the logging: the registration is what
// mark startup and mark termination wait on, so releasing it before the copy would let
// a scan interleave with the publication. See cn1SatbBulkBegin.
JAVA_BOOLEAN cn1__satbReg = JAVA_FALSE;
if(!cls->primitiveType) {
cn1__satbReg = JAVA_TRUE;
if(cn1SatbBulkBegin()) {
// One acquisition of the SATB mutex per 256 references rather than per
// reference; this used to be two locked enqueues per element. See
// cn1SatbEnqueueRangeLocked.
cn1SatbEnqueueRangeLocked(((JAVA_ARRAY_OBJECT*)(*dstArr).data) + dstOffset, length);
#ifndef CN1_NO_BULK_INSERTION_BARRIER
cn1SatbEnqueueRangeLocked(((JAVA_ARRAY_OBJECT*)(*srcArr).data) + srcOffset, length);
#endif
}
}
/* java.lang.System.arraycopy is contractually overlap-safe (the spec defines
Expand All @@ -985,6 +1025,9 @@ JAVA_VOID java_lang_System_arraycopy___java_lang_Object_int_java_lang_Object_int
* heap corruption on the arm64 clean target). memmove is the correct,
* overlap-safe primitive. */
memmove( (*dstArr).data + (dstOffset * byteSize), (*srcArr).data + (srcOffset * byteSize), length * byteSize);
if(cn1__satbReg) {
cn1SatbBulkEnd();
}
}

JAVA_LONG java_lang_System_currentTimeMillis___R_long(CODENAME_ONE_THREAD_STATE) {
Expand Down Expand Up @@ -1704,6 +1747,11 @@ JAVA_INT java_lang_Object_hashCode___R_int(CODENAME_ONE_THREAD_STATE, JAVA_OBJEC
i->threadBlockedByGC = JAVA_FALSE;
i->threadActive = JAVA_FALSE;
i->threadKilled = JAVA_FALSE;
#ifdef CN1_GC_CONFORM
// Malloc'd, so this starts as garbage. See gcThreadStartMs in cn1_globals.h.
{ extern void cn1StallRegisterThread(struct ThreadLocalData* t);
cn1StallRegisterThread(i); }
#endif
i->interrupted = JAVA_FALSE;

i->currentThreadObject = 0;
Expand Down Expand Up @@ -1735,7 +1783,6 @@ JAVA_INT java_lang_Object_hashCode___R_int(CODENAME_ONE_THREAD_STATE, JAVA_OBJEC
// ThreadLocalData is malloc'd (not zeroed); 0 means "frameless native-stack
// limit not yet computed" -- it is filled in lazily on first frameless entry.
i->nativeStackLimit = 0;

i->pendingHeapAllocations = malloc(PER_THREAD_ALLOCATION_COUNT * sizeof(void *));
memset(i->pendingHeapAllocations, 0, PER_THREAD_ALLOCATION_COUNT * sizeof(void *));
i->heapAllocationSize = 0;
Expand Down Expand Up @@ -2358,6 +2405,14 @@ void markDeadThread(struct ThreadLocalData *d)
d->threadActive = JAVA_FALSE;
found = iter;
nThreadsToKill++;
#ifdef CN1_GC_CONFORM
// Bank this thread's lifetime before its TLD leaves allThreads, so the duty
// denominator keeps it. Mirrors cn1StallMutatorNs on the numerator side: both
// have to survive the thread that earned them, and both count only lightweight
// non-collector threads so the two describe one population.
{ extern void cn1StallRetireThread(struct ThreadLocalData* t);
cn1StallRetireThread(d); }
#endif
collectThreadResources(d);
break;
}
Expand Down
Loading
Loading