22
33import io .argus .cli .config .CliConfig ;
44import io .argus .cli .config .Messages ;
5+ import io .argus .cli .model .AgeDistribution ;
56import io .argus .cli .model .GcNewResult ;
7+ import io .argus .cli .provider .GcAgeProvider ;
68import io .argus .cli .provider .GcNewProvider ;
79import io .argus .cli .provider .ProviderRegistry ;
810import io .argus .cli .render .AnsiStyle ;
911import io .argus .cli .render .RichRenderer ;
1012import io .argus .core .command .CommandGroup ;
1113
14+ import java .util .List ;
15+
1216/**
1317 * Shows young generation GC detail: survivor spaces, tenuring threshold, eden.
18+ * With --age-histogram shows per-age object distribution.
1419 */
1520public final class GcNewCommand implements Command {
1621
1722 private static final int WIDTH = RichRenderer .DEFAULT_WIDTH ;
1823 private static final int BAR_WIDTH = 16 ;
24+ private static final int AGE_BAR_WIDTH = 20 ;
1925
2026 @ Override
2127 public String name () { return "gcnew" ; }
@@ -38,9 +44,11 @@ public void execute(String[] args, CliConfig config, ProviderRegistry registry,
3844 String sourceOverride = null ;
3945 boolean json = "json" .equals (config .format ());
4046 boolean useColor = config .color ();
47+ boolean ageHistogram = false ;
4148 for (int i = 1 ; i < args .length ; i ++) {
4249 if (args [i ].startsWith ("--source=" )) sourceOverride = args [i ].substring (9 );
4350 else if (args [i ].equals ("--format=json" )) json = true ;
51+ else if (args [i ].equals ("--age-histogram" )) ageHistogram = true ;
4452 }
4553
4654 String source = sourceOverride != null ? sourceOverride : config .defaultSource ();
@@ -96,9 +104,144 @@ public void execute(String[] args, CliConfig config, ProviderRegistry registry,
96104 String gcLine = "YGC: " + result .ygc () + " (" + String .format ("%.3fs" , result .ygct ()) + ")" ;
97105 System .out .println (RichRenderer .boxLine (gcLine , WIDTH ));
98106
107+ // Age histogram
108+ if (ageHistogram ) {
109+ System .out .println (RichRenderer .emptyLine (WIDTH ));
110+ System .out .println (RichRenderer .boxSeparator (WIDTH ));
111+ System .out .println (RichRenderer .boxLine (
112+ " " + AnsiStyle .style (useColor , AnsiStyle .BOLD , AnsiStyle .CYAN )
113+ + messages .get ("gcnew.age.title" )
114+ + AnsiStyle .style (useColor , AnsiStyle .RESET ), WIDTH ));
115+ System .out .println (RichRenderer .emptyLine (WIDTH ));
116+
117+ GcAgeProvider ageProvider = registry .findGcAgeProvider (pid , sourceOverride );
118+ if (ageProvider == null ) {
119+ System .out .println (RichRenderer .boxLine (
120+ " " + messages .get ("gcnew.age.unavailable" ), WIDTH ));
121+ } else {
122+ AgeDistribution dist = ageProvider .getAgeDistribution (pid );
123+ renderAgeHistogram (dist , useColor );
124+ }
125+ }
126+
99127 System .out .println (RichRenderer .boxFooter (useColor , null , WIDTH ));
100128 }
101129
130+ private void renderAgeHistogram (AgeDistribution dist , boolean useColor ) {
131+ List <AgeDistribution .AgeEntry > entries = dist .entries ();
132+
133+ if (entries .isEmpty ()) {
134+ System .out .println (RichRenderer .boxLine (
135+ " " + "No age data available. Run with -Xlog:gc+age=debug for live data." , WIDTH ));
136+ if (dist .tenuringThreshold () > 0 ) {
137+ System .out .println (RichRenderer .boxLine (
138+ " Tenuring: " + dist .tenuringThreshold ()
139+ + " / max: " + dist .maxTenuringThreshold (), WIDTH ));
140+ }
141+ return ;
142+ }
143+
144+ // Header
145+ String bold = AnsiStyle .style (useColor , AnsiStyle .BOLD );
146+ String reset = AnsiStyle .style (useColor , AnsiStyle .RESET );
147+ System .out .println (RichRenderer .boxLine (
148+ " " + bold
149+ + RichRenderer .padRight ("Age" , 5 )
150+ + RichRenderer .padLeft ("Bytes" , 12 )
151+ + RichRenderer .padLeft ("Cumulative" , 13 )
152+ + " Bar"
153+ + reset , WIDTH ));
154+ System .out .println (RichRenderer .emptyLine (WIDTH ));
155+
156+ long total = dist .survivorCapacity ();
157+ if (total == 0 && !entries .isEmpty ()) total = entries .getLast ().cumulativeBytes ();
158+
159+ // Group ages >= 6 together
160+ long ageGe6Bytes = 0 ;
161+ long ageGe6Cumulative = 0 ;
162+ boolean hasHighAges = false ;
163+
164+ for (AgeDistribution .AgeEntry e : entries ) {
165+ if (e .age () >= 6 ) {
166+ ageGe6Bytes += e .bytes ();
167+ ageGe6Cumulative = e .cumulativeBytes ();
168+ hasHighAges = true ;
169+ }
170+ }
171+
172+ for (AgeDistribution .AgeEntry e : entries ) {
173+ if (e .age () >= 6 ) continue ;
174+ renderAgeLine (e .age (), String .valueOf (e .age ()), e .bytes (), e .cumulativeBytes (),
175+ total , useColor , dist .tenuringThreshold ());
176+ }
177+
178+ if (hasHighAges ) {
179+ renderAgeLine (-1 , "6+" , ageGe6Bytes , ageGe6Cumulative , total , useColor , dist .tenuringThreshold ());
180+ }
181+
182+ System .out .println (RichRenderer .emptyLine (WIDTH ));
183+
184+ // Summary lines
185+ long survivorCap = dist .survivorCapacity ();
186+ long desiredSize = dist .desiredSurvivorSize ();
187+ int survivorPct = survivorCap > 0 && desiredSize > 0
188+ ? (int ) Math .min (100 , total * 100 / desiredSize ) : 0 ;
189+
190+ System .out .println (RichRenderer .boxLine (
191+ " Tenuring: " + dist .tenuringThreshold () + " / max: " + dist .maxTenuringThreshold (), WIDTH ));
192+ if (desiredSize > 0 ) {
193+ System .out .println (RichRenderer .boxLine (
194+ " Survivor: " + survivorPct + "% ("
195+ + RichRenderer .formatKB (total / 1024 )
196+ + " / " + RichRenderer .formatKB (desiredSize / 1024 ) + ")" , WIDTH ));
197+ }
198+
199+ // Insights
200+ System .out .println (RichRenderer .emptyLine (WIDTH ));
201+ if (!entries .isEmpty () && total > 0 ) {
202+ long age1Bytes = entries .getFirst ().age () == 1 ? entries .getFirst ().bytes () : 0 ;
203+ int age1Pct = (int ) (age1Bytes * 100 / total );
204+ if (age1Pct >= 50 ) {
205+ System .out .println (RichRenderer .boxLine (
206+ " \u2192 " + age1Pct + "% die at age 1 (healthy)" , WIDTH ));
207+ }
208+ }
209+
210+ // MaxTenuringThreshold suggestion
211+ if (dist .tenuringThreshold () > 4 && !entries .isEmpty ()) {
212+ // Find age at which 80% is accumulated
213+ long threshold80 = (long ) (total * 0.80 );
214+ for (AgeDistribution .AgeEntry e : entries ) {
215+ if (e .cumulativeBytes () >= threshold80 ) {
216+ if (e .age () < dist .maxTenuringThreshold ()) {
217+ System .out .println (RichRenderer .boxLine (
218+ " \u2192 Consider -XX:MaxTenuringThreshold=" + e .age (), WIDTH ));
219+ }
220+ break ;
221+ }
222+ }
223+ }
224+ }
225+
226+ private void renderAgeLine (int age , String label , long bytes , long cumulative ,
227+ long total , boolean useColor , int tenuringThreshold ) {
228+ int pct = total > 0 ? (int ) (bytes * 100 / total ) : 0 ;
229+ int barLen = AGE_BAR_WIDTH * pct / 100 ;
230+ String bar = "\u2588 " .repeat (Math .max (0 , barLen ));
231+
232+ boolean atThreshold = age == tenuringThreshold ;
233+ String color = atThreshold ? AnsiStyle .style (useColor , AnsiStyle .YELLOW ) : "" ;
234+ String reset = atThreshold ? AnsiStyle .style (useColor , AnsiStyle .RESET ) : "" ;
235+
236+ String line = color
237+ + RichRenderer .padLeft (label , 3 ) + " "
238+ + RichRenderer .padLeft (RichRenderer .formatKB (bytes / 1024 ), 10 ) + " "
239+ + RichRenderer .padLeft (RichRenderer .formatKB (cumulative / 1024 ), 10 ) + " "
240+ + RichRenderer .padRight (bar , AGE_BAR_WIDTH ) + " " + pct + "%"
241+ + reset ;
242+ System .out .println (RichRenderer .boxLine (" " + line , WIDTH ));
243+ }
244+
102245 private static void printJson (GcNewResult r ) {
103246 System .out .println ("{\" s0c\" :" + r .s0c () + ",\" s1c\" :" + r .s1c ()
104247 + ",\" s0u\" :" + r .s0u () + ",\" s1u\" :" + r .s1u ()
0 commit comments