Skip to content
Open
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
12 changes: 12 additions & 0 deletions .github/workflows/continuous.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,18 @@ jobs:
outputs:
cache-key: ${{ steps.cache-key.outputs.cache-key }}

dependency-review:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Dependency Review
uses: actions/dependency-review-action@v4
with:
fail-on-severity: high
comment-summary-in-pr: always

test:
needs: build
runs-on: ubuntu-latest
Expand Down
186 changes: 186 additions & 0 deletions PROJECT_REVIEW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# BTrace Project Review: Top 10 Most Impactful Improvements

## Context

BTrace is a production-safe dynamic tracing tool for Java (v3.0.0-SNAPSHOT) with ~44K lines of Java across 24+ modules. After a thorough review of code quality, architecture, build system, CI/CD, documentation, and feature gaps, the following 10 improvements are ranked by their impact on reliability, maintainability, and user experience.

---

## 1. Fix Swallowed Exceptions Across Critical Modules (Fix - High Impact)

**Problem:** 30+ catch blocks silently discard exceptions with no logging, making production debugging extremely difficult. Found in btrace-compiler (`CompilerHelper.java`), btrace-client (`Client.java`, `JpsUtils.java`), btrace-extension (`ExtensionBridgeImpl.java`, `ExtensionInspector.java`), and btrace-agent.

**Solution:** Add SLF4J logging to all swallowed exceptions. For each, determine whether to log at WARN (expected/recoverable) or ERROR (unexpected) level. Where exceptions indicate genuine failures, propagate or rethrow.

**Key files:**
- `btrace-compiler/src/main/java/org/openjdk/btrace/compiler/CompilerHelper.java` (lines 150, 270, 276, 281, 297, 303)
- `btrace-client/src/main/java/org/openjdk/btrace/client/Client.java` (lines 218, 224, 239, 383)
- `btrace-client/src/main/java/org/openjdk/btrace/client/JpsUtils.java` (lines 20, 74, 79)
- `btrace-extension/src/main/java/org/openjdk/btrace/extension/impl/ExtensionBridgeImpl.java` (lines 114, 163, 176)
- `btrace-ext-cli/src/main/java/org/openjdk/btrace/extcli/ExtensionInspector.java` (lines 111, 114, 122, 159, 173, 185, 203, 237)
- `btrace-ext-cli/src/main/java/org/openjdk/btrace/extcli/Installer.java` (lines 94, 153)
- `btrace-compiler/src/main/java/org/openjdk/btrace/compiler/VerifierVisitor.java` (lines 589, 593, 627, 843, 852)

**Effort:** Medium

---

## 2. Fix Resource Leaks with try-with-resources (Fix - High Impact)

**Problem:** Multiple files create I/O resources (FileOutputStream, FileReader, BufferedReader) without try-with-resources, risking memory/handle leaks in production.

**Solution:** Convert all identified resource creations to try-with-resources blocks.

**Key files:**
- `btrace-runtime/src/main/java/org/openjdk/btrace/runtime/DOTWriter.java` (line 29 - FileOutputStream)
- `btrace-compiler/src/main/java/org/openjdk/btrace/compiler/PCPP.java` (lines 114, 787 - FileReader/BufferedReader)
- `btrace-compiler/src/main/java/org/openjdk/btrace/compiler/CompilerHelper.java` (lines 287-307 - dump() method)
- `btrace-runtime/src/main/java/org/openjdk/btrace/runtime/BTraceRuntimeImplBase.java` (lines 63-66 - FileInputStream)

**Effort:** Small

---

## 3. Replace Production Assertions with Proper Null Checks (Fix - High Impact)

**Problem:** Java `assert` statements are disabled by default in production JVMs (`-ea` flag required). Critical null checks rely on assertions, meaning they silently pass in production, leading to NullPointerExceptions downstream.

**Solution:** Replace `assert x != null` with explicit null checks that throw `NullPointerException` or `IllegalStateException` with descriptive messages.

**Key files:**
- `btrace-runtime/src/main/java/org/openjdk/btrace/runtime/BTraceRuntimeAccessImpl.java` - `assert rtw != null`
- `btrace-instr/src/main/java/org/openjdk/btrace/instr/InstrumentingMethodVisitor.java` - `assert opType != null`
- `btrace-runtime/src/main/java15/org/openjdk/btrace/runtime/Indy.java` - `assert repository != null`
- `btrace-client/src/main/java/org/openjdk/btrace/client/Client.java` - `assert protocol != null`

**Effort:** Small

---

## 4. Unify Logging: Replace System.out/err with SLF4J (Improvement - High Impact)

**Problem:** 57+ files use `System.out.println`, `System.err.println`, or `printStackTrace()` instead of SLF4J. This makes log management, filtering, and routing impossible in production deployments.

**Solution:** Systematically replace all `System.out/err` and `printStackTrace()` calls with appropriate SLF4J logger calls. Add SLF4J Logger field to classes that lack one.

**Key files:** 57+ files across all modules. Priority targets:
- `btrace-compiler/src/main/java/org/openjdk/btrace/compiler/PCPP.java`
- `btrace-compiler/src/main/java/org/openjdk/btrace/compiler/CompilerHelper.java`
- `btrace-client/src/main/java/org/openjdk/btrace/client/Client.java`
- `btrace-agent/src/main/java/org/openjdk/btrace/agent/Main.java`

**Effort:** Large

---

## 5. Add Dependency Vulnerability Scanning to CI (Improvement - Medium Impact)

**Problem:** The CI pipeline (`continuous.yml`) runs tests across multiple JDK versions but has no dependency vulnerability scanning. There's a CodeQL workflow but no OWASP/Dependabot-style dependency check.

**Solution:** Add a GitHub Actions step using `dependency-review-action` for PRs and/or OWASP dependency-check Gradle plugin for builds. Optionally add SBOM generation.

**Key files:**
- `.github/workflows/continuous.yml` - Add dependency scanning job
- `build.gradle` or `common.gradle` - Add OWASP dependency-check plugin
- Optionally add `cyclonedx-gradle-plugin` for SBOM generation

**Effort:** Small

---

## 6. Replace Generic RuntimeException Wrapping with Specific Exceptions (Improvement - Medium Impact)

**Problem:** 32+ locations wrap checked exceptions in `new RuntimeException(e)`, losing exception type information and making it impossible for callers to handle specific failure modes. Some even use `new RuntimeException(e.toString())` which loses the stack trace entirely.

**Solution:** Create a small set of BTrace-specific unchecked exceptions (e.g., `BTraceCompilationException`, `BTraceInstrumentationException`, `BTraceAgentException`) and use them instead of generic RuntimeException. Where the original exception type matters, preserve it.

**Key files:**
- `btrace-compiler/src/main/java/org/openjdk/btrace/compiler/CompilerClassWriter.java:75` - `new RuntimeException(e.toString())` loses stack trace
- `btrace-compiler/src/main/java/org/openjdk/btrace/compiler/Compiler.java:237`
- `btrace-runtime/src/main/java/org/openjdk/btrace/runtime/BTraceMBean.java` (lines 107, 145, 173)
- `btrace-runtime/src/main/java/org/openjdk/btrace/runtime/BTraceRuntimeImplBase.java` (6 locations)
- `btrace-boot/src/main/java/org/openjdk/btrace/boot/Loader.java` (lines 114, 151, 159)

**Effort:** Medium

---

## 7. Complete FIXME/HACK Implementations (Fix - Medium Impact)

**Problem:** Several production code paths have incomplete implementations marked with FIXME/HACK comments:
- `BTraceMBean.java:303`: "FIXME: This is highly incomplete, revisit..." - MBean type conversion is incomplete
- `PCPP.java:298`: `!!HACK!!` - Word token handling workaround
- `PCPP.java:464`: "FIXME: should identify some of these, like (-1), as constants"

**Solution:** Complete the type-to-OpenType conversion in BTraceMBean (add support for all JMX standard types). Address the PCPP preprocessor hacks with proper implementations.

**Key files:**
- `btrace-runtime/src/main/java/org/openjdk/btrace/runtime/BTraceMBean.java` (line 303)
- `btrace-compiler/src/main/java/org/openjdk/btrace/compiler/PCPP.java` (lines 298, 464)

**Effort:** Medium

---

## 8. Update Documentation for v3.0.0 (Improvement - Medium Impact)

**Problem:** Tutorial documentation references BTrace 2.3.0, but the project is at 3.0.0-SNAPSHOT. Users following the getting-started guide may encounter confusion with outdated version references and missing v3 migration guidance.

**Solution:** Update all documentation to reference v3.0.0. Add a migration guide from v2.x to v3.x. Ensure sample code and CLI examples match the current API.

**Key files:**
- `docs/BTraceTutorial.md` - Update version references
- `docs/GettingStarted.md` - Update installation instructions
- `docs/QuickReference.md` - Verify accuracy
- `docs/FAQ.md` - Add v3 migration section
- `README.md` - Ensure consistency

**Effort:** Medium

---

## 9. Add OpenTelemetry Extension for Modern Observability (Feature - High Impact)

**Problem:** BTrace has extensions for StatsD and HdrHistogram metrics, but lacks integration with OpenTelemetry, the emerging industry standard for observability. Users must manually bridge BTrace data to their observability platforms.

**Solution:** Create a `btrace-otel` extension module under `btrace-extensions/` that exports BTrace trace data as OpenTelemetry spans/metrics. This would allow BTrace to integrate with Jaeger, Prometheus, Grafana, Datadog, etc. out of the box.

**Key files:**
- New: `btrace-extensions/btrace-otel/` module
- Reference: `btrace-extensions/btrace-statsd/` (existing extension pattern to follow)
- Reference: `btrace-extension/src/main/java/org/openjdk/btrace/extension/` (extension SPI)
- `settings.gradle` - Register new module

**Effort:** Large

---

## 10. Add GraalVM Native Image Compatibility (Feature - Medium Impact)

**Problem:** GraalVM native image is increasingly popular for Java applications. BTrace's reliance on dynamic bytecode manipulation (ASM) and runtime attachment may not work with native images. There's no documentation or fallback strategy for GraalVM users.

**Solution:** Document GraalVM limitations clearly. Investigate and implement a compile-time instrumentation mode that weaves BTrace scripts at build time (pre-AOT), producing instrumented bytecode compatible with native image. This could leverage the existing btrace-gradle-plugin.

**Key files:**
- `btrace-gradle-plugin/` - Add compile-time weaving task
- `btrace-instr/` - Reuse existing instrumentation for build-time mode
- New: `docs/GraalVMSupport.md` - Document limitations and workarounds

**Effort:** Large

---

## Summary Table

| # | Title | Category | Impact | Effort |
|---|-------|----------|--------|--------|
| 1 | Fix swallowed exceptions (30+ locations) | Fix | High | Medium |
| 2 | Fix resource leaks with try-with-resources | Fix | High | Small |
| 3 | Replace production assertions with null checks | Fix | High | Small |
| 4 | Unify logging to SLF4J (57+ files) | Improvement | High | Large |
| 5 | Add dependency vulnerability scanning to CI | Improvement | Medium | Small |
| 6 | Replace RuntimeException with specific exceptions | Improvement | Medium | Medium |
| 7 | Complete FIXME/HACK implementations | Fix | Medium | Medium |
| 8 | Update documentation for v3.0.0 | Improvement | Medium | Medium |
| 9 | Add OpenTelemetry extension | Feature | High | Large |
| 10 | Add GraalVM native image compatibility | Feature | Medium | Large |
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ private RemoteClient(
this.settings.from(ctx.getSettings());
Class<?> btraceClazz = loadClass(cmd);
if (btraceClazz == null) {
throw new RuntimeException("can not load BTrace class");
throw new IllegalStateException("Cannot load BTrace class from received bytecode");
}

initClient();
Expand Down
25 changes: 14 additions & 11 deletions btrace-client/src/main/java/org/openjdk/btrace/client/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -215,14 +215,15 @@ private static boolean isPortAvailable(int port) {
Socket clSocket = null;
try {
clSocket = new Socket("127.0.0.1", port);
} catch (IOException ignored) {
// ignore
} catch (IOException e) {
// Connection refused means port is available
log.debug("Port {} is available (connection refused)", port);
}
if (clSocket != null) {
try {
clSocket.close();
} catch (IOException ignored) {
// ignore
} catch (IOException e) {
log.debug("Failed to close probe socket on port {}", port, e);
}
return false;
}
Expand All @@ -236,7 +237,8 @@ private boolean isAgentAvailableAfterLoadFailure(VirtualMachine vm) {
if (serverPort != null) {
return Integer.parseInt(serverPort) == port;
}
} catch (Exception ignore) {
} catch (Exception e) {
log.debug("Failed to check VM system properties", e);
// fall through to port probe
}
return !isPortAvailable(port);
Expand Down Expand Up @@ -380,7 +382,8 @@ private void closeSocketQuietly(Socket socket) {
}
try {
socket.close();
} catch (IOException ignore) {
} catch (IOException e) {
log.debug("Failed to close socket", e);
}
}

Expand Down Expand Up @@ -748,12 +751,10 @@ public void attach(String pid, String agentPath, String sysCp, String bootCp) th
log.debug("loaded {}", agentPath);
}
} catch (RuntimeException | IOException re) {
System.err.println("[DEBUG] IOException/RuntimeException during attach:");
re.printStackTrace();
log.debug("IOException/RuntimeException during attach", re);
throw re;
} catch (Exception exp) {
System.err.println("[DEBUG] Exception during attach:");
exp.printStackTrace();
log.debug("Exception during attach", exp);
throw new IOException("Failed to attach to PID " + pid, exp);
}
}
Expand Down Expand Up @@ -1203,7 +1204,9 @@ private void send(Command cmd) throws IOException {
}

private void commandLoop(CommandListener listener) throws IOException {
assert protocol != null : "null protocol?";
if (protocol == null) {
throw new IllegalStateException("Command loop started before protocol was established");
}
AtomicBoolean exited = new AtomicBoolean(false);
while (true) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,14 @@ static boolean hasBTraceServer(int pid) {
try {
vm = VirtualMachine.attach(String.valueOf(pid));
result = vm.getSystemProperties().containsKey("btrace.port");
} catch (Throwable ignored) {
} catch (Throwable e) {
log.debug("Failed to check BTrace server on pid {}", pid, e);
} finally {
if (vm != null) {
try {
vm.detach();
} catch (IOException ignored) {
} catch (IOException e) {
log.debug("Failed to detach from VM {}", pid, e);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ public Map<String, byte[]> compile(
preprocessedCompUnits.add(MemoryJavaFileManager.preprocessedFileObject(jfo, includeDirs));
}
} catch (IOException ioExp) {
throw new RuntimeException(ioExp);
throw new RuntimeException("Failed to preprocess BTrace script", ioExp);
}
return compile(preprocessedCompUnits, err, sourcePath, classPath);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ protected String getCommonSuperClass(String type1, String type2) {
c = cl.loadClass(type1.replace('/', '.'));
d = cl.loadClass(type2.replace('/', '.'));
} catch (Exception e) {
throw new RuntimeException(e.toString());
throw new RuntimeException("Failed to load classes for common superclass resolution: " + type1 + ", " + type2, e);
}
if (c.isAssignableFrom(d)) {
return type1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,8 @@ private void addExtensionJars(Path directory, List<String> jars) {
for (Path jar : stream) {
jars.add(jar.toAbsolutePath().toString());
}
} catch (IOException ignored) {
// Directory not accessible, skip silently
} catch (IOException e) {
log.debug("Unable to scan extension directory: {}", directory, e);
}
}

Expand Down Expand Up @@ -267,42 +267,38 @@ Map<String, byte[]> compile(
if (maskedClassLoader instanceof AutoCloseable) {
try {
((AutoCloseable) maskedClassLoader).close();
} catch (Exception ignored) {
} catch (Exception e) {
log.debug("Failed to close masked classloader", e);
}
}
if (effectiveManager != manager) {
try {
effectiveManager.close();
} catch (IOException ignored) {
} catch (IOException e) {
log.debug("Failed to close effective file manager", e);
}
}
try {
manager.close();
} catch (IOException ignored) {
} catch (IOException e) {
log.debug("Failed to close file manager", e);
}
}
return result;
}

private void dump(String name, byte[] code) {
OutputStream os = null;
name = name.replace(".", "_") + ".class";
File f = new File(System.getProperty("java.io.tmpdir"), name);
try {
name = name.replace(".", "_") + ".class";
File f = new File(System.getProperty("java.io.tmpdir"), name);
if (!f.exists()) {
f.getParentFile().createNewFile();
}
os = new FileOutputStream(f);
os.write(code);
} catch (IOException ignored) {

} finally {
if (os != null) {
try {
os.close();
} catch (IOException ignored) {
}
try (OutputStream os = new FileOutputStream(f)) {
os.write(code);
}
} catch (IOException e) {
log.debug("Failed to dump class file: {}", f, e);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ static JavaFileObject makeStringSource(String name, String code, List<String> in
try {
pcpp.run(new StringReader(code), name);
} catch (IOException exp) {
throw new RuntimeException(exp);
throw new RuntimeException("Preprocessing failed for " + name, exp);
}
return new StringInputBuffer(name, out.toString());
} else {
Expand Down
Loading
Loading