Class StaticPinningScanner

java.lang.Object
se.deversity.asynctest.analysis.StaticPinningScanner

public final class StaticPinningScanner extends Object
Compile-time / class-load-time static bytecode scanner that identifies virtual-thread pinning sites without running any tests.

Problem

VirtualThreadPinningDetector (in the async-test-lib artifact — deliberately not a @link, since this module depends on nothing else in the project) finds pinning at runtime by monitoring thread states during stress tests. If the pinning code path is rarely exercised, it goes undetected. The runtime detector also requires virtual threads to be actively scheduled and pinned during the observation window.

Approach

Using the ASM bytecode library this scanner walks compiled .class files and flags any method that:
  1. Enters a monitor via MONITORENTER
  2. While inside that monitor, calls a blocking JDK method (see BLOCKING_METHODS)
Such methods pin a carrier platform thread when called from a virtual thread, defeating the scalability benefit of Loom.

Limitations

  • Tracks monitor nesting depth only within a single method body; cross-method synchronization (e.g. a synchronized method calling a blocking helper) requires inter-procedural analysis and is not detected.
  • Exception-handler edges (MONITOREXIT in finally blocks) may undercount nesting depth; false negatives are possible but false positives are not.
Since:
1.6.0
  • Method Details

    • scanClass

      public static List<StaticPinningScanner.PinningSite> scanClass(byte[] classBytes)
      Scans a single .class file and returns all detected pinning sites.
      Parameters:
      classBytes - raw bytecode of the compiled class
      Returns:
      unmodifiable list of pinning sites (empty if none found)
    • scanDirectory

      public static List<StaticPinningScanner.PinningSite> scanDirectory(Path root) throws IOException
      Scans all .class files reachable from root (recursively) and returns every detected pinning site.

      Useful as a post-compile step or as part of a JUnit @BeforeAll sanity check:

      
       @BeforeAll
       static void noLoomPinningSites() throws IOException {
           Path classes = Path.of("target/classes");
           List<PinningSite> sites = StaticPinningScanner.scanDirectory(classes);
           assertTrue(sites.isEmpty(), "Pinning sites detected:\n" + sites);
       }
       
      Parameters:
      root - directory containing compiled .class files
      Returns:
      unmodifiable list of all pinning sites found under root
      Throws:
      IOException - if any .class file cannot be read
    • scanClass

      public static List<StaticPinningScanner.PinningSite> scanClass(String binaryName, ClassLoader classLoader) throws IOException
      Convenience overload: loads the class bytes from a ClassLoader by converting the binary name to a resource path and delegates to scanClass(byte[]).
      Parameters:
      binaryName - e.g. "com.example.MyService"
      classLoader - class loader to load the resource from
      Throws:
      IOException - if the resource cannot be read
      IllegalArgumentException - if the class resource is not found