Skip to content

Add a JVMTI agent for causal profiling of Java #129

Add a JVMTI agent for causal profiling of Java

Add a JVMTI agent for causal profiling of Java #129

Workflow file for this run

name: CI
on:
push:
branches: [master, wip_*]
pull_request:
branches: [master]
permissions:
contents: read
jobs:
build-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential \
cmake \
pkg-config \
libsqlite3-dev \
libbz2-dev
- name: Configure perf permissions
run: |
# Allow perf_event_open for profiling
echo 1 | sudo tee /proc/sys/kernel/perf_event_paranoid
- name: Build coz
run: |
mkdir build
cd build
cmake .. -DCMAKE_BUILD_TYPE=RelWithDebInfo
make -j$(nproc)
- name: Build benchmarks
run: |
cd build
cmake .. -DBUILD_BENCHMARKS=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo
make -j$(nproc) toy lock_test kmeans
- name: Verify build artifacts
run: |
test -f build/libcoz/libcoz.so
test -x build/benchmarks/toy/toy
test -x build/benchmarks/lock_test/lock_test
test -x build/benchmarks/kmeans/kmeans
echo "Build artifacts verified"
- name: Run unit tests
run: |
cd build
ctest --output-on-failure -j$(nproc)
rm -f profile.jsonl
echo "Unit tests passed"
- name: Run toy benchmark with coz
run: |
cd build
# Run coz with the toy benchmark for a short duration
timeout 30 ../coz run -o profile.jsonl --- ./benchmarks/toy/toy || true
if [ ! -f profile.jsonl ]; then
echo "ERROR: profile.jsonl was not created"
exit 1
fi
echo "Profile created successfully"
- name: Validate toy profile output
run: |
cd build
if [ ! -s profile.jsonl ]; then
echo "ERROR: profile.jsonl is empty"
exit 1
fi
echo "=== Profile Contents ==="
cat profile.jsonl
echo ""
# Check for expected profile format (should contain experiment lines)
EXPERIMENT_COUNT=$(grep -c 'experiment' profile.jsonl || echo 0)
echo "=== Profile Summary ==="
echo " Total lines: $(wc -l < profile.jsonl)"
echo " Experiments: $EXPERIMENT_COUNT"
if [ "$EXPERIMENT_COUNT" -eq 0 ]; then
echo "ERROR: No experiment data in profile"
exit 1
fi
# Validate profile references toy.cpp line 18 (the loop in function a())
if grep -q "toy.cpp:18" profile.jsonl; then
echo " Found experiments for toy.cpp:18 (loop in function a())"
else
echo "ERROR: toy.cpp:18 not found in profile - expected loop in a() to be profiled"
exit 1
fi
echo ""
echo "Profile validation PASSED"
- name: Validate toy optimization potential
run: |
cd build
python3 << 'VALIDATION_SCRIPT'
import sys
import json
experiments = []
current_exp = None
with open('profile.jsonl', 'r') as f:
for line in f:
line = line.strip()
if not line:
continue
obj = json.loads(line)
if obj.get('type') == 'experiment':
current_exp = obj
elif obj.get('type') == 'throughput-point' and current_exp:
current_exp['delta'] = float(obj['delta'])
experiments.append(current_exp)
current_exp = None
# Filter for toy.cpp:18 (the loop in a())
line18_exps = [e for e in experiments if 'toy.cpp:18' in e['selected']]
if not line18_exps:
print("ERROR: No experiments found for toy.cpp:18")
sys.exit(1)
print(f"Found {len(line18_exps)} experiments for toy.cpp:18")
# Group by speedup percentage and calculate average delta
speedup_deltas = {}
for e in line18_exps:
s = int(float(e['speedup']) * 100)
if s not in speedup_deltas:
speedup_deltas[s] = []
speedup_deltas[s].append(e['delta'])
print("\nSpeedup vs Progress Delta:")
for speedup in sorted(speedup_deltas.keys()):
deltas = speedup_deltas[speedup]
avg_delta = sum(deltas) / len(deltas)
print(f" {speedup}%: avg_delta={avg_delta:.2f} (n={len(deltas)})")
if len(speedup_deltas) < 3:
print(f"WARNING: Only {len(speedup_deltas)} speedup levels - need more data for accurate validation")
else:
print("\nOptimization potential analysis: PASSED")
print(" - toy.cpp:18 (loop in a()) correctly identified as optimization target")
print("\nProfile validation COMPLETE")
VALIDATION_SCRIPT
- name: Run lock_test benchmark with coz
run: |
cd build
timeout 60 ../coz run -o lock_test_profile.jsonl --- ./benchmarks/lock_test/lock_test || true
if [ ! -f lock_test_profile.jsonl ]; then
echo "ERROR: lock_test_profile.jsonl was not created"
exit 1
fi
echo "Lock test profile created successfully"
- name: Validate lock_test profile
run: |
cd build
if [ ! -s lock_test_profile.jsonl ]; then
echo "ERROR: lock_test_profile.jsonl is empty"
exit 1
fi
echo "=== Lock Test Profile Contents ==="
cat lock_test_profile.jsonl
echo ""
python3 << 'VALIDATION_SCRIPT'
import sys
import json
experiments = []
current_exp = None
with open('lock_test_profile.jsonl', 'r') as f:
for line in f:
line = line.strip()
if not line:
continue
obj = json.loads(line)
if obj.get('type') == 'experiment':
current_exp = obj
elif obj.get('type') == 'throughput-point' and current_exp:
current_exp['delta'] = float(obj['delta'])
experiments.append(current_exp)
current_exp = None
# Check for experiments on critical_work (line 12) and local_work (line 20)
line12_exps = [e for e in experiments if 'lock_test.cpp:12' in e['selected']]
line20_exps = [e for e in experiments if 'lock_test.cpp:20' in e['selected']]
print(f"Experiments for line 12 (critical_work): {len(line12_exps)}")
print(f"Experiments for line 20 (local_work): {len(line20_exps)}")
if not line12_exps:
print("ERROR: No experiments found for lock_test.cpp:12 (critical section)")
sys.exit(1)
# Check samples to verify critical_work dominates
with open('lock_test_profile.jsonl', 'r') as f:
for line in f:
obj = json.loads(line.strip())
if obj.get('type') == 'samples':
loc = obj['location']
count = obj['count']
print(f" Samples: {loc} = {count}")
print("\nLock contention benchmark validation: PASSED")
print(" - lock_test.cpp:12 (critical section) identified as profiling target")
VALIDATION_SCRIPT
- name: Upload profile artifacts
uses: actions/upload-artifact@v4
with:
name: coz-profiles
path: |
build/profile.jsonl
build/lock_test_profile.jsonl
retention-days: 7
build-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
# cmake is preinstalled on the runner; make sure pkg-config and sqlite
# (needed to configure the benchmark tree) are available. coreutils
# provides gtimeout, since macOS has no GNU `timeout`.
brew install pkg-config sqlite coreutils || true
- name: Build coz
run: |
mkdir build
cd build
cmake .. -DCMAKE_BUILD_TYPE=RelWithDebInfo
make -j$(sysctl -n hw.ncpu)
- name: Build benchmarks
run: |
cd build
cmake .. -DBUILD_BENCHMARKS=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo
make -j$(sysctl -n hw.ncpu) toy lock_test kmeans
- name: Verify build artifacts
run: |
# On macOS the injectable library keeps the .so suffix (see
# libcoz/CMakeLists.txt) so it can be used with DYLD_INSERT_LIBRARIES.
test -f build/libcoz/libcoz.so
test -x build/benchmarks/toy/toy
test -x build/benchmarks/lock_test/lock_test
test -x build/benchmarks/kmeans/kmeans
echo "Build artifacts verified"
- name: Run unit tests
run: |
cd build
# Includes test_run_kmeans, which profiles kmeans live via kperf and so
# also validates that macOS sampling actually collects samples.
ctest --output-on-failure -j$(sysctl -n hw.ncpu)
echo "Unit tests passed"
- name: Run toy benchmark with coz
run: |
cd build
gtimeout 30 ../coz run -o profile.jsonl --- ./benchmarks/toy/toy || true
if [ ! -s profile.jsonl ]; then
echo "ERROR: profile.jsonl was not created"
exit 1
fi
echo "Profile created:"
cat profile.jsonl
lang-rust:
name: Rust bindings (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v4
- name: Install dependencies (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake pkg-config
echo 1 | sudo tee /proc/sys/kernel/perf_event_paranoid
- name: Install dependencies (macOS)
if: runner.os == 'macOS'
run: brew install pkg-config coreutils || true
- name: Build coz
run: |
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build build -j3
test -f build/libcoz/libcoz.so || test -f build/libcoz/libcoz.dylib
- name: Check the crate on both platforms
working-directory: rust
run: |
cargo build --release --examples
cargo test --release
- name: Profile the Rust toy under coz
run: |
# `timeout` is GNU-only; coreutils provides gtimeout on macOS.
TIMEOUT=timeout
command -v gtimeout >/dev/null && TIMEOUT=gtimeout
# rust/examples/toy runs ~9e6 work units of 10k iterations each across
# two threads (~1-2 minutes on a 2-core runner); the timeout is a
# safety net since experiments are written to the profile as they run.
$TIMEOUT 300 ./coz run -o profile.jsonl --- ./rust/target/release/examples/toy || true
test -s profile.jsonl
- name: Validate the Rust profile
run: python3 .github/scripts/check_profile.py profile.jsonl toy.rs
lang-go:
name: Go bindings (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.26'
- name: Install dependencies (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake pkg-config
echo 1 | sudo tee /proc/sys/kernel/perf_event_paranoid
- name: Install dependencies (macOS)
if: runner.os == 'macOS'
run: brew install pkg-config coreutils || true
- name: Build coz
run: |
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build build -j3
test -f build/libcoz/libcoz.so || test -f build/libcoz/libcoz.dylib
- name: Vet, format and test
working-directory: go
run: |
test -z "$(gofmt -l .)" || { echo "gofmt found unformatted files:"; gofmt -l .; exit 1; }
go vet ./...
go test -race ./...
# The package must also build with cgo off, degrading to no-ops.
CGO_ENABLED=0 go build ./...
- name: Build the Go toy
working-directory: go
run: |
# coz cannot read Go's compressed DWARF (.zdebug_* / __zdebug_*).
go build -ldflags=-compressdwarf=false -o "$RUNNER_TEMP/gotoy" ./example
- name: Profile the Go toy under coz
run: |
TIMEOUT=timeout
command -v gtimeout >/dev/null && TIMEOUT=gtimeout
$TIMEOUT 180 ./coz run -o profile.jsonl --- "$RUNNER_TEMP/gotoy" || true
test -s profile.jsonl
- name: Validate the Go profile
run: python3 .github/scripts/check_profile.py profile.jsonl toy.go
lang-java:
name: Java agent (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v4
# Temurin, not GraalVM: the agent samples at safepoints, and the Graal JIT
# leaves hot counted loops without a safepoint poll, pushing the mean
# time-to-safepoint from ~100us to ~100ms. See java/README.md.
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- name: Install dependencies (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake pkg-config
- name: Install dependencies (macOS)
if: runner.os == 'macOS'
run: brew install pkg-config coreutils || true
- name: Build the agent and coz.jar
run: |
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_JAVA_AGENT=ON
cmake --build build --target cozjava coz-jar -j3
test -f build/java/libcozjava.so
test -f build/java/coz.jar
- name: Build the Java toy
working-directory: java
run: |
mkdir -p "$RUNNER_TEMP/classes"
# -g keeps the line tables the agent reads through GetLineNumberTable.
javac -g -cp ../build/java/coz.jar -d "$RUNNER_TEMP/classes" example/Toy.java
- name: Profile the Java toy under the agent
run: |
TIMEOUT=timeout
command -v gtimeout >/dev/null && TIMEOUT=gtimeout
$TIMEOUT 240 java \
-agentpath:"$PWD/build/java/libcozjava.so=output=$PWD/profile.jsonl,scope=example,verbose=1" \
-cp "$RUNNER_TEMP/classes:$PWD/build/java/coz.jar" example.Toy || true
test -s profile.jsonl
- name: Validate the Java profile
run: python3 .github/scripts/check_profile.py profile.jsonl Toy.java