Skip to content

Commit d30561f

Browse files
committed
feat: advanced GC log analysis — timeline heatmap, diff, rate analysis, leak detection (#109, #110, #111, #112)
Add four new GC log analysis features to strengthen Argus as a GCEasy alternative: - Pause timeline heatmap: ASCII visualization of GC pauses over time with color-coded severity and Full GC markers - GC log diff (gclogdiff): compare two GC log files with color-coded metric deltas, cause shift tracking, and CI-friendly exit codes for regression detection - Allocation/promotion rate analysis: compute allocation rate, promotion rate, reclaim efficiency with sparkline timelines (replaces GCEasy paid feature) - Memory leak detector: linear regression on heap-after-GC trend with R² confidence, OOM time estimation, and staircase pattern detection Also includes: i18n (en/ko/ja/zh), shell completions (bash/zsh/ fish/powershell), JSON output extensions, and 23 unit tests. Signed-off-by: rlaope <piyrw9754@gmail.com>
1 parent 51521d5 commit d30561f

20 files changed

Lines changed: 1441 additions & 13 deletions

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import io.argus.cli.command.WatchCommand;
2020
import io.argus.cli.command.GcCauseCommand;
2121
import io.argus.cli.command.GcLogCommand;
22+
import io.argus.cli.command.GcLogDiffCommand;
2223
import io.argus.cli.command.GcRunCommand;
2324
import io.argus.cli.command.GcCommand;
2425
import io.argus.cli.command.GcNewCommand;
@@ -170,6 +171,7 @@ public static void main(String[] args) {
170171
register(commands, new CompareCommand());
171172
register(commands, new SlowlogCommand());
172173
register(commands, new GcLogCommand());
174+
register(commands, new GcLogDiffCommand());
173175
register(commands, new FlameCommand());
174176
register(commands, new SuggestCommand());
175177
register(commands, new InfoCommand());

argus-cli/src/main/java/io/argus/cli/command/GcLogCommand.java

Lines changed: 153 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public void execute(String[] args, CliConfig config, ProviderRegistry registry,
9292
PrintStream original = System.out;
9393
ByteArrayOutputStream capture = new ByteArrayOutputStream();
9494
System.setOut(new PrintStream(capture));
95-
printRich(analysis, logFile, true);
95+
printRich(analysis, events, logFile, true);
9696
System.setOut(original);
9797
String html = HtmlExporter.toHtml(capture.toString(), "Argus GC Log Analysis — " + logFile.getFileName());
9898
try {
@@ -105,10 +105,10 @@ public void execute(String[] args, CliConfig config, ProviderRegistry registry,
105105
return;
106106
}
107107

108-
printRich(analysis, logFile, useColor);
108+
printRich(analysis, events, logFile, useColor);
109109
}
110110

111-
private void printRich(GcLogAnalysis a, Path file, boolean c) {
111+
private void printRich(GcLogAnalysis a, List<GcEvent> events, Path file, boolean c) {
112112
System.out.print(RichRenderer.brandedHeader(c, "gclog",
113113
"GC log analysis with tuning recommendations"));
114114
System.out.println(RichRenderer.boxHeader(c, "GC Log Analysis", WIDTH,
@@ -150,10 +150,72 @@ private void printRich(GcLogAnalysis a, Path file, boolean c) {
150150
+ " " + a.p50PauseMs() + "ms ─── " + a.maxPauseMs() + "ms";
151151
System.out.println(RichRenderer.boxLine(bar, WIDTH));
152152

153+
// Pause Timeline
154+
section(c, "Pause Timeline");
155+
String timeline = GcTimelineRenderer.render(events, a.p50PauseMs(), a.p95PauseMs(), WIDTH, c);
156+
System.out.print(timeline);
157+
153158
// Heap
154159
section(c, "Heap");
155-
kv(c, "Peak Heap", formatKB(a.peakHeapKB()));
156-
kv(c, "Avg After GC", formatKB(a.avgHeapAfterKB()));
160+
kv(c, "Peak Heap", RichRenderer.formatKB(a.peakHeapKB()));
161+
kv(c, "Avg After GC", RichRenderer.formatKB(a.avgHeapAfterKB()));
162+
163+
// Allocation & Promotion Rates
164+
GcRateAnalyzer.RateAnalysis rates = a.rateAnalysis();
165+
if (rates != null && rates.allocationRateKBPerSec() > 0) {
166+
section(c, "Allocation & Promotion Rates");
167+
kv(c, "Allocation Rate",
168+
RichRenderer.formatRate(rates.allocationRateKBPerSec()) + "/s (avg)"
169+
+ " peak: " + RichRenderer.formatRate(rates.peakAllocationRateKBPerSec()) + "/s");
170+
kv(c, "Promotion Rate",
171+
RichRenderer.formatRate(rates.promotionRateKBPerSec()) + "/s (avg)"
172+
+ " peak: " + RichRenderer.formatRate(rates.peakPromotionRateKBPerSec()) + "/s");
173+
kv(c, "Reclaim Efficiency",
174+
String.format("%.1f%%", rates.reclaimEfficiencyPercent()));
175+
String ratioColor = rates.promoAllocRatioPercent() > 5
176+
? AnsiStyle.style(c, AnsiStyle.YELLOW) : AnsiStyle.style(c, AnsiStyle.GREEN);
177+
kv(c, "Promo/Alloc Ratio",
178+
ratioColor + String.format("%.1f%%", rates.promoAllocRatioPercent())
179+
+ AnsiStyle.style(c, AnsiStyle.RESET) + " (healthy: <5%)");
180+
System.out.println(RichRenderer.emptyLine(WIDTH));
181+
System.out.println(RichRenderer.boxLine(
182+
" Alloc " + sparkline(rates.allocationRateWindows())
183+
+ " " + RichRenderer.formatRate(rates.allocationRateKBPerSec()) + "/s avg", WIDTH));
184+
System.out.println(RichRenderer.boxLine(
185+
" Promo " + sparkline(rates.promotionRateWindows())
186+
+ " " + RichRenderer.formatRate(rates.promotionRateKBPerSec()) + "/s avg", WIDTH));
187+
}
188+
189+
// Memory Leak Detection
190+
GcLeakDetector.LeakAnalysis leak = a.leakAnalysis();
191+
if (leak != null && leak.trendPoints().length > 0) {
192+
section(c, "Memory Leak Detection");
193+
if (leak.leakDetected()) {
194+
String leakColor = AnsiStyle.style(c, AnsiStyle.RED, AnsiStyle.BOLD);
195+
kv(c, "Status",
196+
leakColor + "\u26a0 LEAK DETECTED"
197+
+ AnsiStyle.style(c, AnsiStyle.RESET)
198+
+ String.format(" (R\u00b2=%.2f, %.0f%% confidence)",
199+
leak.confidencePercent() / 100.0, leak.confidencePercent()));
200+
kv(c, "Pattern", leak.pattern()
201+
+ (leak.staircaseSteps() > 0 ? " (" + leak.staircaseSteps() + " steps)" : ""));
202+
kv(c, "Growth Rate",
203+
RichRenderer.formatRate(leak.heapGrowthRateKBPerSec()) + "/s"
204+
+ " (" + RichRenderer.formatRate(leak.heapGrowthRateKBPerSec() * 60) + "/min)");
205+
if (leak.estimatedOomSec() > 0) {
206+
kv(c, "Est. OOM in", formatDuration(leak.estimatedOomSec()));
207+
}
208+
System.out.println(RichRenderer.emptyLine(WIDTH));
209+
String chart = renderTrendChart(leak.trendPoints(), leak.trendMinKB(),
210+
leak.trendMaxKB(), WIDTH, c);
211+
System.out.print(chart);
212+
} else {
213+
kv(c, "Status",
214+
AnsiStyle.style(c, AnsiStyle.GREEN) + "\u2713 No leak detected"
215+
+ AnsiStyle.style(c, AnsiStyle.RESET)
216+
+ String.format(" (R\u00b2=%.2f)", leak.confidencePercent() / 100.0));
217+
}
218+
}
157219

158220
// Cause Breakdown
159221
if (!a.causeBreakdown().isEmpty()) {
@@ -227,10 +289,63 @@ private void kv(boolean c, String key, String value) {
227289
" " + RichRenderer.padRight(key, 18) + " " + value, WIDTH));
228290
}
229291

230-
private static String formatKB(long kb) {
231-
if (kb < 1024) return kb + "KB";
232-
if (kb < 1024 * 1024) return (kb / 1024) + "MB";
233-
return String.format("%.1fGB", kb / (1024.0 * 1024));
292+
private static String formatDuration(double sec) {
293+
if (sec < 60) return String.format("%.0f sec", sec);
294+
if (sec < 3600) return String.format("%.1f min", sec / 60);
295+
return String.format("%.1f hours", sec / 3600);
296+
}
297+
298+
private static String sparkline(double[] values) {
299+
if (values == null || values.length == 0) return "";
300+
String bars = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
301+
double min = Double.MAX_VALUE, max = 0;
302+
for (double v : values) {
303+
if (v < min) min = v;
304+
if (v > max) max = v;
305+
}
306+
StringBuilder sb = new StringBuilder();
307+
for (double v : values) {
308+
if (max == min) {
309+
sb.append(bars.charAt(0));
310+
} else {
311+
int idx = (int) ((v - min) / (max - min) * (bars.length() - 1));
312+
sb.append(bars.charAt(Math.max(0, Math.min(idx, bars.length() - 1))));
313+
}
314+
}
315+
return sb.toString();
316+
}
317+
318+
private static String renderTrendChart(double[] points, double minKB, double maxKB,
319+
int width, boolean c) {
320+
if (points.length == 0) return "";
321+
int chartHeight = 5;
322+
int chartWidth = Math.min(points.length, width - 20);
323+
double range = maxKB - minKB;
324+
325+
// Build rows top-to-bottom
326+
StringBuilder sb = new StringBuilder();
327+
for (int row = chartHeight - 1; row >= 0; row--) {
328+
String label = row == chartHeight - 1 ? RichRenderer.padLeft(RichRenderer.formatKB((long) maxKB), 8)
329+
: row == 0 ? RichRenderer.padLeft(RichRenderer.formatKB((long) minKB), 8)
330+
: " ";
331+
StringBuilder line = new StringBuilder(" " + label + " ");
332+
for (int col = 0; col < chartWidth; col++) {
333+
int idx = col * points.length / chartWidth;
334+
double val = points[Math.min(idx, points.length - 1)];
335+
double normalized = range == 0 ? 0 : (val - minKB) / range * (chartHeight - 1);
336+
if (Math.abs(normalized - row) < 0.6) {
337+
line.append(AnsiStyle.style(c, AnsiStyle.RED)).append('\u25cf')
338+
.append(AnsiStyle.style(c, AnsiStyle.RESET));
339+
} else if (normalized > row) {
340+
line.append(AnsiStyle.style(c, AnsiStyle.YELLOW)).append('\u2592')
341+
.append(AnsiStyle.style(c, AnsiStyle.RESET));
342+
} else {
343+
line.append(' ');
344+
}
345+
}
346+
sb.append(RichRenderer.boxLine(line.toString(), width)).append('\n');
347+
}
348+
return sb.toString();
234349
}
235350

236351
private static void printJson(GcLogAnalysis a, Path file) {
@@ -267,7 +382,35 @@ private static void printJson(GcLogAnalysis a, Path file) {
267382
sb.append(",\"problem\":\"").append(RichRenderer.escapeJson(rec.problem())).append('"');
268383
sb.append(",\"flag\":\"").append(RichRenderer.escapeJson(rec.flag())).append("\"}");
269384
}
270-
sb.append("]}");
385+
sb.append(']');
386+
387+
// Rate analysis
388+
GcRateAnalyzer.RateAnalysis rates = a.rateAnalysis();
389+
if (rates != null) {
390+
sb.append(",\"rateAnalysis\":{");
391+
sb.append("\"allocationRateKBPerSec\":").append(String.format("%.1f", rates.allocationRateKBPerSec()));
392+
sb.append(",\"peakAllocationRateKBPerSec\":").append(String.format("%.1f", rates.peakAllocationRateKBPerSec()));
393+
sb.append(",\"promotionRateKBPerSec\":").append(String.format("%.1f", rates.promotionRateKBPerSec()));
394+
sb.append(",\"peakPromotionRateKBPerSec\":").append(String.format("%.1f", rates.peakPromotionRateKBPerSec()));
395+
sb.append(",\"reclaimEfficiencyPercent\":").append(String.format("%.1f", rates.reclaimEfficiencyPercent()));
396+
sb.append(",\"promoAllocRatioPercent\":").append(String.format("%.1f", rates.promoAllocRatioPercent()));
397+
sb.append('}');
398+
}
399+
400+
// Leak analysis
401+
GcLeakDetector.LeakAnalysis leak = a.leakAnalysis();
402+
if (leak != null) {
403+
sb.append(",\"leakAnalysis\":{");
404+
sb.append("\"leakDetected\":").append(leak.leakDetected());
405+
sb.append(",\"heapGrowthRateKBPerSec\":").append(String.format("%.3f", leak.heapGrowthRateKBPerSec()));
406+
sb.append(",\"estimatedOomSec\":").append(String.format("%.0f", leak.estimatedOomSec()));
407+
sb.append(",\"confidencePercent\":").append(String.format("%.1f", leak.confidencePercent()));
408+
sb.append(",\"pattern\":\"").append(leak.pattern()).append('"');
409+
sb.append(",\"staircaseSteps\":").append(leak.staircaseSteps());
410+
sb.append('}');
411+
}
412+
413+
sb.append('}');
271414
System.out.println(sb);
272415
}
273416
}

0 commit comments

Comments
 (0)