Skip to content

Phase 3: INVOKEDYNAMIC handler isolation#802

Draft
jbachorik wants to merge 8 commits intodevelopfrom
jb/miminal_bootstrap
Draft

Phase 3: INVOKEDYNAMIC handler isolation#802
jbachorik wants to merge 8 commits intodevelopfrom
jb/miminal_bootstrap

Conversation

@jbachorik
Copy link
Collaborator

@jbachorik jbachorik commented Feb 17, 2026

Summary

Replace INVOKESTATIC handler copying with INVOKEDYNAMIC dispatch. Probe handler methods now stay in the probe class (bootstrap CL) and are called via ConstantCallSite, eliminating bytecode copying into target classes.

Architecture change

Old (INVOKESTATIC + CopyingVisitor) New (INVOKEDYNAMIC + ConstantCallSite)
Isolation Probe bytecode embedded in target class Handlers stay in probe class
Dispatch Direct INVOKESTATIC to copied handler INVOKEDYNAMIC → cached ConstantCallSite
Unload Incomplete (stale embedded code) Complete (cache cleared on unregister)
Bootstrap Indy.class (Java 15-specific, reflection) IndyDispatcher + HandlerRepository interface
Golden files 3× per test (static/dynamic/leveled) 1× per test (unified)
Overhead Direct call ~1.2 ns (ConstantCallSite inlines after JIT)

Dispatch chain

Instrumented target method
  → INVOKEDYNAMIC (first call triggers bootstrap)
    → IndyDispatcher.bootstrap()
      → HandlerRepositoryImpl.resolveHandler()
        → MethodHandles.publicLookup().findStatic(probeClass, handlerName, type)
          → ConstantCallSite (cached — subsequent calls go direct)

Integration test fixes

  1. AnyType descriptor transformation — Probe methods using @AnyType get Lorg/openjdk/btrace/core/types/AnyType;Ljava/lang/Object; in descriptors so INVOKEDYNAMIC call site types match. Applied in both BTraceProbeNode.getBytecode() and BTraceProbePersisted.register().

  2. StackWalker auxiliary frame skippinggetCallerClassLoader() and getCallerClass() in BTraceRuntimeImpl_9 and _11 now skip org.openjdk.btrace.runtime.auxiliary.* frames so probe handler frames are transparent to classloader resolution.

  3. HandlerRepositoryImpl cleanup — Removed fragile asType() fallback; handler resolution is now a clean findStatic lookup with warn-on-failure.

Review fixes

  • ConcurrentHashMap.computeIfAbsent null NPEConcurrentHashMap disallows null values; replaced with get() + put() pattern so failed resolutions aren't cached and can retry.
  • Symmetric probe lifecycle — Added HandlerRepositoryImpl.unregisterProbe() to both BTraceProbeNode.unregister() and BTraceProbePersisted.unregister(). Removed premature registerProbe from BTraceProbeFactory.createProbe() — registration now happens only in probe.register() after definedClass is set.
  • COMPUTE_FRAMES → 0 in BTraceProbePersisted.transformAnyTypeDescriptors() — only descriptors change (not control flow), so existing frames are preserved as-is. COMPUTE_FRAMES was unnecessary and risked ClassNotFoundException during hierarchy resolution.
  • Redundant unregisterProbe removed from Client.onExit() (now handled by probe lifecycle).

Benchmark results

DispatchBenchmark uses the real BTraceTransformer pipeline to instrument a target class with a compiled BTrace probe, then measures dispatch overhead.

Benchmark                                  Mode  Cnt   Score   Error  Units
DispatchBenchmark.baseline_noArgs          avgt   10   0.527 ± 0.016  ns/op
DispatchBenchmark.baseline_withReturn      avgt   10   0.527 ± 0.015  ns/op
DispatchBenchmark.instrumented_noArgs      avgt   10   1.529 ± 0.033  ns/op
DispatchBenchmark.instrumented_withReturn  avgt   10  20.223 ± 0.309  ns/op
  • Entry handler: ~1.0 ns overhead — pure INVOKEDYNAMIC dispatch cost
  • Return handler with @Duration: ~19.7 ns overhead — dominated by two System.nanoTime() calls (~11 ns each on macOS), not dispatch

Files

New: IndyDispatcher, HandlerRepository, DispatchBenchmark, DispatchTarget, Workload, DispatchScript
Deleted: CopyingVisitor, Indy, ~400 redundant golden files (static/dynamic/leveled → unified)
Refactored: HandlerRepositoryImpl, Instrumentor, Assembler, BTraceProbeNode, BTraceProbePersisted, BTraceRuntimeImpl_9/_11

Test plan

  • ./gradlew :btrace-instr:test — all instrumentor tests pass
  • ./gradlew :integration-tests:test -Pintegration — all 22 integration tests pass (including Docker)
  • ./gradlew :benchmarks:runtime-benchmarks:jmh -PjmhInclude='DispatchBenchmark' — benchmarks run and produce stable results
  • Manual: verify attach/detach cycle cleans up handler cache entries

🤖 Generated with Claude Code

jbachorik and others added 2 commits February 16, 2026 23:19
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace INVOKESTATIC handler copying with INVOKEDYNAMIC dispatch
  via ConstantCallSite for probe handler isolation
- Transform AnyType→Object in probe method descriptors
- Skip auxiliary frames in StackWalker classloader resolution
- Add real instrumentation JMH benchmarks (DispatchBenchmark)
- Fix ConcurrentHashMap.computeIfAbsent null NPE in handler cache
- Add symmetric unregisterProbe on probe unregister
- Remove premature registerProbe from BTraceProbeFactory
- Unify static/dynamic/leveled golden files into single set
- Delete CopyingVisitor and Indy (no longer needed)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jbachorik jbachorik added the AI AI-generated code label Feb 17, 2026
@jbachorik jbachorik changed the title INVOKEDYNAMIC handler isolation with dispatch benchmarks Phase 3: INVOKEDYNAMIC handler isolation Feb 17, 2026
jbachorik and others added 5 commits February 17, 2026 22:57
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On JDK 8, MethodHandleNatives references package-private types
(MemberName, LambdaForm) that Class.forName() cannot resolve from the
agent classloader. The default ClassWriter.getCommonSuperClass() throws,
causing addGuard() to fail silently — leaving linkCallSite() unpatched
and the LinkingFlag guard uninstalled. Override getCommonSuperClass() to
fall back to java/lang/Object on resolution failure, matching the
pattern used by BTraceClassWriter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…entation

Two fixes for the StackOverflowError in testTraceAll on JDK 8:

1. Guard probe handler execution against re-entrance: openLinkerCheck now
   calls LinkingFlag.guardLinking() before the INVOKEDYNAMIC probe call
   and reset() after it. This prevents infinite recursion when a probe
   handler (e.g. doall -> AtomicLong.getAndIncrement) calls instrumented
   code that would fire the same probe again.

2. Exclude org/openjdk/btrace/ classes from instrumentation unconditionally
   at the top of transform(), before acquiring the read lock. Agent classes
   loaded by MaskedClassLoader were bypassing the loader-based sensitive
   check (which only covers bootstrap and system classloaders).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jbachorik jbachorik force-pushed the jb/miminal_bootstrap branch from 7558470 to 37d8726 Compare March 15, 2026 20:40
- LinkingFlag: replace ThreadLocal<Integer> with ThreadLocal<int[]> to
  eliminate autoboxing on the hot path (3 ThreadLocal ops per probe).
  Use anonymous class instead of lambda to avoid INVOKEDYNAMIC in the
  bootstrap classloader.

- IndyDispatcher: add null-check for volatile repository field before
  resolveHandler/resolveRuntime calls (race during agent init).

- HandlerRepositoryImpl: replace manual get/put with computeIfAbsent
  to avoid redundant findStatic lookups under concurrent resolution.

- LinkerInstrumentor: log at debug level when getCommonSuperClass
  falls back to Object, instead of silently swallowing the exception.

- BTraceTransformer: clarify comment on intentional duplicate of the
  org/openjdk/btrace/ sensitive prefix check.

- BTraceRuntimeImpl_9/11: replace forEach+AtomicInteger/AtomicReference
  with StackWalker.walk()+stream to short-circuit after finding the
  target frame and eliminate per-call wrapper allocations.

Note: the try-finally around probe dispatch (counter leak on exception)
is deferred — emitting ATHROW in the exception handler triggers recursive
instrumentation in ThrowInstrumentor, requiring deeper integration with
the visitor chain.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI AI-generated code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant