docs/architecture/BTraceInstrAnalysis.md
Document Version: 2.0 Date: 2025-12-22 (Updated) Original Analysis: 2025-12-20 Analysis Scope: btrace-instr module correctness, performance, and architecture
Implementation Complete: 10 of 14 issues resolved
| Priority | Category | Fixed | Remaining |
|---|---|---|---|
| P0 | Critical Correctness | 2/2 ✅ | 0 |
| P1 | High-Impact Performance | 5/5 ✅ | 0 |
| P2 | Allocation Pressure | 1/2 | 1 |
| P3 | Code Quality | 1/3 | 2 |
| P4 | Test Coverage | 0/1 | 1 |
All critical (P0) and high-impact performance (P1) issues have been resolved:
Performance Improvements:
Correctness:
P2-P4 Issues (Lower Priority):
This document presents a comprehensive analysis of the btrace-instr module, identifying critical issues that affect correctness, performance, and maintainability. The analysis was conducted through systematic exploration of the instrumentation engine, handler management, and test infrastructure.
14 issues identified across 3 priority levels:
| Priority | Category | Count | Impact |
|---|---|---|---|
| P0 | Critical Correctness | 2 | Data corruption, type safety violations |
| P1 | High-Impact Performance | 5 | 20-30% performance improvement potential |
| P2-P4 | Architectural Debt | 7 | Maintainability, test coverage, code quality |
Fixing P0+P1 issues (estimated 3 days) will provide highest value:
Priority: P0 (Critical)
File: btrace-instr/src/main/java/org/openjdk/btrace/instr/MethodTracker.java:62
Type: Data Corruption
The registerCounter() method contains a copy-paste error that corrupts the origMeans array during resize operations:
public static synchronized void registerCounter(int methodId, int mean) {
if (counters.length <= methodId) {
int newLen = methodId * 2;
counters = Arrays.copyOf(counters, newLen);
rLocks = Arrays.copyOf(rLocks, newLen);
means = Arrays.copyOf(means, newLen);
origMeans = Arrays.copyOf(means, newLen); // LINE 62 - BUG: copies 'means' instead of 'origMeans'
samplers = Arrays.copyOf(samplers, newLen);
tsArray = Arrays.copyOf(tsArray, newLen);
}
// ...
}
What Goes Wrong:
origMeans is copied from means (current adjusted values) instead of origMeans (original values)Affected Code Paths:
hitAdaptive() (line 130-158) - uses origMeans[methodId] to adjust sampling ratehitTimedAdaptive() (line 168-196) - same issue with timingReal-World Impact:
Fix (1-line change):
origMeans = Arrays.copyOf(origMeans, newLen); // Correct: copy origMeans, not means
Verification:
@Test
void testRegisterCounterPreservesOrigMeans() {
// Register 100 method IDs to trigger resize
for (int i = 0; i < 100; i++) {
MethodTracker.registerCounter(i, 1000 + i);
}
// Verify origMeans preserved correctly
// (Requires adding test accessor for origMeans array)
for (int i = 0; i < 100; i++) {
assertEquals(1000 + i, getOrigMean(i), "Original mean corrupted for method " + i);
}
}
Priority: P0 (Critical)
File: btrace-instr/src/main/java/org/openjdk/btrace/instr/VariableMapper.java:136,139
Type: Type Safety Violation
The map() method uses sentinel value 0xFFFFFFFF to signal "invalid mapping" instead of proper error handling:
private int map(int var, int[] currentMapping) {
// ... snip ...
int offset = (var - argsSize);
if (offset >= 0) {
if (currentMapping.length <= offset) {
return 0xFFFFFFFF; // LINE 136 - MAGIC VALUE
}
int newVar = currentMapping[offset];
return (newVar & REMAP_FLAG) != 0 ? unmask(newVar) : 0xFFFFFFFF; // LINE 139 - MAGIC VALUE
}
return var;
}
Callers Check Magic Value:
// InstrumentingMethodVisitor.java:1055
int origVar = vMapper.map(var, label);
if (origVar != 0xFFFFFFFF) { // Checking for magic value
// ... use origVar ...
}
// InstrumentingMethodVisitor.java:1075
int mappedVar = vMapper.map(v.index, label);
if (mappedVar != 0xFFFFFFFF) {
// ... use mappedVar ...
}
Type Safety Issues:
int can't distinguish between valid value and errorFailure Modes:
Option A: Exception-based (Recommended)
Best for truly exceptional conditions (unmapped variables should not occur in correct code).
public int map(int var) {
return map(var, mapping);
}
public int map(int var, Label label) {
return map(var, labelMappings.getOrDefault(label, mapping));
}
private int map(int var, int[] currentMapping) {
if (((var & REMAP_FLAG) != 0)) {
return unmask(var);
}
int offset = (var - argsSize);
if (offset >= 0) {
if (currentMapping.length <= offset) {
throw new IllegalStateException(
String.format("Unmapped variable offset %d (var=%d, argsSize=%d)", offset, var, argsSize));
}
int newVar = currentMapping[offset];
if ((newVar & REMAP_FLAG) == 0) {
throw new IllegalStateException(
String.format("Variable not remapped: var=%d, offset=%d", var, offset));
}
return unmask(newVar);
}
return var;
}
Caller Updates:
// InstrumentingMethodVisitor.java:1055
try {
int origVar = vMapper.map(var, label);
// ... use origVar (always valid) ...
} catch (IllegalStateException e) {
log.debug("Variable not mapped: {}", var, e);
// ... handle unmapped variable ...
}
Option B: OptionalInt (Alternative)
Best for nullable semantics where "no mapping" is a valid state.
public OptionalInt tryMap(int var) {
return tryMap(var, mapping);
}
public OptionalInt tryMap(int var, Label label) {
return tryMap(var, labelMappings.getOrDefault(label, mapping));
}
private OptionalInt tryMap(int var, int[] currentMapping) {
if (((var & REMAP_FLAG) != 0)) {
return OptionalInt.of(unmask(var));
}
int offset = (var - argsSize);
if (offset >= 0) {
if (currentMapping.length <= offset) {
return OptionalInt.empty(); // No mapping
}
int newVar = currentMapping[offset];
if ((newVar & REMAP_FLAG) == 0) {
return OptionalInt.empty(); // Not remapped
}
return OptionalInt.of(unmask(newVar));
}
return OptionalInt.of(var);
}
Caller Updates:
// InstrumentingMethodVisitor.java:1055
OptionalInt origVar = vMapper.tryMap(var, label);
if (origVar.isPresent()) {
// ... use origVar.getAsInt() ...
} else {
// ... handle unmapped variable ...
}
Use Option A (Exception-based) because:
map() methodPriority: P1 (High Performance Impact)
File: btrace-instr/src/main/java/org/openjdk/btrace/instr/HandlerRepositoryImpl.java:43-77
Type: Hot Path Allocation
The getProbeHandler() method regenerates identical bytecode on every invokedynamic linkage (Java 15+ only). This is called from Indy.bootstrap() during invokedynamic callsite linking.
Current Code Flow:
public static byte[] getProbeHandler(
String callerName, String probeName, String handlerName, String handlerDesc) {
// LINE 45 - NEW ALLOCATION (every call)
DebugSupport debugSupport = new DebugSupport(SharedSettings.GLOBAL);
BTraceProbe probe = probeMap.get(probeName);
// LINE 47 - NEW ALLOCATION (every call)
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
String handlerClassName = callerName.replace('.', '/') + "$" + probeName.replace('/', '_');
// LINE 50 - NEW ALLOCATION (every call - anonymous class instance)
ClassVisitor visitor =
new CopyingVisitor(handlerClassName, true, writer) {
@Override
protected String getMethodName(String name) {
int idx = name.lastIndexOf("$");
if (idx > -1) {
return name.substring(idx + 1);
}
return name;
}
};
probe.copyHandlers(visitor); // Generates bytecode
byte[] data = writer.toByteArray(); // LINE 63 - Same bytes every time
// ... dump logic ...
return data;
}
Allocation Profile (per call):
Frequency:
Key Insight: Output is deterministic
public final class HandlerRepositoryImpl {
private static final Logger log = LoggerFactory.getLogger(HandlerRepositoryImpl.class);
private static final Map<String, BTraceProbe> probeMap = new ConcurrentHashMap<>();
// NEW: Cache for generated handler bytecode
private static final Map<String, byte[]> handlerBytecodeCache = new ConcurrentHashMap<>();
// ... existing static initializer ...
public static void registerProbe(BTraceProbe probe) {
probeMap.put(probe.getClassName(true), probe);
}
public static void unregisterProbe(BTraceProbe probe) {
String probeName = probe.getClassName(true);
probeMap.remove(probeName);
// NEW: Invalidate cache entries for this probe
String probePrefix = probeName + "#";
handlerBytecodeCache.keySet().removeIf(key -> key.startsWith(probePrefix));
}
public static byte[] getProbeHandler(
String callerName, String probeName, String handlerName, String handlerDesc) {
// NEW: Cache key from handler signature
String cacheKey = probeName + "#" + handlerName + handlerDesc;
// NEW: Check cache first (lock-free read)
return handlerBytecodeCache.computeIfAbsent(cacheKey, k -> {
// EXISTING: Generation logic (only runs on cache miss)
DebugSupport debugSupport = new DebugSupport(SharedSettings.GLOBAL);
BTraceProbe probe = probeMap.get(probeName);
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
String handlerClassName = callerName.replace('.', '/') + "$" + probeName.replace('/', '_');
ClassVisitor visitor =
new CopyingVisitor(handlerClassName, true, writer) {
@Override
protected String getMethodName(String name) {
int idx = name.lastIndexOf("$");
return idx > -1 ? name.substring(idx + 1) : name;
}
};
probe.copyHandlers(visitor);
byte[] data = writer.toByteArray();
// Dump logic (still runs, useful for debugging)
if (debugSupport.isDumpClasses()) {
try {
String handlerPath =
debugSupport.getDumpClassDir() + "/" + handlerClassName.replace('/', '_') + ".class";
log.debug("BTrace INDY handler dumped: {}", handlerPath);
Files.write(Paths.get(handlerPath), data, StandardOpenOption.CREATE);
} catch (Throwable e) {
log.debug("Failed to dump BTrace INDY handler", e);
}
}
return data;
});
}
}
Performance Gains:
Memory Cost:
Correctness:
unregisterProbe() (probe updates are rare)ConcurrentHashMap provides thread-safe accesscomputeIfAbsent() ensures single generation per key@Test
void testHandlerBytecodeCache() {
// Register a probe
BTraceProbe probe = createTestProbe("TestProbe");
HandlerRepositoryImpl.registerProbe(probe);
// First call - should generate bytecode
byte[] handler1 = HandlerRepositoryImpl.getProbeHandler(
"com.example.Test", "TestProbe", "onMethod", "()V");
// Second call - should return cached bytecode
byte[] handler2 = HandlerRepositoryImpl.getProbeHandler(
"com.example.Test", "TestProbe", "onMethod", "()V");
// Verify same bytecode returned (cached)
assertSame(handler1, handler2, "Should return cached bytecode");
// Unregister probe - should invalidate cache
HandlerRepositoryImpl.unregisterProbe(probe);
// Re-register and call again - should regenerate
HandlerRepositoryImpl.registerProbe(probe);
byte[] handler3 = HandlerRepositoryImpl.getProbeHandler(
"com.example.Test", "TestProbe", "onMethod", "()V");
// Verify new bytecode generated (cache was invalidated)
assertNotSame(handler1, handler3, "Should regenerate after unregister");
}
Priority: P1 (High Performance Impact)
File: btrace-instr/src/main/java/org/openjdk/btrace/instr/BTraceProbeSupport.java:103,112
Type: Repeated Computation
The getApplicableHandlers() method compiles regex patterns on every class transformation:
Collection<OnMethod> getApplicableHandlers(BTraceClassReader cr) {
Collection<OnMethod> applicables = new ArrayList<>(onMethods.size());
String targetName = cr.getJavaClassName();
outer:
for (OnMethod om : onMethods) {
String probeClass = om.getClazz();
// ... exact match check ...
// LINE 102-107 - PATTERN COMPILED EVERY TIME
if (om.isClassRegexMatcher() && !om.isClassAnnotationMatcher()) {
Pattern p = Pattern.compile(probeClass); // EXPENSIVE
if (p.matcher(targetName).matches()) {
applicables.add(om);
continue;
}
}
// LINE 109-118 - PATTERN COMPILED AGAIN
if (om.isClassAnnotationMatcher()) {
Collection<String> annoTypes = cr.getAnnotationTypes();
if (om.isClassRegexMatcher()) {
Pattern p = Pattern.compile(probeClass); // EXPENSIVE (duplicate)
for (String annoType : annoTypes) {
if (p.matcher(annoType).matches()) {
applicables.add(om);
continue outer;
}
}
}
// ...
}
// ...
}
return applicables;
}
Call Frequency:
Pattern Compilation Cost:
Aggregate Impact:
Memory Churn:
Step 1: Add Pattern field to OnMethod
// In OnMethod.java
public class OnMethod {
// Existing fields...
private volatile Pattern compiledClassPattern = null;
/**
* Returns pre-compiled pattern for class matching.
* Pattern is compiled lazily on first access and cached.
*
* @return compiled pattern, or null if not a regex matcher
*/
public Pattern getClassPattern() {
if (!isClassRegexMatcher()) {
return null;
}
if (compiledClassPattern == null) {
synchronized (this) {
if (compiledClassPattern == null) {
try {
compiledClassPattern = Pattern.compile(getClazz());
} catch (PatternSyntaxException e) {
log.warn("Invalid regex pattern in OnMethod: {}", getClazz(), e);
compiledClassPattern = Pattern.compile("(?!)"); // Never matches
}
}
}
}
return compiledClassPattern;
}
// Existing methods...
}
Step 2: Update BTraceProbeSupport
Collection<OnMethod> getApplicableHandlers(BTraceClassReader cr) {
Collection<OnMethod> applicables = new ArrayList<>(onMethods.size());
String targetName = cr.getJavaClassName();
outer:
for (OnMethod om : onMethods) {
String probeClass = om.getClazz();
if (probeClass == null || probeClass.isEmpty()) continue;
if (probeClass.equals(targetName)) {
applicables.add(om);
continue;
}
// Check regex match - USE PRE-COMPILED PATTERN
if (om.isClassRegexMatcher() && !om.isClassAnnotationMatcher()) {
Pattern p = om.getClassPattern(); // Pre-compiled, no allocation
if (p != null && p.matcher(targetName).matches()) {
applicables.add(om);
continue;
}
}
if (om.isClassAnnotationMatcher()) {
Collection<String> annoTypes = cr.getAnnotationTypes();
if (om.isClassRegexMatcher()) {
Pattern p = om.getClassPattern(); // Pre-compiled, no allocation
if (p != null) {
for (String annoType : annoTypes) {
if (p.matcher(annoType).matches()) {
applicables.add(om);
continue outer;
}
}
}
} else {
if (annoTypes.contains(probeClass)) {
applicables.add(om);
continue;
}
}
}
// Check subtype hierarchy
if (om.isSubtypeMatcher()) {
if (isSubTypeOf(cr.getClassName(), cr.getClassLoader(), probeClass)) {
applicables.add(om);
}
}
}
return applicables;
}
Performance:
Memory:
Thread Safety:
volatile ensures visibility across threadsCould compile patterns during OnMethod construction instead of lazily:
// In OnMethod constructor or builder
if (isClassRegexMatcher()) {
try {
this.compiledClassPattern = Pattern.compile(clazz);
} catch (PatternSyntaxException e) {
log.warn("Invalid regex pattern: {}", clazz, e);
this.compiledClassPattern = Pattern.compile("(?!)");
}
}
Tradeoff:
Note: ClassFilter already implements pattern caching correctly:
// ClassFilter.java:276
private void init(Collection<OnMethod> onMethods) {
// ... snip ...
for (OnMethod om : onMethods) {
String className = om.getClazz();
if (om.isClassRegexMatcher()) {
if (om.isClassAnnotationMatcher()) {
classAnnotationPatterns.add(Pattern.compile(className)); // Compiled once
} else {
classNamePatterns.add(Pattern.compile(className)); // Compiled once
}
}
// ...
}
}
Refactoring Opportunity: Extract pattern caching logic to shared utility.
Priority: P1 (Performance)
File: btrace-instr/src/main/java/org/openjdk/btrace/instr/CallGraph.java
Type: Algorithm Inefficiency
CallGraph uses O(n) linear search through all nodes for three operations:
Occurrence 1: addEdge() - Lines 62-69
public void addEdge(String fromId, String toId) {
Node fromNode = null;
Node toNode = null;
for (Node n : nodes) { // O(n) search
if (n.id.equals(fromId)) {
fromNode = n;
}
if (n.id.equals(toId)) {
toNode = n;
}
}
// ... create nodes if needed ...
fromNode.addEdge(toNode);
}
Occurrence 2: collectOutgoings() - Lines 135-145
private void collectOutgoings(String methodId, Set<String> closure) {
for (Node n : nodes) { // O(n) search
if (n.id.equals(methodId)) {
for (Edge e : n.outgoing) {
// ... recursive collection ...
}
break;
}
}
}
Occurrence 3: collectIncomings() - Lines 149-159
private void collectIncomings(String methodId, Set<String> closure) {
for (Node n : nodes) { // O(n) search
if (n.id.equals(methodId)) {
for (Edge e : n.incoming) {
// ... recursive collection ...
}
break;
}
}
}
Complexity Analysis:
addEdge() called for every method call edge (100s-1000s)Real-World Scenarios:
Measurement:
public final class CallGraph {
private final Set<Node> nodes = new HashSet<>();
private final Map<String, Node> nodeIndex = new HashMap<>(); // NEW: O(1) lookup index
private final Set<Edge> edges = new HashSet<>();
// ... existing Node and Edge classes ...
public void addEdge(String fromId, String toId) {
// O(1) lookup instead of O(n)
Node fromNode = nodeIndex.get(fromId);
Node toNode = nodeIndex.get(toId);
if (fromNode == null) {
fromNode = new Node(fromId);
nodes.add(fromNode);
nodeIndex.put(fromId, fromNode); // Maintain index
}
if (toNode == null) {
toNode = new Node(toId);
nodes.add(toNode);
nodeIndex.put(toId, toNode); // Maintain index
}
fromNode.addEdge(toNode);
}
public void addStarting(String methodId) {
// O(1) lookup
Node n = nodeIndex.get(methodId);
if (n == null) {
n = new Node(methodId);
nodes.add(n);
nodeIndex.put(methodId, n); // Maintain index
}
n.entry = true;
}
private void collectOutgoings(String methodId, Set<String> closure) {
// O(1) lookup instead of O(n)
Node n = nodeIndex.get(methodId);
if (n != null) {
for (Edge e : n.outgoing) {
if (!closure.contains(e.to.id)) {
closure.add(e.to.id);
collectOutgoings(e.to.id, closure);
}
}
}
}
private void collectIncomings(String methodId, Set<String> closure) {
// O(1) lookup instead of O(n)
Node n = nodeIndex.get(methodId);
if (n != null) {
for (Edge e : n.incoming) {
if (!closure.contains(e.from.id)) {
closure.add(e.from.id);
collectIncomings(e.from.id, closure);
}
}
}
}
// hasCycle() - no changes needed (doesn't use lookup)
// Existing getter methods remain unchanged
}
Performance:
Memory:
Correctness:
@Test
void testCallGraphIndexConsistency() {
CallGraph graph = new CallGraph();
// Add edges
graph.addEdge("method1", "method2");
graph.addEdge("method2", "method3");
graph.addEdge("method1", "method3");
// Verify nodes added correctly
assertEquals(3, graph.nodeCount());
// Verify closure collection works
Set<String> closure = graph.getOutgoingClosure("method1");
assertTrue(closure.contains("method2"));
assertTrue(closure.contains("method3"));
}
@Test
void testCallGraphPerformance() {
CallGraph graph = new CallGraph();
// Build large graph
for (int i = 0; i < 1000; i++) {
for (int j = i + 1; j < i + 5 && j < 1000; j++) {
graph.addEdge("method" + i, "method" + j);
}
}
// Verify reasonable construction time (should be < 10ms)
long start = System.nanoTime();
Set<String> closure = graph.getOutgoingClosure("method0");
long duration = System.nanoTime() - start;
assertTrue(duration < 10_000_000, "Closure collection too slow: " + duration + "ns");
}
Priority: P1 (Performance)
File: btrace-instr/src/main/java/org/openjdk/btrace/instr/InstrumentUtils.java:253
Type: Repeated Computation
The getActionPrefix() method performs string operations on every call:
// InstrumentUtils.java:253
static String getActionPrefix(String className) {
return Constants.BTRACE_METHOD_PREFIX + className.replace('/', '$') + "$";
}
Callers:
Instrumentor.getActionMethodName() - called for every OnMethod handlerBTraceTransformer.unregister() - called for every OnMethod during unregistrationCall Frequency:
Cost per call:
Aggregate:
Memory:
// In BTraceProbe.java (interface)
public interface BTraceProbe {
// Existing methods...
/**
* Returns the action method prefix for this probe.
* This is computed once and cached.
*
* Format: BTRACE_METHOD_PREFIX + className.replace('/', '$') + "$"
* Example: "$btrace$com$example$MyProbe$"
*
* @return cached action prefix
*/
default String getActionPrefix() {
return Constants.BTRACE_METHOD_PREFIX +
getClassName(true).replace('/', '$') + "$";
}
}
// In BTraceProbeSupport.java or BTraceProbePersisted.java
public class BTraceProbeSupport implements BTraceProbe {
// Existing fields...
private String cachedActionPrefix = null; // NEW: cache field
@Override
public String getActionPrefix() {
if (cachedActionPrefix == null) {
cachedActionPrefix = Constants.BTRACE_METHOD_PREFIX +
getClassName(true).replace('/', '$') + "$";
}
return cachedActionPrefix;
}
// Existing methods...
}
// Update callers to use cached version
// InstrumentUtils.java - REMOVE getActionPrefix() or mark deprecated
@Deprecated
static String getActionPrefix(String className) {
// Kept for compatibility, but users should use BTraceProbe.getActionPrefix()
return Constants.BTRACE_METHOD_PREFIX + className.replace('/', '$') + "$";
}
static String getActionMethodName(BTraceProbe bp, String name) {
return bp.getActionPrefix() + name; // Use cached version
}
If probe doesn't have convenient place for field:
// InstrumentUtils.java
private static final Map<String, String> actionPrefixCache = new ConcurrentHashMap<>();
static String getActionPrefix(String className) {
return actionPrefixCache.computeIfAbsent(className, cn ->
Constants.BTRACE_METHOD_PREFIX + cn.replace('/', '$') + "$"
);
}
Tradeoff:
Performance:
Memory:
Priority: P1 (Test Performance)
File: btrace-instr/src/test/java/org/openjdk/btrace/instr/InstrumentorTestBase.java:406-418
Type: Algorithm Inefficiency
The loadFile() method uses quadratic memory allocation:
private byte[] loadFile(InputStream is) throws IOException {
byte[] result = new byte[0]; // Start with empty array
byte[] buffer = new byte[1024];
int read = -1;
while ((read = is.read(buffer)) > 0) {
// Allocate new array with old data + new data
byte[] newresult = new byte[result.length + read]; // O(n²) allocation
// Copy old data
System.arraycopy(result, 0, newresult, 0, result.length);
// Copy new data
System.arraycopy(buffer, 0, newresult, result.length, read);
result = newresult; // Old array becomes garbage
}
return result;
}
For 10KB file with 1KB buffer (10 iterations):
Total:
For larger files:
Test Impact:
private byte[] loadFile(InputStream is) throws IOException {
// ByteArrayOutputStream uses geometric growth (2× on resize)
ByteArrayOutputStream baos = new ByteArrayOutputStream(8192);
byte[] buffer = new byte[8192];
int read;
while ((read = is.read(buffer)) != -1) {
baos.write(buffer, 0, read); // Appends to internal buffer
}
return baos.toByteArray(); // Single final copy
}
How ByteArrayOutputStream Works:
For 10KB file:
Performance:
Code Simplicity:
Option A: Pre-allocated buffer (if file size known):
private byte[] loadFile(InputStream is, int expectedSize) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream(expectedSize);
// ... rest same ...
}
Option B: Files.readAllBytes() (if InputStream is FileInputStream):
private byte[] loadFile(String path) throws IOException {
return Files.readAllBytes(Paths.get(path));
}
Recommendation: Use ByteArrayOutputStream (simple, works for all InputStreams)
Priority: P2 (Optimization)
File: btrace-instr/src/main/java/org/openjdk/btrace/instr/InstrumentingMethodVisitor.java
Type: Hot Path Allocation
SavedState objects are allocated on every jump/branch/try-catch in instrumented methods:
// Line 234 - visitJumpInsn
SavedState state = new SavedState(localTypes, stack, newLocals, SavedState.CONDITIONAL);
jumpTargetStates.put(label, state);
// Line 246 - visitTableSwitchInsn
for (Label l : labels) {
SavedState state = new SavedState(localTypes, stack, newLocals, SavedState.CONDITIONAL);
jumpTargetStates.put(l, state);
}
// Line 342 - visitTryCatchBlock
SavedState state = new SavedState(localTypes, stack, newLocals, SavedState.TRY_CATCH);
jumpTargetStates.put(handler, state);
SavedState Constructor (lines 1670-1679):
SavedState(LocalVarTypes lvTypes, SimulatedStack sStack,
Collection<LocalVarSlot> newLocals, int kind) {
this.lvTypes = lvTypes.toArray(); // NEW ALLOCATION (Object[] copy)
this.sStack = sStack.toArray(); // NEW ALLOCATION (Object[] copy)
this.newLocals = new HashSet<>(newLocals); // NEW ALLOCATION (HashSet copy)
this.kind = kind;
}
Allocation Profile (per SavedState):
Frequency:
Aggregate:
Option A: Lazy Copy-on-Read (Recommended)
Don't copy data until it's actually needed (often never accessed):
class SavedState {
// Store references instead of copies
private final LocalVarTypes lvTypesRef;
private final SimulatedStack sStackRef;
private final Collection<LocalVarSlot> newLocalsRef;
// Lazy materialized copies
private Object[] lvTypesCopy = null;
private Object[] sStackCopy = null;
private Set<LocalVarSlot> newLocalsCopy = null;
private final int kind;
SavedState(LocalVarTypes lvTypes, SimulatedStack sStack,
Collection<LocalVarSlot> newLocals, int kind) {
this.lvTypesRef = lvTypes;
this.sStackRef = sStack;
this.newLocalsRef = newLocals;
this.kind = kind;
}
Object[] getLvTypes() {
if (lvTypesCopy == null) {
lvTypesCopy = lvTypesRef.toArray(); // Lazy copy
}
return lvTypesCopy;
}
Object[] getSStack() {
if (sStackCopy == null) {
sStackCopy = sStackRef.toArray(); // Lazy copy
}
return sStackCopy;
}
Set<LocalVarSlot> getNewLocals() {
if (newLocalsCopy == null) {
newLocalsCopy = new HashSet<>(newLocalsRef); // Lazy copy
}
return newLocalsCopy;
}
int getKind() {
return kind;
}
}
Benefits:
Tradeoffs:
Option B: ThreadLocal Object Pool
Reuse SavedState objects across multiple method instrumentations:
static class SavedStatePool {
private static final ThreadLocal<ArrayDeque<SavedState>> pool =
ThreadLocal.withInitial(() -> new ArrayDeque<>(16));
static SavedState acquire(LocalVarTypes lvTypes, SimulatedStack sStack,
Collection<LocalVarSlot> newLocals, int kind) {
SavedState state = pool.get().poll();
if (state == null) {
state = new SavedState(); // Allocate if pool empty
}
state.init(lvTypes, sStack, newLocals, kind); // Reuse existing object
return state;
}
static void release(SavedState state) {
state.clear(); // Reset for reuse
pool.get().offer(state);
}
}
class SavedState {
private Object[] lvTypes;
private Object[] sStack;
private Set<LocalVarSlot> newLocals;
private int kind;
SavedState() {
// Empty constructor for pooling
}
void init(LocalVarTypes lvTypes, SimulatedStack sStack,
Collection<LocalVarSlot> newLocals, int kind) {
this.lvTypes = lvTypes.toArray();
this.sStack = sStack.toArray();
this.newLocals = new HashSet<>(newLocals);
this.kind = kind;
}
void clear() {
this.lvTypes = null;
this.sStack = null;
this.newLocals = null;
}
// Getters remain same...
}
Challenges:
Benefits:
Start with Option A (Lazy Copy-on-Read):
If profiling shows high read rate:
Priority: P2 (Optimization)
File: btrace-instr/src/main/java/org/openjdk/btrace/instr/VariableMapper.java
Type: Frequent Allocation
VariableMapper performs frequent array copies during method transformation:
Arrays.copyOf() Calls:
public void noteLabel(Label label) {
labelMappings.put(currentLabel, Arrays.copyOf(mapping, nextMappedVar));
currentLabel = label;
}
void setMapping(int from, int to, int size) {
int padding = size == 1 ? 0 : 1;
if (mapping.length <= from + padding) {
mapping = Arrays.copyOf(mapping, Math.max(mapping.length * 2, from + padding + 1));
}
// ...
}
public int remap(int var, int size) {
// ...
int offset = var - argsSize;
if (offset >= mapping.length) {
mapping = Arrays.copyOf(mapping, Math.max(mapping.length * 2, offset + 1));
}
// ...
}
Frequency:
Cost Per Copy:
Aggregate:
Option A: Track Used Length (Recommended)
Don't copy entire mapping array, only used portion:
public class VariableMapper {
private final int argsSize;
private int nextMappedVar = 0;
private int[] mapping = new int[64]; // Larger initial size (was 8)
private static final Label FIRST_LABEL = new Label();
private Label currentLabel = FIRST_LABEL;
private final Map<Label, int[]> labelMappings = new HashMap<>(16);
public VariableMapper(int argsSize) {
this.argsSize = argsSize;
nextMappedVar = argsSize;
}
public void noteLabel(Label label) {
// Copy only used portion (not entire array)
labelMappings.put(currentLabel, Arrays.copyOf(mapping, nextMappedVar));
currentLabel = label;
}
// Rest remains same...
}
Benefits:
Option B: Cache Identical Mappings
Consecutive labels often have identical mappings (linear code):
public void noteLabel(Label label) {
int[] prevMapping = labelMappings.get(currentLabel);
// Check if current mapping same as previous
if (prevMapping != null && Arrays.equals(prevMapping, 0, nextMappedVar,
mapping, 0, nextMappedVar)) {
labelMappings.put(label, prevMapping); // Reuse (no copy)
} else {
labelMappings.put(label, Arrays.copyOf(mapping, nextMappedVar)); // Copy needed
}
currentLabel = label;
}
Benefits:
Tradeoffs:
Option C: Hybrid (Recommended)
Combine Option A (track used length) + larger initial size:
private int[] mapping = new int[64]; // Increased from 8
public void noteLabel(Label label) {
labelMappings.put(currentLabel, Arrays.copyOf(mapping, nextMappedVar));
currentLabel = label;
}
Simple change with good results:
Can estimate variable count from method descriptor:
public VariableMapper(int argsSize, String methodDescriptor) {
this.argsSize = argsSize;
this.nextMappedVar = argsSize;
// Estimate: args + 8 locals + 16 instrumentation vars
int estimatedVars = argsSize + 8 + 16;
this.mapping = new int[Math.max(64, estimatedVars)];
}
Avoids most resize operations.
Phase 1 (Low effort, good benefit):
Phase 2 (If profiling shows benefit):
Priority: P3 (Code Quality)
File: btrace-instr/src/main/java/org/openjdk/btrace/instr/MethodInstrumentor.java:511
Type: Debug Code
ValidationResult constructor contains debug stack trace dump:
public ValidationResult(boolean valid, int[] argsIndex) {
if (argsIndex == null) {
Thread.dumpStack(); // DEBUG CODE - PRINTS TO STDERR
}
isValid = valid;
this.argsIndex = argsIndex;
}
Replace with proper null check:
public ValidationResult(boolean valid, int[] argsIndex) {
this.argsIndex = Objects.requireNonNull(argsIndex, "argsIndex must not be null");
this.isValid = valid;
}
Priority: P3 (Type Safety) Files: Multiple test files Type: Type Safety Violation
test/btrace/BTRACE87.java:15:List a = new ArrayList(); // Raw type
test/DerivedClass.java:53:super(new ArrayList()); // Raw type
Add proper generics:
List<Object> a = new ArrayList<>();
super(new ArrayList<Object>());
Full audit of test code to find all raw type violations.
Priority: P3 (Maintenance) Location: Test infrastructure Type: Test Organization
396 golden files for 199 test scripts (2× duplication):
onmethod/ directory structure paralleled in multiple locationsPriority: P4 (Risk Mitigation) Type: Test Coverage
Concurrency Test:
@Test
void testConcurrentTransformation() {
BTraceTransformer transformer = new BTraceTransformer();
BTraceProbe probe = createTestProbe();
transformer.register(probe);
// 10 threads transforming same class
CountDownLatch latch = new CountDownLatch(10);
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
try {
byte[] classBytes = loadClass("TestTarget");
byte[] result = transformer.transform(null, "TestTarget",
null, null, classBytes);
assertNotNull(result);
} finally {
latch.countDown();
}
});
}
assertTrue(latch.await(10, TimeUnit.SECONDS));
}
ClassLoader GC Test:
@Test
void testClassLoaderGC() {
WeakReference<ClassLoader> clRef = new WeakReference<>(
new URLClassLoader(new URL[0])
);
// Register probe with classloader
BTraceProbe probe = createProbeWithClassLoader(clRef.get());
HandlerRepositoryImpl.registerProbe(probe);
// Force GC
System.gc();
Thread.sleep(100);
System.gc();
// Verify classloader collected
assertNull(clRef.get(), "ClassLoader not garbage collected");
// Verify no memory leak in ClassCache
// (Requires access to ClassCache internals)
}
Large Method Test:
@Test
void testLargeMethod() {
// Generate class with 300 local variables
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
cw.visit(V1_8, ACC_PUBLIC, "LargeMethod", null, "java/lang/Object", null);
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "test", "()V", null, null);
mv.visitCode();
// Create 300 local variables
for (int i = 0; i < 300; i++) {
mv.visitLdcInsn(i);
mv.visitVarInsn(ISTORE, i + 1);
}
mv.visitInsn(RETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();
cw.visitEnd();
// Transform with BTrace
byte[] classBytes = cw.toByteArray();
BTraceTransformer transformer = new BTraceTransformer();
transformer.register(createTestProbe());
byte[] result = transformer.transform(null, "LargeMethod",
null, null, classBytes);
assertNotNull(result, "Failed to instrument large method");
// Verify instrumented class loads and runs
ClassLoader cl = new ByteArrayClassLoader(result);
Class<?> clazz = cl.loadClass("LargeMethod");
clazz.getMethod("test").invoke(clazz.newInstance());
}
Handler Cache Test:
@Test
void testHandlerCacheInvalidation() {
BTraceProbe probe = createTestProbe("TestProbe");
HandlerRepositoryImpl.registerProbe(probe);
// Get handler (should cache)
byte[] handler1 = HandlerRepositoryImpl.getProbeHandler(
"com.example.Test", "TestProbe", "onMethod", "()V");
// Get again (should hit cache)
byte[] handler2 = HandlerRepositoryImpl.getProbeHandler(
"com.example.Test", "TestProbe", "onMethod", "()V");
assertSame(handler1, handler2, "Should return cached bytecode");
// Unregister probe
HandlerRepositoryImpl.unregisterProbe(probe);
// Re-register
HandlerRepositoryImpl.registerProbe(probe);
// Get handler again (cache should be invalidated)
byte[] handler3 = HandlerRepositoryImpl.getProbeHandler(
"com.example.Test", "TestProbe", "onMethod", "()V");
assertNotSame(handler1, handler3, "Cache should be invalidated");
}
Focus: Correctness bugs that could cause silent failures
Fix MethodTracker copy bug (30 min)
MethodTracker.java:62Remove Thread.dumpStack() (15 min)
MethodInstrumentor.java:511Replace magic 0xFFFFFFFF (4 hours)
VariableMapper.java:136,139Fix test loadFile() O(n²) (30 min)
InstrumentorTestBase.java:406-418Outcome: All P0 issues resolved
Focus: High-impact optimizations
Cache handler bytecode (3 hours)
HandlerRepositoryImpl.java:43-77Cache compiled patterns (4 hours)
OnMethod.java (new field)BTraceProbeSupport.java:103,112 (use cached)Add CallGraph HashMap index (2 hours)
CallGraph.javaCache getActionPrefix (1 hour)
BTraceProbe.java (new method)BTraceProbeSupport.java (cache field)Outcome: 20-30% transformation speedup
Focus: Reduce GC pressure
InstrumentingMethodVisitor lazy copy (4 hours)
InstrumentingMethodVisitor.java:1670-1679VariableMapper pre-sizing (3 hours)
VariableMapper.java:19,54Outcome: 40-60% allocation reduction
Focus: Maintainability
Fix raw types in tests (2 hours)
Investigate test duplication (8 hours)
Outcome: Improved type safety and test maintainability
Focus: Edge case coverage
Outcome: >85% hot path coverage
| Metric | Baseline | Target | Measurement |
|---|---|---|---|
| Transformation Speed | 100% | 70-80% | Microbenchmark: 1000 classes |
| Memory Allocations | 100% | 40-60% | Allocation profiler |
| Handler Lookup | ~1ms | ~100ns | Cached invokedynamic linkage |
| Pattern Matching | 10-100μs | <1μs | Cached pattern per class |
| Metric | Baseline | Target |
|---|---|---|
| Magic Values | 1 (0xFFFFFFFF) | 0 |
| Debug Code | 1 (Thread.dumpStack) | 0 |
| Raw Types | 2+ | 0 |
| Test Coverage | ~70% | >85% |
| Data Corruption Bugs | 1 (MethodTracker) | 0 |
| File | Issues | Priority | Effort |
|---|---|---|---|
MethodTracker.java:62 | Copy-paste bug | P0 | 30m |
VariableMapper.java:136,139 | Magic values | P0 | 4h |
HandlerRepositoryImpl.java:43-77 | Handler caching | P1 | 3h |
BTraceProbeSupport.java:103,112 | Pattern compilation | P1 | 4h |
CallGraph.java:62-69,135-145,149-159 | Linear search | P1 | 2h |
InstrumentUtils.java:253 | String transformations | P1 | 1h |
InstrumentorTestBase.java:406-418 | O(n²) file loading | P1 | 30m |
InstrumentingMethodVisitor.java:1670-1679 | SavedState allocations | P2 | 4h |
MethodInstrumentor.java:511 | Debug code | P3 | 15m |
The btrace-instr module is generally well-architected but suffers from:
Fix P0+P1 issues first (estimated 3 days):
Address P2-P4 issues incrementally:
Most issues are optimization opportunities rather than fundamental design flaws. The instrumentation engine architecture is sound; it just needs tuning for production performance characteristics.
End of Document