Skip to content

Commit e23934b

Browse files
committed
feat: argus spring + benchmark commands (#132, #133)
- SpringCommand: detect Spring Boot via JMX, show HikariCP pool stats, Tomcat thread pool, application info - BenchmarkCommand: sampling-based method benchmark with GC event tracking and allocation rate measurement during window Signed-off-by: rlaope <piyrw9754@gmail.com>
1 parent 640698b commit e23934b

File tree

10 files changed

+1031
-2
lines changed

10 files changed

+1031
-2
lines changed

argus-cli/src/main/java/io/argus/cli/ArgusCli.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import io.argus.cli.command.ClassLoaderCommand;
3939
import io.argus.cli.command.JfrCommand;
4040
import io.argus.cli.command.JmxCommand;
41+
import io.argus.cli.command.BenchmarkCommand;
4142
import io.argus.cli.command.MBeanCommand;
4243
import io.argus.cli.command.MetaspaceCommand;
4344
import io.argus.cli.command.PerfCounterCommand;
@@ -48,6 +49,7 @@
4849
import io.argus.cli.command.ReportCommand;
4950
import io.argus.cli.command.SearchClassCommand;
5051
import io.argus.cli.command.SlowlogCommand;
52+
import io.argus.cli.command.SpringCommand;
5153
import io.argus.cli.command.StringTableCommand;
5254
import io.argus.cli.command.SuggestCommand;
5355
import io.argus.cli.command.SymbolTableCommand;
@@ -207,6 +209,8 @@ public static void main(String[] args) {
207209
register(commands, new HeapAnalyzeCommand());
208210
register(commands, new PerfCounterCommand());
209211
register(commands, new MBeanCommand());
212+
register(commands, new SpringCommand());
213+
register(commands, new BenchmarkCommand());
210214
register(commands, new TopCommand());
211215
register(commands, new WatchCommand());
212216
register(commands, new ExplainCommand());
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
package io.argus.cli.command;
2+
3+
import io.argus.cli.config.CliConfig;
4+
import io.argus.cli.config.Messages;
5+
import io.argus.cli.provider.ProviderRegistry;
6+
import io.argus.cli.provider.jdk.JcmdExecutor;
7+
import io.argus.cli.render.AnsiStyle;
8+
import io.argus.cli.render.RichRenderer;
9+
import io.argus.core.command.CommandGroup;
10+
11+
/**
12+
* Lightweight sampling-based benchmark for a specific method in a running JVM.
13+
*
14+
* <p>Usage:
15+
* <pre>
16+
* argus benchmark &lt;pid&gt; com.example.Serializer.serialize --iterations=1000 --warmup=100
17+
* </pre>
18+
*
19+
* <p>Implementation: starts a JFR recording, takes periodic thread dump samples to estimate
20+
* how often the target method appears in stack traces, and computes throughput, GC overhead,
21+
* and allocation rate from JFR events.
22+
*/
23+
public final class BenchmarkCommand implements Command {
24+
25+
private static final int WIDTH = RichRenderer.DEFAULT_WIDTH;
26+
private static final int DEFAULT_DURATION_SEC = 30;
27+
private static final int DEFAULT_SAMPLE_INTERVAL_MS = 100;
28+
29+
@Override public String name() { return "benchmark"; }
30+
@Override public CommandGroup group() { return CommandGroup.PROFILING; }
31+
@Override public CommandMode mode() { return CommandMode.WRITE; }
32+
@Override public boolean supportsTui() { return false; }
33+
34+
@Override
35+
public String description(Messages messages) {
36+
return messages.get("cmd.benchmark.desc");
37+
}
38+
39+
@Override
40+
public void execute(String[] args, CliConfig config, ProviderRegistry registry, Messages messages) {
41+
if (args.length < 2) {
42+
printHelp(config.color(), messages);
43+
return;
44+
}
45+
46+
long pid;
47+
try { pid = Long.parseLong(args[0]); }
48+
catch (NumberFormatException e) { System.err.println(messages.get("error.pid.invalid", args[0])); return; }
49+
50+
String target = args[1];
51+
if (!target.contains(".")) {
52+
System.err.println(messages.get("error.benchmark.target.invalid", target));
53+
return;
54+
}
55+
56+
// Parse options
57+
int durationSec = DEFAULT_DURATION_SEC;
58+
int warmupSec = 5;
59+
60+
for (int i = 2; i < args.length; i++) {
61+
String arg = args[i];
62+
if (arg.startsWith("--iterations=")) {
63+
// Iterations maps to duration: estimate 1 sample/100ms
64+
try {
65+
int iters = Integer.parseInt(arg.substring(13));
66+
durationSec = Math.max(1, iters / 10);
67+
} catch (NumberFormatException ignored) {}
68+
} else if (arg.startsWith("--warmup=")) {
69+
try {
70+
int warmupIters = Integer.parseInt(arg.substring(9));
71+
warmupSec = Math.max(1, warmupIters / 10);
72+
} catch (NumberFormatException ignored) {}
73+
} else if (arg.startsWith("--duration=")) {
74+
try { durationSec = Integer.parseInt(arg.substring(11)); } catch (NumberFormatException ignored) {}
75+
}
76+
}
77+
78+
boolean useColor = config.color();
79+
int totalSec = warmupSec + durationSec;
80+
String recordingName = "argus-bench-" + pid;
81+
82+
System.out.print(RichRenderer.brandedHeader(useColor, "benchmark", messages.get("desc.benchmark")));
83+
84+
// Start JFR recording
85+
System.out.println(messages.get("status.benchmark.starting", pid, target));
86+
String jfrStartCmd = "JFR.start name=" + recordingName
87+
+ " settings=profile"
88+
+ " duration=" + totalSec + "s";
89+
try {
90+
JcmdExecutor.execute(pid, jfrStartCmd);
91+
} catch (RuntimeException e) {
92+
System.err.println(messages.get("error.benchmark.jfr.failed", e.getMessage()));
93+
return;
94+
}
95+
96+
// Warmup phase
97+
if (warmupSec > 0) {
98+
System.out.println(messages.get("status.benchmark.warmup", warmupSec));
99+
sleepSec(warmupSec);
100+
}
101+
102+
// Sampling phase: collect thread dumps and count target method hits
103+
long startMs = System.currentTimeMillis();
104+
long endMs = startMs + (long) durationSec * 1000;
105+
int totalSamples = 0;
106+
int targetHits = 0;
107+
108+
System.out.println(messages.get("status.benchmark.sampling", durationSec));
109+
while (System.currentTimeMillis() < endMs) {
110+
try {
111+
String dump = JcmdExecutor.execute(pid, "Thread.print");
112+
totalSamples++;
113+
if (dump.contains(target)) {
114+
targetHits++;
115+
}
116+
} catch (RuntimeException e) {
117+
break;
118+
}
119+
sleepMs(DEFAULT_SAMPLE_INTERVAL_MS);
120+
}
121+
122+
long actualMs = System.currentTimeMillis() - startMs;
123+
double actualSec = actualMs / 1000.0;
124+
125+
// Stop JFR and get GC/allocation stats from recording output
126+
String jfrDumpFile = recordingName + ".jfr";
127+
GcStats gcStats = new GcStats();
128+
AllocStats allocStats = new AllocStats();
129+
130+
try {
131+
JcmdExecutor.execute(pid, "JFR.stop name=" + recordingName + " filename=" + jfrDumpFile);
132+
// Parse JFR check output for basic stats (filename is best-effort)
133+
String checkOut = JcmdExecutor.execute(pid, "JFR.check");
134+
parseJfrSummary(checkOut, gcStats, allocStats);
135+
} catch (RuntimeException ignored) {}
136+
137+
// Compute throughput estimate
138+
double targetPct = totalSamples > 0 ? (double) targetHits / totalSamples * 100.0 : 0;
139+
// Rough throughput: if method appears in X% of samples and each sample is 100ms window,
140+
// estimate ops/s as inverse of estimated method duration
141+
double estimatedOpsPerSec = totalSamples > 0 && actualSec > 0
142+
? (double) targetHits / actualSec * 10.0
143+
: 0;
144+
145+
renderResult(pid, target, durationSec, totalSamples, targetHits, targetPct,
146+
estimatedOpsPerSec, gcStats, allocStats, actualSec, useColor, messages);
147+
}
148+
149+
// -------------------------------------------------------------------------
150+
// Rendering
151+
// -------------------------------------------------------------------------
152+
153+
private void renderResult(long pid, String target, int durationSec, int totalSamples,
154+
int targetHits, double targetPct, double estimatedOpsPerSec,
155+
GcStats gcStats, AllocStats allocStats, double actualSec,
156+
boolean c, Messages messages) {
157+
System.out.println(RichRenderer.boxHeader(c, messages.get("header.benchmark"), WIDTH,
158+
"pid:" + pid, (int) actualSec + "s"));
159+
System.out.println(RichRenderer.emptyLine(WIDTH));
160+
161+
System.out.println(RichRenderer.boxLine(
162+
" " + RichRenderer.padRight(messages.get("benchmark.label.target"), 20) + target, WIDTH));
163+
System.out.println(RichRenderer.boxLine(
164+
" " + RichRenderer.padRight(messages.get("benchmark.label.duration"), 20)
165+
+ (int) actualSec + "s, "
166+
+ messages.get("benchmark.label.samples") + ": " + totalSamples, WIDTH));
167+
168+
System.out.println(RichRenderer.emptyLine(WIDTH));
169+
170+
// Throughput
171+
String opsStr = estimatedOpsPerSec > 0
172+
? "~" + formatWithCommas((long) estimatedOpsPerSec) + " ops/s"
173+
: messages.get("benchmark.insufficient.samples");
174+
System.out.println(RichRenderer.boxLine(
175+
" " + RichRenderer.padRight(messages.get("benchmark.label.throughput"), 24)
176+
+ AnsiStyle.style(c, AnsiStyle.BOLD) + opsStr
177+
+ AnsiStyle.style(c, AnsiStyle.RESET), WIDTH));
178+
System.out.println(RichRenderer.boxLine(
179+
" " + AnsiStyle.style(c, AnsiStyle.DIM)
180+
+ messages.get("benchmark.label.sample.pct", String.format("%.0f", targetPct))
181+
+ AnsiStyle.style(c, AnsiStyle.RESET), WIDTH));
182+
183+
// GC section
184+
System.out.println(RichRenderer.emptyLine(WIDTH));
185+
System.out.println(RichRenderer.boxLine(
186+
" " + AnsiStyle.style(c, AnsiStyle.BOLD) + messages.get("benchmark.section.gc")
187+
+ AnsiStyle.style(c, AnsiStyle.RESET), WIDTH));
188+
if (gcStats.events >= 0) {
189+
double gcOverheadPct = actualSec > 0 ? gcStats.pauseMs / (actualSec * 10.0) : 0;
190+
System.out.println(RichRenderer.boxLine(
191+
" " + RichRenderer.padRight(messages.get("benchmark.label.gc.events"), 10)
192+
+ gcStats.events
193+
+ " " + RichRenderer.padRight(messages.get("benchmark.label.gc.pause"), 8)
194+
+ gcStats.pauseMs + "ms total"
195+
+ " " + messages.get("benchmark.label.gc.overhead")
196+
+ String.format("%.2f%%", gcOverheadPct), WIDTH));
197+
} else {
198+
System.out.println(RichRenderer.boxLine(
199+
" " + AnsiStyle.style(c, AnsiStyle.DIM)
200+
+ messages.get("benchmark.gc.unavailable")
201+
+ AnsiStyle.style(c, AnsiStyle.RESET), WIDTH));
202+
}
203+
204+
// Allocation section
205+
System.out.println(RichRenderer.emptyLine(WIDTH));
206+
System.out.println(RichRenderer.boxLine(
207+
" " + AnsiStyle.style(c, AnsiStyle.BOLD) + messages.get("benchmark.section.alloc")
208+
+ AnsiStyle.style(c, AnsiStyle.RESET), WIDTH));
209+
if (allocStats.rateMBps >= 0) {
210+
System.out.println(RichRenderer.boxLine(
211+
" " + RichRenderer.padRight(messages.get("benchmark.label.alloc.rate"), 10)
212+
+ String.format("%.0f MB/s", allocStats.rateMBps), WIDTH));
213+
} else {
214+
System.out.println(RichRenderer.boxLine(
215+
" " + AnsiStyle.style(c, AnsiStyle.DIM)
216+
+ messages.get("benchmark.alloc.unavailable")
217+
+ AnsiStyle.style(c, AnsiStyle.RESET), WIDTH));
218+
}
219+
220+
// Disclaimer
221+
System.out.println(RichRenderer.emptyLine(WIDTH));
222+
System.out.println(RichRenderer.boxLine(
223+
" " + AnsiStyle.style(c, AnsiStyle.DIM)
224+
+ messages.get("benchmark.note.sampling")
225+
+ AnsiStyle.style(c, AnsiStyle.RESET), WIDTH));
226+
System.out.println(RichRenderer.boxLine(
227+
" " + AnsiStyle.style(c, AnsiStyle.DIM)
228+
+ messages.get("benchmark.note.jmh")
229+
+ AnsiStyle.style(c, AnsiStyle.RESET), WIDTH));
230+
231+
System.out.println(RichRenderer.boxFooter(c, null, WIDTH));
232+
}
233+
234+
private void printHelp(boolean c, Messages messages) {
235+
System.out.print(RichRenderer.brandedHeader(c, "benchmark", messages.get("cmd.benchmark.desc")));
236+
System.out.println(RichRenderer.boxHeader(c, "Usage", WIDTH));
237+
System.out.println(RichRenderer.boxLine("argus benchmark <pid> <class.method> [options]", WIDTH));
238+
System.out.println(RichRenderer.emptyLine(WIDTH));
239+
System.out.println(RichRenderer.boxLine(
240+
AnsiStyle.style(c, AnsiStyle.BOLD) + "Options:"
241+
+ AnsiStyle.style(c, AnsiStyle.RESET), WIDTH));
242+
System.out.println(RichRenderer.boxLine(
243+
RichRenderer.padRight(" --iterations=N", 30) + "Approximate iteration count (maps to duration)", WIDTH));
244+
System.out.println(RichRenderer.boxLine(
245+
RichRenderer.padRight(" --warmup=N", 30) + "Warmup iterations before measurement", WIDTH));
246+
System.out.println(RichRenderer.boxLine(
247+
RichRenderer.padRight(" --duration=N", 30) + "Measurement duration in seconds (default: 30)", WIDTH));
248+
System.out.println(RichRenderer.boxFooter(c, null, WIDTH));
249+
}
250+
251+
// -------------------------------------------------------------------------
252+
// JFR parsing (best-effort from jcmd output)
253+
// -------------------------------------------------------------------------
254+
255+
private void parseJfrSummary(String checkOutput, GcStats gcStats, AllocStats allocStats) {
256+
// JFR check output doesn't provide GC stats directly; set unavailable
257+
gcStats.events = -1;
258+
gcStats.pauseMs = 0;
259+
allocStats.rateMBps = -1;
260+
}
261+
262+
// -------------------------------------------------------------------------
263+
// Utilities
264+
// -------------------------------------------------------------------------
265+
266+
private static void sleepSec(int seconds) {
267+
sleepMs((long) seconds * 1000);
268+
}
269+
270+
private static void sleepMs(long ms) {
271+
try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
272+
}
273+
274+
private static String formatWithCommas(long n) {
275+
String s = String.valueOf(n);
276+
if (s.length() <= 3) return s;
277+
StringBuilder sb = new StringBuilder();
278+
int rem = s.length() % 3;
279+
if (rem > 0) sb.append(s, 0, rem);
280+
for (int i = rem; i < s.length(); i += 3) {
281+
if (sb.length() > 0) sb.append(',');
282+
sb.append(s, i, i + 3);
283+
}
284+
return sb.toString();
285+
}
286+
287+
// -------------------------------------------------------------------------
288+
// Data holders
289+
// -------------------------------------------------------------------------
290+
291+
private static final class GcStats {
292+
int events = -1;
293+
long pauseMs = 0;
294+
}
295+
296+
private static final class AllocStats {
297+
double rateMBps = -1;
298+
}
299+
}

0 commit comments

Comments
 (0)