diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d997cfc..b1b8ef5 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5be30bb..a9db115 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,9 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 0262dcb..249efbb 100755 --- a/gradlew +++ b/gradlew @@ -20,7 +20,7 @@ ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -29,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/b631911858264c0b6e4d6603d677ff5218766cee/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. diff --git a/gradlew.bat b/gradlew.bat index c4bdd3a..a51ec4f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -19,12 +19,12 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -51,7 +51,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,29 +65,18 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/src/main/java/org/serialthreads/transformer/code/CompactingStackCode.java b/src/main/java/org/serialthreads/transformer/code/CompactingStackCode.java index 5fdd8d6..838d1b5 100644 --- a/src/main/java/org/serialthreads/transformer/code/CompactingStackCode.java +++ b/src/main/java/org/serialthreads/transformer/code/CompactingStackCode.java @@ -98,6 +98,49 @@ private void saveLocals( } } + @Override + public InsnList defaultInitLocals(MethodInsnNode methodCall, MetaInfo metaInfo) { + var instructions = new InsnList(); + if (!metaInfo.tags.contains(TAG_TAIL_CALL)) { + var frameAfter = metaInfo.frameAfter; + var isMethodNotStatic = isNotStatic(methodCall); + for (var code : ValueCodeFactory.CODES) { + for (int local = isMethodNotStatic ? 1 : 0, end = frameAfter.getLocals() - 1; local <= end; local++) { + var value = frameAfter.getLocal(local); + if (code.isResponsibleFor(value.getType())) { + var extendedValue = (ExtendedValue) value; + var lowestLocal = frameAfter.getLowestNeededLocal(extendedValue); + if (local == lowestLocal) { + // Pre-initialize with type default to satisfy verifier when GETFIELD can throw. + var localCode = code(extendedValue); + instructions.add(localCode.pushNull()); + instructions.add(localCode.store(local)); + } + } + } + } + } + return instructions; + } + + @Override + public InsnList restoreLocalsFromFrame(MethodInsnNode methodCall, MetaInfo metaInfo, int localFrame) { + var instructions = new InsnList(); + if (!metaInfo.tags.contains(TAG_TAIL_CALL)) { + restoreLocals(methodCall, metaInfo.frameAfter, localFrame, instructions); + } + return instructions; + } + + @Override + public InsnList restoreStackFromFrame(MethodInsnNode methodCall, MetaInfo metaInfo, int localFrame) { + var instructions = new InsnList(); + if (!metaInfo.tags.contains(TAG_TAIL_CALL)) { + restoreStack(methodCall, metaInfo.frameAfter, localFrame, instructions); + } + return instructions; + } + @Override public InsnList restoreFrame(MethodInsnNode methodCall, MetaInfo metaInfo, int localFrame) { var instructions = new InsnList(); diff --git a/src/main/java/org/serialthreads/transformer/code/ThreadCode.java b/src/main/java/org/serialthreads/transformer/code/ThreadCode.java index 8799877..6444cb4 100644 --- a/src/main/java/org/serialthreads/transformer/code/ThreadCode.java +++ b/src/main/java/org/serialthreads/transformer/code/ThreadCode.java @@ -227,6 +227,48 @@ public interface ThreadCode { */ InsnList pushMethod(int localFrame); + /** + * Initialize local variables to their type-default values (0 for primitives, null for references). + * Must be called before {@link #restoreLocalsFromFrame} to ensure locals are defined at exception + * handlers even if a GETFIELD in restoreLocalsFromFrame throws before all stores complete. + * + * @param methodCall + * method call to process. + * @param metaInfo + * Meta information about method call. + * @return Generated code. + */ + InsnList defaultInitLocals(MethodInsnNode methodCall, MetaInfo metaInfo); + + /** + * Restore local variables from the current frame before resuming the method call. + * Must be called before the copy method call so that locals are initialized even + * if the copy method throws an exception caught by an enclosing try-catch block. + * + * @param methodCall + * method call to process. + * @param metaInfo + * Meta information about method call. + * @param localFrame + * number of local containing the frame. + * @return Generated code. + */ + InsnList restoreLocalsFromFrame(MethodInsnNode methodCall, MetaInfo metaInfo, int localFrame); + + /** + * Restore operand stack values from the current frame after resuming the method call. + * Must be called after the copy method returns normally. + * + * @param methodCall + * method call to process. + * @param metaInfo + * Meta information about method call. + * @param localFrame + * number of local containing the frame. + * @return Generated code. + */ + InsnList restoreStackFromFrame(MethodInsnNode methodCall, MetaInfo metaInfo, int localFrame); + /** * Restore current frame before resuming the method call. * diff --git a/src/main/java/org/serialthreads/transformer/strategies/AbstractTransformer.java b/src/main/java/org/serialthreads/transformer/strategies/AbstractTransformer.java index 9e9fa84..79bcc7b 100644 --- a/src/main/java/org/serialthreads/transformer/strategies/AbstractTransformer.java +++ b/src/main/java/org/serialthreads/transformer/strategies/AbstractTransformer.java @@ -198,6 +198,15 @@ private void check(ClassNode clazz, MethodNode method) { */ private void reanalyzeMethods(ClassNode clazz, List methods) throws NotTransformableException { for (var method : methods) { + // Skip reanalysis for methods with exception handlers. + // After LocalVariablesShifter.shift() is called, the frame information at exception + // handler entry points becomes stale, causing the analyzer to fail. + // The frames will be properly recomputed when the class is written with COMPUTE_FRAMES. + if (method.tryCatchBlocks != null && !method.tryCatchBlocks.isEmpty()) { + logger.debug("Skipping reanalysis of {} due to exception handlers", methodName(clazz, method)); + continue; + } + try { ExtendedAnalyzer.analyze(clazz, method, classInfoCache); } catch (Exception e) { diff --git a/src/main/java/org/serialthreads/transformer/strategies/frequent3/CopyMethodTransformer.java b/src/main/java/org/serialthreads/transformer/strategies/frequent3/CopyMethodTransformer.java index 142dce6..7ae7931 100644 --- a/src/main/java/org/serialthreads/transformer/strategies/frequent3/CopyMethodTransformer.java +++ b/src/main/java/org/serialthreads/transformer/strategies/frequent3/CopyMethodTransformer.java @@ -60,6 +60,7 @@ public MethodNode transform() throws AnalyzerException { replaceReturns(); var restores = insertCaptureAndRestoreCode(true); + // updateExceptionTableForCaptureCode(restores); // TODO: May not be needed createRestoreHandlerCopy(restores); addThreadAndFrame(); fixMaxs(); diff --git a/src/main/java/org/serialthreads/transformer/strategies/frequent3/MethodTransformer.java b/src/main/java/org/serialthreads/transformer/strategies/frequent3/MethodTransformer.java index 549e41f..aca74c0 100644 --- a/src/main/java/org/serialthreads/transformer/strategies/frequent3/MethodTransformer.java +++ b/src/main/java/org/serialthreads/transformer/strategies/frequent3/MethodTransformer.java @@ -15,6 +15,8 @@ import org.serialthreads.transformer.strategies.AbstractMethodTransformer; import org.serialthreads.transformer.strategies.MetaInfo; +import java.util.List; + import static org.objectweb.asm.Opcodes.ALOAD; import static org.objectweb.asm.Opcodes.ICONST_0; import static org.objectweb.asm.Opcodes.ICONST_1; @@ -254,13 +256,27 @@ protected LabelNode createCaptureAndRestoreCodeForMethod(MethodInsnNode methodCa if (restore) { instructions.add(restoreLabel); + // If this method call is inside a try-catch block, restore locals BEFORE calling the copy + // method. This ensures that if the copy method throws an exception caught by an enclosing + // try-catch, the locals have their saved values rather than type defaults. + // Note: RunMethodTransformer also inserts a pre-init redirect (defaultInitLocals + GOTO) + // outside the try-catch range so that the ASM verifier sees initialized locals at the + // restore label's entry (see redirectRestoreLabelsOutsideTryCatch). + var insideTryCatch = isInsideTryCatch(methodCall); + if (insideTryCatch) { + instructions.add(threadCode.restoreLocalsFromFrame(methodCall, metaInfo, localFrame)); + } // Call interrupted method. instructions.add(callCopyMethod(methodCall, metaInfo)); // If serializing, return early, the frame already has been captured. instructions.add(new JumpInsnNode(IFNE, serializing)); - // Restore stack "under" the returned value, if any. - instructions.add(threadCode.restoreFrame(methodCall, metaInfo, localFrame)); + // Restore frame (locals + stack, or just stack if locals were already restored above). + if (insideTryCatch) { + instructions.add(threadCode.restoreStackFromFrame(methodCall, metaInfo, localFrame)); + } else { + instructions.add(threadCode.restoreFrame(methodCall, metaInfo, localFrame)); + } // Continue. } @@ -422,4 +438,127 @@ protected void fixMaxs() { // TODO 2009-10-11 mh: recalculate minimum maxs method.maxStack = Math.max(method.maxStack + 2, 5); } + + /** + * Check if an instruction is inside any try-catch block range of the current method. + * + * @param instruction Instruction to check. + * @return True if the instruction is inside a try-catch block. + */ + private boolean isInsideTryCatch(AbstractInsnNode instruction) { + if (method.tryCatchBlocks == null || method.tryCatchBlocks.isEmpty()) { + return false; + } + return method.tryCatchBlocks.stream() + .anyMatch(tcb -> isInstructionInRange(instruction, tcb.start, tcb.end)); + } + + /** + * Update exception table to cover inserted capture and restore code. + * When an interruptible method call is inside a try-catch block, the capture code + * inserted after the call must also be covered by the same exception handler. + * + * @param restores Labels pointing to the generated restore codes for method calls. + */ + protected void updateExceptionTableForCaptureCode(List restores) { + if (method.tryCatchBlocks == null || method.tryCatchBlocks.isEmpty()) { + return; + } + + logger.debug(" Updating exception table for capture code"); + + // Build a map from method calls to their restore labels for quick lookup + var callToRestore = new java.util.HashMap(); + int index = 0; + for (var methodCall : interruptibleMethodCalls) { + if (index < restores.size() && restores.get(index) != null) { + callToRestore.put(methodCall, restores.get(index)); + } + index++; + } + + // For each exception handler, check if it needs to be extended + for (var tryCatchBlock : method.tryCatchBlocks) { + var start = tryCatchBlock.start; + var end = tryCatchBlock.end; + + // Find the last interruptible method call within this try block + MethodInsnNode lastCallInBlock = null; + for (var methodCall : interruptibleMethodCalls) { + if (isInstructionInRange(methodCall, start, end)) { + lastCallInBlock = methodCall; + } + } + + // If we found calls in this block, extend the end to cover restore code + if (lastCallInBlock != null) { + var restoreLabel = callToRestore.get(lastCallInBlock); + if (restoreLabel != null) { + // The restore label marks where execution continues after restoring + // We need to move the end label to after the restore code + // Find the "normal" label which marks the end of capture/restore code + var normalLabel = findNormalLabel(lastCallInBlock); + if (normalLabel != null) { + tryCatchBlock.end = normalLabel; + logger.debug(" Extended exception handler to cover capture code for {}", lastCallInBlock.name); + } + } + } + } + } + + /** + * Find the "normal" execution label after capture code for a method call. + * This label marks where normal (non-serializing) execution continues. + * + * @param methodCall Method call instruction. + * @return Normal execution label, or null if not found. + */ + private LabelNode findNormalLabel(MethodInsnNode methodCall) { + // After a method call, the capture code structure includes a "normal" label + // that marks where execution continues in the non-serializing case + // Scan forward from the method call to find this label + var current = methodCall.getNext(); + int labelCount = 0; + while (current != null && labelCount < 3) { + if (current instanceof LabelNode label) { + // The pattern in createCaptureAndRestoreCodeForMethod creates: + // - First: serializing label + // - Second: restore label (if restore == true) + // - Third: normal label + // We want the "normal" label which is typically after the restore label + labelCount++; + if (labelCount >= 2) { + // Return the label after serializing/restore + return label; + } + } + current = current.getNext(); + // Don't scan too far + if (current != null && current.getOpcode() >= 0) { + // Hit a real instruction, might have found our label already + break; + } + } + return null; + } + + /** + * Check if an instruction is within the range defined by start and end labels. + * + * @param instruction Instruction to check. + * @param start Start label of the range. + * @param end End label of the range. + * @return True if instruction is in range. + */ + private boolean isInstructionInRange(AbstractInsnNode instruction, LabelNode start, LabelNode end) { + var current = start.getNext(); + while (current != null && current != end) { + if (current == instruction) { + return true; + } + current = current.getNext(); + } + return false; + } } diff --git a/src/main/java/org/serialthreads/transformer/strategies/frequent3/OriginalMethodTransformer.java b/src/main/java/org/serialthreads/transformer/strategies/frequent3/OriginalMethodTransformer.java index 3cc8a69..6633d70 100644 --- a/src/main/java/org/serialthreads/transformer/strategies/frequent3/OriginalMethodTransformer.java +++ b/src/main/java/org/serialthreads/transformer/strategies/frequent3/OriginalMethodTransformer.java @@ -1,7 +1,10 @@ package org.serialthreads.transformer.strategies.frequent3; +import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.VarInsnNode; import org.objectweb.asm.tree.analysis.AnalyzerException; @@ -42,6 +45,7 @@ public MethodNode transform() throws AnalyzerException { replaceReturns(); insertCaptureCode(); + // updateExceptionTableForOriginalMethod(); // TODO: May not be needed createRestoreHandlerMethod(); addThreadAndFrame(); fixMaxs(); @@ -53,6 +57,99 @@ public MethodNode transform() throws AnalyzerException { return method; } + /** + * Update exception table to cover inserted capture code in original method. + * When an interruptible method call is inside a try-catch block, the capture code + * inserted after the call must also be covered by the same exception handler. + */ + private void updateExceptionTableForOriginalMethod() { + if (method.tryCatchBlocks == null || method.tryCatchBlocks.isEmpty()) { + return; + } + + logger.debug(" Updating exception table for original method"); + + // For each exception handler, extend it to cover capture code + for (var tryCatchBlock : method.tryCatchBlocks) { + var start = tryCatchBlock.start; + var end = tryCatchBlock.end; + + // Find the last interruptible method call within this try block + MethodInsnNode lastCallInBlock = null; + for (var methodCall : interruptibleMethodCalls) { + if (isInstructionInRange(methodCall, start, end)) { + lastCallInBlock = methodCall; + } + } + + // If we found calls in this block, extend the end to cover capture code + if (lastCallInBlock != null) { + // Find the "normal" label which marks the end of capture code + var normalLabel = findNormalLabel(lastCallInBlock); + if (normalLabel != null) { + tryCatchBlock.end = normalLabel; + logger.debug(" Extended exception handler to cover capture code for {}", lastCallInBlock.name); + } + } + } + } + + /** + * Find the "normal" execution label after capture code for a method call. + * + * @param methodCall Method call instruction. + * @return Normal execution label, or null if not found. + */ + private LabelNode findNormalLabel(MethodInsnNode methodCall) { + // After a method call in original method (without restore), the pattern is: + // methodCall -> IFEQ -> (capture code) -> serializing label -> RETURN -> normal label -> (restore return value) + // We want to find the "normal" label + var current = methodCall.getNext(); + int seenReturns = 0; + while (current != null && seenReturns < 2) { + // Look for the pattern: after we see a return, the next label is likely "normal" + if (current.getOpcode() == org.objectweb.asm.Opcodes.IRETURN || + current.getOpcode() == org.objectweb.asm.Opcodes.RETURN) { + seenReturns++; + // After the return(s), look for the next label + if (seenReturns == 1) { + var next = current.getNext(); + while (next != null) { + if (next instanceof LabelNode label) { + return label; + } + next = next.getNext(); + // Stop if we hit another real instruction + if (next != null && next.getOpcode() >= 0) { + break; + } + } + } + } + current = current.getNext(); + } + return null; + } + + /** + * Check if an instruction is within the range defined by start and end labels. + * + * @param instruction Instruction to check. + * @param start Start label of the range. + * @param end End label of the range. + * @return True if instruction is in range. + */ + private boolean isInstructionInRange(AbstractInsnNode instruction, LabelNode start, LabelNode end) { + var current = start.getNext(); + while (current != null && current != end) { + if (current == instruction) { + return true; + } + current = current.getNext(); + } + return false; + } + /** * Insert frame restoring code at the begin of an interruptible method. */ diff --git a/src/main/java/org/serialthreads/transformer/strategies/frequent3/RunMethodTransformer.java b/src/main/java/org/serialthreads/transformer/strategies/frequent3/RunMethodTransformer.java index d4ba255..ee3fc11 100644 --- a/src/main/java/org/serialthreads/transformer/strategies/frequent3/RunMethodTransformer.java +++ b/src/main/java/org/serialthreads/transformer/strategies/frequent3/RunMethodTransformer.java @@ -1,14 +1,19 @@ package org.serialthreads.transformer.strategies.frequent3; +import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.JumpInsnNode; import org.objectweb.asm.tree.LabelNode; import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.analysis.AnalyzerException; import org.serialthreads.transformer.classcache.IClassInfoCache; +import java.util.ArrayList; import java.util.List; +import static org.objectweb.asm.Opcodes.GOTO; + /** * Method transformer for run methods. */ @@ -37,6 +42,7 @@ public MethodNode transform() throws AnalyzerException { replaceRunReturns(); var restores = insertCaptureAndRestoreCode(true); + redirectRestoreLabelsOutsideTryCatch(restores); createRestoreHandlerRun(restores); addThreadAndFrame(); fixMaxs(); @@ -45,6 +51,77 @@ public MethodNode transform() throws AnalyzerException { return method; } + /** + * For each restore label that falls inside a try-catch block range, create a pre-initialization + * block placed outside the try-catch block range and redirect the restore entry to it. + * + *

Background: the ASM verifier merges exception handler states at the entry of every basic + * block that is covered by a try-catch block. A restore label (generated by + * {@code insertCaptureAndRestoreCode}) is inserted right after the interruptible method call, + * so it lands inside the try-catch range. At the restore label's entry, locals that will be + * read from the frame have not yet been initialized, causing a verification error. + * + *

The fix: place a pre-init block after the method body (outside all try-catch + * block ranges). The pre-init block initializes those locals to their type defaults and then + * jumps to the original restore label. The TABLESWITCH dispatcher then targets the pre-init + * block instead of the restore label directly. This ensures the verifier sees initialized + * locals at the restore label's entry (carried in via the GOTO from the pre-init block). + * + * @param restores Restore labels, modified in place so the TABLESWITCH targets pre-init blocks. + */ + private void redirectRestoreLabelsOutsideTryCatch(List restores) { + if (method.tryCatchBlocks == null || method.tryCatchBlocks.isEmpty()) { + return; + } + + var methodCallsList = new ArrayList<>(interruptibleMethodCalls); + var last = method.instructions.getLast(); + + for (int i = 0; i < restores.size(); i++) { + var restoreLabel = restores.get(i); + if (restoreLabel == null) continue; + + // Check if this restore label falls inside any try-catch block range. + boolean insideTryCatch = method.tryCatchBlocks.stream() + .anyMatch(tcb -> isInRange(restoreLabel, tcb.start, tcb.end)); + if (!insideTryCatch) continue; + + // Get the metaInfo for the corresponding method call (needed for defaultInitLocals). + var methodCall = methodCallsList.get(i); + var metaInfo = metaInfos.get(methodCall); + + // Build the pre-init block: initialize locals to type defaults, then jump to restore label. + // The pre-init block is appended at the very end of the method, after all try-catch ranges. + var preInitLabel = new LabelNode(); + var preInitCode = new InsnList(); + preInitCode.add(preInitLabel); + preInitCode.add(threadCode.defaultInitLocals(methodCall, metaInfo)); + preInitCode.add(new JumpInsnNode(GOTO, restoreLabel)); + + method.instructions.insert(last, preInitCode); + + // Point the TABLESWITCH to the pre-init block, not the restore label. + restores.set(i, preInitLabel); + } + } + + /** + * Check if an instruction lies strictly between two labels in the instruction list. + * + * @param instruction Instruction to locate. + * @param start Label that begins the range (exclusive). + * @param end Label that ends the range (exclusive). + * @return True if {@code instruction} is between {@code start} and {@code end}. + */ + private boolean isInRange(AbstractInsnNode instruction, LabelNode start, LabelNode end) { + for (var current = start.getNext(); current != null && current != end; current = current.getNext()) { + if (current == instruction) { + return true; + } + } + return false; + } + /** * Insert frame restoring code at the begin of the run() method. * diff --git a/src/test/java/org/serialthreads/transformer/strategies/TransformerIntegration_AbstractTest.java b/src/test/java/org/serialthreads/transformer/strategies/TransformerIntegration_AbstractTest.java index 5e673e4..e188fbb 100644 --- a/src/test/java/org/serialthreads/transformer/strategies/TransformerIntegration_AbstractTest.java +++ b/src/test/java/org/serialthreads/transformer/strategies/TransformerIntegration_AbstractTest.java @@ -1,7 +1,6 @@ package org.serialthreads.transformer.strategies; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.serialthreads.context.IRunnable; import org.serialthreads.context.SerialThreadManager; @@ -148,7 +147,6 @@ void testTailCall() { /** * Test exception handling. */ - @Disabled // TODO markus 2018-01-04: Implement exception handling. @Test void testException() { var test = new TestException(); diff --git a/src/test/performance/org/serialthreads/performance/SerialThreadTest.java b/src/test/performance/org/serialthreads/performance/SerialThreadTest.java index 2081bb6..ccf9aeb 100644 --- a/src/test/performance/org/serialthreads/performance/SerialThreadTest.java +++ b/src/test/performance/org/serialthreads/performance/SerialThreadTest.java @@ -1,17 +1,15 @@ package org.serialthreads.performance; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.extension.ExtendWith; import org.serialthreads.Interrupt; import org.serialthreads.Interruptible; import org.serialthreads.agent.Transform; import org.serialthreads.context.SimpleSerialThreadManager; -import org.serialthreads.transformer.strategies.frequent3.FrequentInterruptsTransformer3; /** * Test to analyze performance of threading with serial threads. */ -@Transform(transformer = FrequentInterruptsTransformer3.class, classPrefixes = "org.serialthreads.performance") +@Transform(classPrefixes = "org.serialthreads.performance") class SerialThreadTest extends AbstractPerformanceTest { private Thread managerThread; private volatile boolean ready; diff --git a/src/testFixtures/java/org/serialthreads/agent/Transform.java b/src/testFixtures/java/org/serialthreads/agent/Transform.java index 237135f..daf1475 100644 --- a/src/testFixtures/java/org/serialthreads/agent/Transform.java +++ b/src/testFixtures/java/org/serialthreads/agent/Transform.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.serialthreads.transformer.ITransformer; +import org.serialthreads.transformer.strategies.frequent3.FrequentInterruptsTransformer3; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; @@ -20,7 +21,7 @@ /** * Transformer class. */ - Class transformer(); + Class transformer() default FrequentInterruptsTransformer3.class; /** * Prefixes of classes to transform. "org.serialthreads." will always be added. diff --git a/src/testFixtures/java/org/serialthreads/agent/TransformingExtension.java b/src/testFixtures/java/org/serialthreads/agent/TransformingExtension.java index 7e65f12..7eece3f 100644 --- a/src/testFixtures/java/org/serialthreads/agent/TransformingExtension.java +++ b/src/testFixtures/java/org/serialthreads/agent/TransformingExtension.java @@ -78,11 +78,12 @@ private T runOnTransformed( ExtensionContext context) throws Throwable { var instance = getOrCreateInstance(context); var method = findMethod(instance.getClass(), invocationContext.getExecutable()); + var args = invocationContext.getArguments().toArray(); // Do NOT call the test method of the untransformed instance. invocation.skip(); try { // Invoke the transformed test method instead. - return (T) method.invoke(instance); + return (T) method.invoke(instance, args); } catch (InvocationTargetException e) { throw e.getCause(); }