@@ -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