Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .gemini/rules/async-test-instrumentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

### se.deversity.asynctest.agent.AsyncTestAgent
- **Sensitivity**: Critical
- **Note**: The INSTALLED gate must stay at-most-once per JVM: every entry point (premain, agentmain, selfAttach) races on the same compareAndSet, and a second transformer would double-weave accesses and double-count every one. premain installs without retransformation because classes are woven as they load; agentmain must keep RETRANSFORMATION + disableClassFormatChanges(), which is only safe while no weaver adds a member, and there are four besides the Advice, which is a method-entry prologue. FieldAccessWeaver inserts a stack-neutral, branch-free call before each field instruction, a load of the monitor plus a call at the entry of a synchronized method and before each of its returns (#822), a DUP of the monitor, an ALOAD of the local the owner was loaded from, an LDC of the field and a call before a MONITORENTER whose monitor the four instructions before it read from an instance field (ALOAD, GETFIELD, DUP, ASTORE, with no label between, #793), a DUP plus call after a VarHandle reference getAndSet it does not substitute (the ownership take, #555), DUP2 and SWAP plus a call before a JCTools offer, and a DUP before plus DUP_X1 and SWAP plus a call after a JCTools poll (#664), a copy of owner and value plus a call before a volatile field store and after a volatile field load, with a DUP of the receiver before an instance load (#742), and replaces an int VarHandle, AtomicIntegerFieldUpdater, AtomicBoolean or AtomicInteger store, swap or value-returning release, or a reference-slot store or getAndSet on an AtomicReference, its updater, its array or an instance-field VarHandle, or an acquiring reference read, an AtomicReference get or getAcquire, an updater get, an AtomicReferenceArray get or getAcquire or a VarHandle getVolatile or getAcquire (the acquire of the store it returned, #741), or an AtomicStampedReference get, getReference, set, compareAndSet, weakCompareAndSet or attemptStamp (#817), or an AtomicInteger get or an AtomicLong get, store or compare-and-set (a get then set, #1157), with a static hook consuming the same stack and returning the same result, plus one POP where a VarHandle call site declared a void result and one CHECKCAST to the declared type after a VarHandle reference getAndSet or acquiring read (the spinlock, #554, #558, #658, #667; the ownership offer and take, #664). CollectionAccessWeaver replaces each call its tables list with a static hook consuming the same stack, and inserts only the monitor loads and the loop back-edge call its own rules describe. ThreadConstructionWeaver inserts a DUP plus a call after the constructor call of a new Thread(...), and an ALOAD 0 plus a call after a Thread subclass constructor's superclass call (#737). ConstructionWeaver inserts a call that consumes and returns the order argument before a LinkedHashMap(int, float, boolean) constructor call and a DUP or an ALOAD 0 plus a call after it (#807), a DUP plus a call after a new CompletableFuture() constructor call (#1054), and an ALOAD 0, an LDC of the class name and a call before each RETURN of a constructor and after a this(...) delegation (#791); it leaves alone a class in a named module that cannot read the library. InterruptHandlerWeaver inserts an LDC of the locations of a method's InterruptedException handlers that neither rethrow nor restore the flag, and a call, before the first instruction of a method that declares one, and leaves alone a class in a named module that cannot read the library. PersistenceHandleWeaver inserts, before a call whose owner is a JPA EntityManager or Hibernate Session by name, a stack-neutral copy of the receiver (DUP; SWAP, DUP_X1; or DUP2_X1, POP2, DUP_X2 for zero, one or two one-slot arguments) and a call, and leaves calls with more or wider arguments alone (#992). RunBlockingWeaver inserts, before a static call of kotlinx.coroutines.BuildersKt.runBlockingK, runBlocking or their $default bridges by name, one call of a ()V hook that takes and leaves nothing, in every woven class outside kotlinx.coroutines (#1007). KotlinMutexWeaver replaces a kotlinx.coroutines.sync.Mutex lock, unlock or tryLock call, or its static $default bridge, matched by owner, name and descriptor, with a static hook consuming the same stack and returning the same result, in every woven class outside kotlinx.coroutines (#1007). So frames stay valid and only maxStack grows (COMPUTE_MAXS, never COMPUTE_FRAMES, which would load classes from inside the agent). In every mode, install and the transformer open java.util.concurrent.locks, with fields=true also java.util.concurrent.atomic, and with collections=true also java.util and java.io (a synchronized wrapper's mutex, #996; a piped writer's sink, #994), to one module per woven loader and to nothing else: the module of the TelemetryRegistry copy that loader resolves, never the woven loader or its ancestors as such (UpdaterAccess, #659, #668, #855); the locks opening lets the library read a ReentrantLock owner at analysis. In every mode, a woven class in a named module gets a read edge to the library copy its loader resolves (UpdaterAccess.letRead, #862): the JVM's own edge for a transformed class reaches only the agent loader's unnamed module. The library resolves an updater bound before the attach by reading the JDK implementation from that copy (JdkUpdaterShapeCanaryTest pins the shape it reads), and the opening must fail silently like the rest of install. Nothing may throw out of premain — an exception there aborts JVM startup, which is why install() catches Throwable and releases the gate rather than propagating. Both modes use AgentBuilder.InitializationStrategy.NoOp: no weaver needs a loaded-type initializer, and Byte Buddy's default self-injection gave every class woven at load a <clinit> that loads net.bytebuddy.dynamic.Nexus through the system class loader, whose lock virtual threads initializing classes at once contend on and pin (#984). The Premain-Class / Agent-Class manifest entries live in this module's jar, which is why attaching uses -javaagent:async-test-agent.jar.
- **Note**: The INSTALLED gate must stay at-most-once per JVM: every entry point (premain, agentmain, selfAttach) races on the same compareAndSet, and a second transformer would double-weave accesses and double-count every one. premain installs without retransformation because classes are woven as they load; agentmain must keep RETRANSFORMATION + disableClassFormatChanges(), which is only safe while no weaver adds a member, and there are four besides the Advice, which is a method-entry prologue. FieldAccessWeaver inserts a stack-neutral, branch-free call before each field instruction, a load of the monitor plus a call at the entry of a synchronized method and before each of its returns (#822), a DUP of the monitor, an ALOAD of the local the owner was loaded from, an LDC of the field and a call before a MONITORENTER whose monitor the four instructions before it read from an instance field (ALOAD, GETFIELD, DUP, ASTORE, with no label between, #793), a DUP plus call after a VarHandle reference getAndSet it does not substitute (the ownership take, #555), DUP2 and SWAP plus a call before a JCTools offer, and a DUP before plus DUP_X1 and SWAP plus a call after a JCTools poll (#664), a copy of owner and value plus a call before a volatile field store and after a volatile field load, with a DUP of the receiver before an instance load (#742), and replaces an int VarHandle, AtomicIntegerFieldUpdater, AtomicBoolean or AtomicInteger store, swap or value-returning release, or a reference-slot store or getAndSet on an AtomicReference, its updater, its array or an instance-field VarHandle, or an acquiring reference read, an AtomicReference get or getAcquire, an updater get, an AtomicReferenceArray get or getAcquire or a VarHandle getVolatile or getAcquire (the acquire of the store it returned, #741), or an AtomicStampedReference get, getReference, set, compareAndSet, weakCompareAndSet or attemptStamp (#817), or an AtomicInteger get or an AtomicLong get, store or compare-and-set (a get then set, #1157), with a static hook consuming the same stack and returning the same result, plus one POP where a VarHandle call site declared a void result and one CHECKCAST to the declared type after a VarHandle reference getAndSet or acquiring read (the spinlock, #554, #558, #658, #667; the ownership offer and take, #664). CollectionAccessWeaver replaces each call its tables list with a static hook consuming the same stack, and inserts only the monitor loads and the loop back-edge call its own rules describe. ThreadConstructionWeaver inserts a DUP plus a call after the constructor call of a new Thread(...), and an ALOAD 0 plus a call after a Thread subclass constructor's superclass call (#737). ConstructionWeaver inserts a call that consumes and returns the order argument before a LinkedHashMap(int, float, boolean) constructor call and a DUP or an ALOAD 0 plus a call after it (#807), a DUP plus a call after a new CompletableFuture() constructor call (#1054), and an ALOAD 0, an LDC of the class name and a call before each RETURN of a constructor and after a this(...) delegation (#791); it leaves alone a class in a named module that cannot read the library. InterruptHandlerWeaver inserts an LDC of the locations of a method's InterruptedException handlers that neither rethrow nor restore the flag, and a call, before the first instruction of a method that declares one, and leaves alone a class in a named module that cannot read the library. PersistenceHandleWeaver inserts, before a call whose owner is a JPA EntityManager or Hibernate Session by name, a stack-neutral copy of the receiver (DUP; SWAP, DUP_X1; or DUP2_X1, POP2, DUP_X2 for zero, one or two one-slot arguments) and a call, and leaves calls with more or wider arguments alone (#992). RunBlockingWeaver inserts, before a static call of kotlinx.coroutines.BuildersKt.runBlockingK, runBlocking or their $default bridges by name, one call of a ()V hook that takes and leaves nothing, in every woven class outside kotlinx.coroutines (#1007). KotlinMutexWeaver replaces a kotlinx.coroutines.sync.Mutex lock, unlock or tryLock call, or its static $default bridge, matched by owner, name and descriptor, with a static hook consuming the same stack and returning the same result, in every woven class outside kotlinx.coroutines (#1007); before a tryLock hook it first loads the enclosing coroutine's continuation, an ALOAD 0 in an instance invokeSuspend(Object), an ALOAD of a trailing Continuation parameter no non-reference store has overwritten, or an ACONST_NULL, which the hook takes as its last argument (#1220). So frames stay valid and only maxStack grows (COMPUTE_MAXS, never COMPUTE_FRAMES, which would load classes from inside the agent). In every mode, install and the transformer open java.util.concurrent.locks, with fields=true also java.util.concurrent.atomic, and with collections=true also java.util and java.io (a synchronized wrapper's mutex, #996; a piped writer's sink, #994), to one module per woven loader and to nothing else: the module of the TelemetryRegistry copy that loader resolves, never the woven loader or its ancestors as such (UpdaterAccess, #659, #668, #855); the locks opening lets the library read a ReentrantLock owner at analysis. In every mode, a woven class in a named module gets a read edge to the library copy its loader resolves (UpdaterAccess.letRead, #862): the JVM's own edge for a transformed class reaches only the agent loader's unnamed module. The library resolves an updater bound before the attach by reading the JDK implementation from that copy (JdkUpdaterShapeCanaryTest pins the shape it reads), and the opening must fail silently like the rest of install. Nothing may throw out of premain — an exception there aborts JVM startup, which is why install() catches Throwable and releases the gate rather than propagating. Both modes use AgentBuilder.InitializationStrategy.NoOp: no weaver needs a loaded-type initializer, and Byte Buddy's default self-injection gave every class woven at load a <clinit> that loads net.bytebuddy.dynamic.Nexus through the system class loader, whose lock virtual threads initializing classes at once contend on and pin (#984). The Premain-Class / Agent-Class manifest entries live in this module's jar, which is why attaching uses -javaagent:async-test-agent.jar.

## Contract-Frozen Signature
- **Constraint**: You may change internal logic, but MUST NOT modify the method name, parameters, return type, or checked exceptions.
Expand Down
Loading
Loading