Skip to content

Commit 3bec853

Browse files
authored
Merge pull request #120 from rlaope/feat/tenuring-analysis
feat: object age distribution & tenuring analysis (#117)
2 parents 76abcfe + 76b4d73 commit 3bec853

10 files changed

Lines changed: 685 additions & 0 deletions

File tree

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

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,20 @@ public void execute(String[] args, CliConfig config, ProviderRegistry registry,
5555
boolean useColor = config.color();
5656
boolean flagsOnly = false;
5757
boolean showPhases = false;
58+
boolean tenuring = false;
5859
String exportHtml = null;
5960
for (int i = 1; i < args.length; i++) {
6061
if (args[i].equals("--format=json")) json = true;
6162
if (args[i].equals("--suggest-flags")) flagsOnly = true;
6263
if (args[i].startsWith("--export=")) exportHtml = args[i].substring(9);
6364
if (args[i].equals("--phases")) showPhases = true;
65+
if (args[i].equals("--tenuring")) tenuring = true;
66+
}
67+
68+
// --tenuring: dedicated age table analysis from debug GC log
69+
if (tenuring) {
70+
printTenuringAnalysis(logFile, useColor);
71+
return;
6472
}
6573

6674
List<GcEvent> events;
@@ -324,6 +332,107 @@ private void printPhaseBreakdown(boolean c, GcPhaseAnalyzer.PhaseAnalysis analys
324332
+ RichRenderer.padLeft("100%", BAR_WIDTH + 25), WIDTH));
325333
}
326334

335+
private void printTenuringAnalysis(Path logFile, boolean c) {
336+
TenuringAnalyzer.TenuringAnalysis analysis;
337+
try {
338+
analysis = TenuringAnalyzer.analyze(logFile);
339+
} catch (IOException e) {
340+
System.err.println("Failed to read GC log: " + e.getMessage());
341+
return;
342+
}
343+
344+
System.out.print(RichRenderer.brandedHeader(c, "gclog", "Tenuring analysis from GC age table"));
345+
System.out.println(RichRenderer.boxHeader(c, "Tenuring Analysis", WIDTH,
346+
"file:" + logFile.getFileName()));
347+
System.out.println(RichRenderer.emptyLine(WIDTH));
348+
349+
if (analysis.snapshots().isEmpty()) {
350+
System.out.println(RichRenderer.boxLine(
351+
" No age table entries found.", WIDTH));
352+
System.out.println(RichRenderer.boxLine(
353+
" Enable with: -Xlog:gc+age=debug", WIDTH));
354+
System.out.println(RichRenderer.boxFooter(c, null, WIDTH));
355+
return;
356+
}
357+
358+
// Summary
359+
kv(c, "GC snapshots", String.valueOf(analysis.snapshots().size()));
360+
kv(c, "Min threshold", String.valueOf(analysis.minThreshold()));
361+
kv(c, "Max threshold seen", String.valueOf(analysis.maxThresholdSeen()));
362+
363+
if (analysis.prematurePromotionDetected()) {
364+
kv(c, "Premature promotion",
365+
AnsiStyle.style(c, AnsiStyle.RED) + "DETECTED"
366+
+ AnsiStyle.style(c, AnsiStyle.RESET));
367+
}
368+
if (analysis.survivorOverflowDetected()) {
369+
kv(c, "Survivor overflow",
370+
AnsiStyle.style(c, AnsiStyle.YELLOW) + "DETECTED"
371+
+ AnsiStyle.style(c, AnsiStyle.RESET));
372+
}
373+
374+
// Latest age snapshot
375+
if (!analysis.snapshots().isEmpty()) {
376+
TenuringAnalyzer.GcAgeSnapshot latest = analysis.snapshots().getLast();
377+
section(c, "Latest Age Distribution (GC " + latest.gcId() + ")");
378+
379+
var entries = latest.distribution().entries();
380+
long total = latest.distribution().survivorCapacity();
381+
382+
if (!entries.isEmpty()) {
383+
String bold = AnsiStyle.style(c, AnsiStyle.BOLD);
384+
String reset = AnsiStyle.style(c, AnsiStyle.RESET);
385+
System.out.println(RichRenderer.boxLine(
386+
" " + bold
387+
+ RichRenderer.padRight("Age", 5)
388+
+ RichRenderer.padLeft("Bytes", 12)
389+
+ RichRenderer.padLeft("Cumulative", 13)
390+
+ " Bar"
391+
+ reset, WIDTH));
392+
System.out.println(RichRenderer.emptyLine(WIDTH));
393+
394+
for (var e : entries) {
395+
int pct = total > 0 ? (int) (e.bytes() * 100 / total) : 0;
396+
int barLen = 20 * pct / 100;
397+
String bar = "\u2588".repeat(Math.max(0, barLen));
398+
boolean atThreshold = e.age() == latest.distribution().tenuringThreshold();
399+
String lColor = atThreshold ? AnsiStyle.style(c, AnsiStyle.YELLOW) : "";
400+
String lReset = atThreshold ? AnsiStyle.style(c, AnsiStyle.RESET) : "";
401+
System.out.println(RichRenderer.boxLine(" " + lColor
402+
+ RichRenderer.padLeft(String.valueOf(e.age()), 3) + " "
403+
+ RichRenderer.padLeft(RichRenderer.formatKB(e.bytes() / 1024), 10) + " "
404+
+ RichRenderer.padLeft(RichRenderer.formatKB(e.cumulativeBytes() / 1024), 10) + " "
405+
+ RichRenderer.padRight(bar, 20) + " " + pct + "%"
406+
+ lReset, WIDTH));
407+
}
408+
System.out.println(RichRenderer.emptyLine(WIDTH));
409+
System.out.println(RichRenderer.boxLine(
410+
" Tenuring: " + latest.distribution().tenuringThreshold()
411+
+ " / max: " + latest.distribution().maxTenuringThreshold(), WIDTH));
412+
}
413+
}
414+
415+
// Insights
416+
if (!analysis.insights().isEmpty()) {
417+
section(c, "Insights");
418+
for (String insight : analysis.insights()) {
419+
System.out.println(RichRenderer.boxLine(" \u2192 " + insight, WIDTH));
420+
}
421+
}
422+
423+
// Recommendations
424+
if (!analysis.recommendations().isEmpty()) {
425+
section(c, "Recommendations");
426+
for (int i = 0; i < analysis.recommendations().size(); i++) {
427+
System.out.println(RichRenderer.boxLine(
428+
" " + (i + 1) + ". " + analysis.recommendations().get(i), WIDTH));
429+
}
430+
}
431+
432+
System.out.println(RichRenderer.boxFooter(c,
433+
analysis.snapshots().size() + " snapshots", WIDTH));
434+
}
435+
327436
private void section(boolean c, String title) {
328437
System.out.println(RichRenderer.emptyLine(WIDTH));
329438
System.out.println(RichRenderer.boxSeparator(WIDTH));

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

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,26 @@
22

33
import io.argus.cli.config.CliConfig;
44
import io.argus.cli.config.Messages;
5+
import io.argus.cli.model.AgeDistribution;
56
import io.argus.cli.model.GcNewResult;
7+
import io.argus.cli.provider.GcAgeProvider;
68
import io.argus.cli.provider.GcNewProvider;
79
import io.argus.cli.provider.ProviderRegistry;
810
import io.argus.cli.render.AnsiStyle;
911
import io.argus.cli.render.RichRenderer;
1012
import 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
*/
1520
public 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

Comments
 (0)