Profiling another project with jvmlens

A task-oriented guide for adopting jvmlens in any other JVM project. The worked example is a project called builder — substitute your own jar, pid, and package prefix. "Profile builder from builder" just means running the jvmlens jar from inside the builder checkout against the builder process.

This mirrors the repository’s INTEGRATING.md, which is the portable copy-paste version you can hand to another repo. For the full flag reference see Usage.

What you need first

jvmlens is not yet on Maven Central, but every green build on main publishes a rolling latest pre-release, so you can grab the jars without building. Java 17+.

mkdir -p tools
curl -L -o tools/jvmlens.jar       https://github.com/alexmond/jvmlens/releases/download/latest/jvmlens.jar
curl -L -o tools/jvmlens-agent.jar https://github.com/alexmond/jvmlens/releases/download/latest/jvmlens-agent.jar
curl -L -o tools/jvmlens-jmh.jar   https://github.com/alexmond/jvmlens/releases/download/latest/jvmlens-jmh.jar

The URLs are stable (always the most recent green build); it’s a pre-release, so APIs may change until the first tagged release. Or build from source:

git clone https://github.com/alexmond/jvmlens && cd jvmlens
./mvnw -q clean package
#   jvmlens-cli/target/jvmlens.jar          <- CLI + MCP server (analyze/profile/watch/mcp)
#   jvmlens-agent/target/jvmlens-agent.jar  <- the in-process -javaagent jar

Nothing about jvmlens needs to be a dependency of your build — it consumes JFR, which the JDK already produces. jvmlens never calls an LLM and never ships your recording anywhere; every path below runs locally.

Pick your path

You have… You want… Path

A .jfr file already

A one-shot summary

Aanalyze

A class with a main (no JMH module)

A repeatable warm-loop benchmark

A2bench

A running JVM (local or one ssh/kubectl exec away)

An on-demand snapshot

Bprofile <pid>

A long-running service / CI job / container

Continuous summaries, zero attach

C-javaagent

A coding agent that should fetch profiles itself

Profiling exposed as tools

Dmcp

A k8s workload

A sidecar that doesn’t touch your app’s chart

E — Helm

Paths compose — run the agent © in k8s (E), or point an MCP client (D) at a remote host over ssh.

Path A — analyze a recording

java -jar tools/jvmlens.jar analyze builder-run.jfr            # markdown (default)
java -jar tools/jvmlens.jar analyze -r cpu builder-run.jfr     # focus: cpu|memory|locks|gc
java -jar tools/jvmlens.jar analyze -f prompt builder-run.jfr  # wrapped as an LLM task

Capture a recording around a builder workload:

java -XX:StartFlightRecording=duration=30s,filename=builder-run.jfr,settings=profile \
     -jar build/libs/builder.jar build ./big-project

Optimize → measure loop (diff, gate, JMH)

java -jar tools/jvmlens.jar analyze /tmp/run-after -a com.example.builder        # merge JMH -prof jfr forks
java -jar tools/jvmlens.jar analyze --baseline /tmp/run-before /tmp/run-after     # diff (absolute-anchored)
java -jar tools/jvmlens.jar analyze -b before.jfr after.jfr --assert "alloc-pct < 0, gc-pct < 10"  # CI gate
java -jar tools/jvmlens.jar analyze builder-run.jfr --hints --top-k 3             # fix directions + budget size
# inline from a JMH benchmark (put jvmlens-jmh.jar on the classpath):
java -cp benchmarks.jar:tools/jvmlens-jmh.jar org.openjdk.jmh.Main \
  -prof "org.alexmond.jvmlens.jmh.JvmlensProfiler:appPackage=com.example.builder"

Path A2 — bench a main (no JMH module)

Most apps don’t have a JMH module. bench is the no-JMH harness: point it at any main(String[]) and it runs a warmup→timed loop, captures a JFR over only the timed phase, and summarizes — no hand-rolled driver, no pre-recorded file:

java -jar tools/jvmlens.jar bench --main com.example.builder.BuildDriver \
     --cp build/libs/builder.jar -w 20 -i 200 -a com.example.builder --jfr /tmp/before.jfr

--cp loads the workload (it needn’t be on jvmlens’s classpath); --jfr keeps the recording so it can be a --baseline for the after-run. Write a tiny driver main that exercises the hot path once; bench is the loop.

Path B — profile a running process

profile <pid> attaches to a live JVM, records a timed window, and summarizes it — no pre-recorded file, no JMX, no start-up flags on the target.

PID=$(pgrep -f 'builder.jar' | head -1)
java -jar tools/jvmlens.jar profile "$PID"                  # 20s, markdown
java -jar tools/jvmlens.jar profile -d 30 -w 5 "$PID"      # skip 5s startup, record 30s
java -jar tools/jvmlens.jar profile -e async -d 30 "$PID"  # native frames (local pid only)

Remote builder — run jvmlens on the host; only the few-hundred-token summary travels back:

ssh build-host    'java -jar jvmlens.jar profile $(pgrep -f builder.jar) -f prompt'
kubectl exec builder-pod -- java -jar /tools/jvmlens.jar profile 1 -r cpu

Use -w/--warmup to measure the steady-state build, not classloading. A adequacy caveat means too few samples — record longer or under load.

Path C — always-on, in-process (agent)

The agent keeps a JFR ring buffer inside builder and writes a fresh summary every interval — no attach.

java -javaagent:tools/jvmlens-agent.jar=out=/var/log/builder-profile.md,interval=60 \
     -jar build/libs/builder.jar
Key Meaning Default

out

File the latest summary is written to (overwritten each interval)

jvmlens-summary.md

history

JSONL file the agent appends one sample to per interval (multi-day trends)

interval

Seconds between summaries

60

settings

JFR config: profile or default

profile

snapshot

Class#method to capture argument digests for (;-separate several)

For a multi-day watch, add history=<file.jsonl> (the agent appends one CPU+memory+wait sample per interval) and reduce the run later with jvmlens trend <file.jsonl> — a change-over-time digest with a hedged retention indicator, never a confident "leak".

In containers, set it via the JVM-standard env var (honoured by buildpacks and java -jar alike):

JAVA_TOOL_OPTIONS=-javaagent:/agent/jvmlens-agent.jar=out=/agent/builder.md,interval=60

Path D — MCP for coding agents

jvmlens mcp runs a Model Context Protocol server over stdio, exposing scoped, navigable tools (overviewhot_paths / hot_leaves / allocations / lock_contention) plus a live profile tool. It serves data only — never calls an LLM, so recordings stay on the host.

{ "mcpServers": {
  "jvmlens": { "command": "java", "args": ["-jar", "/abs/path/builder/tools/jvmlens.jar", "mcp"] }
} }

Remote builder, no extra ports — the client launches jvmlens over ssh:

{ "mcpServers": {
  "builder-prod": { "command": "ssh", "args": ["build-host", "java", "-jar", "jvmlens.jar", "mcp"] }
} }

Path E — Kubernetes sidecar

deploy/helm/jvmlens is a standalone chart: it runs your image with the agent attached as a separate release, so your app’s own chart is untouched.

scripts/deploy-agent.sh --release builder-profiled --namespace builder \
  --target-image my-registry/builder:1.0
if the profiled copy reuses your app’s envFrom it hits the same database — point it at a throwaway DB or run read-only. See deploy/helm/jvmlens/README.md.

Scoping: make your code lead

For a foreign project the single most useful knob — restrict attribution to your packages so framework frames don’t bury your code. Works on analyze and profile:

java -jar tools/jvmlens.jar analyze -a com.example.builder builder-run.jfr   # include-only
java -jar tools/jvmlens.jar analyze -x com.thirdparty       builder-run.jfr   # exclude more

Both flags are repeatable and comma-separable; the MCP tools accept the same as appPackages / exclude.

A drop-in recipe and CLAUDE.md snippet

A tiny tools/profile.sh your team and agents can run without remembering flags:

#!/usr/bin/env bash
set -euo pipefail
JVMLENS=${JVMLENS:-tools/jvmlens.jar}
PID=$(pgrep -f 'builder.jar' | head -1) || { echo "builder not running"; exit 1; }
exec java -jar "$JVMLENS" profile "$PID" -d "${DURATION:-20}" -w "${WARMUP:-3}" \
  -a "${APP_PKG:-com.example.builder}" -f "${FORMAT:-prompt}"

Paste into the target project’s CLAUDE.md so its coding agents know the workflow:

## Profiling (jvmlens)

- The jar lives at `tools/jvmlens.jar` (built from github.com/alexmond/jvmlens; Java 17+).
- One-shot, running JVM: `./tools/profile.sh`.
- From a .jfr file: `java -jar tools/jvmlens.jar analyze <file.jfr> -a com.example.builder`.
- Focus a concern: add `-r cpu|memory|locks|gc`.
- Always scope with `-a com.example.builder` so our code leads, not framework frames.
- A `⚠` caveat means too few samples — record longer or under load.