Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions argus-cli/src/main/java/io/argus/cli/command/GcLogCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,20 @@ public void execute(String[] args, CliConfig config, ProviderRegistry registry,
boolean useColor = config.color();
boolean flagsOnly = false;
boolean showPhases = false;
boolean tenuring = false;
String exportHtml = null;
for (int i = 1; i < args.length; i++) {
if (args[i].equals("--format=json")) json = true;
if (args[i].equals("--suggest-flags")) flagsOnly = true;
if (args[i].startsWith("--export=")) exportHtml = args[i].substring(9);
if (args[i].equals("--phases")) showPhases = true;
if (args[i].equals("--tenuring")) tenuring = true;
}

// --tenuring: dedicated age table analysis from debug GC log
if (tenuring) {
printTenuringAnalysis(logFile, useColor);
return;
}

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

private void printTenuringAnalysis(Path logFile, boolean c) {
TenuringAnalyzer.TenuringAnalysis analysis;
try {
analysis = TenuringAnalyzer.analyze(logFile);
} catch (IOException e) {
System.err.println("Failed to read GC log: " + e.getMessage());
return;
}

System.out.print(RichRenderer.brandedHeader(c, "gclog", "Tenuring analysis from GC age table"));
System.out.println(RichRenderer.boxHeader(c, "Tenuring Analysis", WIDTH,
"file:" + logFile.getFileName()));
System.out.println(RichRenderer.emptyLine(WIDTH));

if (analysis.snapshots().isEmpty()) {
System.out.println(RichRenderer.boxLine(
" No age table entries found.", WIDTH));
System.out.println(RichRenderer.boxLine(
" Enable with: -Xlog:gc+age=debug", WIDTH));
System.out.println(RichRenderer.boxFooter(c, null, WIDTH));
return;
}

// Summary
kv(c, "GC snapshots", String.valueOf(analysis.snapshots().size()));
kv(c, "Min threshold", String.valueOf(analysis.minThreshold()));
kv(c, "Max threshold seen", String.valueOf(analysis.maxThresholdSeen()));

if (analysis.prematurePromotionDetected()) {
kv(c, "Premature promotion",
AnsiStyle.style(c, AnsiStyle.RED) + "DETECTED"
+ AnsiStyle.style(c, AnsiStyle.RESET));
}
if (analysis.survivorOverflowDetected()) {
kv(c, "Survivor overflow",
AnsiStyle.style(c, AnsiStyle.YELLOW) + "DETECTED"
+ AnsiStyle.style(c, AnsiStyle.RESET));
}

// Latest age snapshot
if (!analysis.snapshots().isEmpty()) {
TenuringAnalyzer.GcAgeSnapshot latest = analysis.snapshots().getLast();
section(c, "Latest Age Distribution (GC " + latest.gcId() + ")");

var entries = latest.distribution().entries();
long total = latest.distribution().survivorCapacity();

if (!entries.isEmpty()) {
String bold = AnsiStyle.style(c, AnsiStyle.BOLD);
String reset = AnsiStyle.style(c, AnsiStyle.RESET);
System.out.println(RichRenderer.boxLine(
" " + bold
+ RichRenderer.padRight("Age", 5)
+ RichRenderer.padLeft("Bytes", 12)
+ RichRenderer.padLeft("Cumulative", 13)
+ " Bar"
+ reset, WIDTH));
System.out.println(RichRenderer.emptyLine(WIDTH));

for (var e : entries) {
int pct = total > 0 ? (int) (e.bytes() * 100 / total) : 0;
int barLen = 20 * pct / 100;
String bar = "\u2588".repeat(Math.max(0, barLen));
boolean atThreshold = e.age() == latest.distribution().tenuringThreshold();
String lColor = atThreshold ? AnsiStyle.style(c, AnsiStyle.YELLOW) : "";
String lReset = atThreshold ? AnsiStyle.style(c, AnsiStyle.RESET) : "";
System.out.println(RichRenderer.boxLine(" " + lColor
+ RichRenderer.padLeft(String.valueOf(e.age()), 3) + " "
+ RichRenderer.padLeft(RichRenderer.formatKB(e.bytes() / 1024), 10) + " "
+ RichRenderer.padLeft(RichRenderer.formatKB(e.cumulativeBytes() / 1024), 10) + " "
+ RichRenderer.padRight(bar, 20) + " " + pct + "%"
+ lReset, WIDTH));
}
System.out.println(RichRenderer.emptyLine(WIDTH));
System.out.println(RichRenderer.boxLine(
" Tenuring: " + latest.distribution().tenuringThreshold()
+ " / max: " + latest.distribution().maxTenuringThreshold(), WIDTH));
}
}

// Insights
if (!analysis.insights().isEmpty()) {
section(c, "Insights");
for (String insight : analysis.insights()) {
System.out.println(RichRenderer.boxLine(" \u2192 " + insight, WIDTH));
}
}

// Recommendations
if (!analysis.recommendations().isEmpty()) {
section(c, "Recommendations");
for (int i = 0; i < analysis.recommendations().size(); i++) {
System.out.println(RichRenderer.boxLine(
" " + (i + 1) + ". " + analysis.recommendations().get(i), WIDTH));
}
}

System.out.println(RichRenderer.boxFooter(c,
analysis.snapshots().size() + " snapshots", WIDTH));
}

private void section(boolean c, String title) {
System.out.println(RichRenderer.emptyLine(WIDTH));
System.out.println(RichRenderer.boxSeparator(WIDTH));
Expand Down
143 changes: 143 additions & 0 deletions argus-cli/src/main/java/io/argus/cli/command/GcNewCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,26 @@

import io.argus.cli.config.CliConfig;
import io.argus.cli.config.Messages;
import io.argus.cli.model.AgeDistribution;
import io.argus.cli.model.GcNewResult;
import io.argus.cli.provider.GcAgeProvider;
import io.argus.cli.provider.GcNewProvider;
import io.argus.cli.provider.ProviderRegistry;
import io.argus.cli.render.AnsiStyle;
import io.argus.cli.render.RichRenderer;
import io.argus.core.command.CommandGroup;

import java.util.List;

/**
* Shows young generation GC detail: survivor spaces, tenuring threshold, eden.
* With --age-histogram shows per-age object distribution.
*/
public final class GcNewCommand implements Command {

private static final int WIDTH = RichRenderer.DEFAULT_WIDTH;
private static final int BAR_WIDTH = 16;
private static final int AGE_BAR_WIDTH = 20;

@Override
public String name() { return "gcnew"; }
Expand All @@ -38,9 +44,11 @@ public void execute(String[] args, CliConfig config, ProviderRegistry registry,
String sourceOverride = null;
boolean json = "json".equals(config.format());
boolean useColor = config.color();
boolean ageHistogram = false;
for (int i = 1; i < args.length; i++) {
if (args[i].startsWith("--source=")) sourceOverride = args[i].substring(9);
else if (args[i].equals("--format=json")) json = true;
else if (args[i].equals("--age-histogram")) ageHistogram = true;
}

String source = sourceOverride != null ? sourceOverride : config.defaultSource();
Expand Down Expand Up @@ -96,9 +104,144 @@ public void execute(String[] args, CliConfig config, ProviderRegistry registry,
String gcLine = "YGC: " + result.ygc() + " (" + String.format("%.3fs", result.ygct()) + ")";
System.out.println(RichRenderer.boxLine(gcLine, WIDTH));

// Age histogram
if (ageHistogram) {
System.out.println(RichRenderer.emptyLine(WIDTH));
System.out.println(RichRenderer.boxSeparator(WIDTH));
System.out.println(RichRenderer.boxLine(
" " + AnsiStyle.style(useColor, AnsiStyle.BOLD, AnsiStyle.CYAN)
+ messages.get("gcnew.age.title")
+ AnsiStyle.style(useColor, AnsiStyle.RESET), WIDTH));
System.out.println(RichRenderer.emptyLine(WIDTH));

GcAgeProvider ageProvider = registry.findGcAgeProvider(pid, sourceOverride);
if (ageProvider == null) {
System.out.println(RichRenderer.boxLine(
" " + messages.get("gcnew.age.unavailable"), WIDTH));
} else {
AgeDistribution dist = ageProvider.getAgeDistribution(pid);
renderAgeHistogram(dist, useColor);
}
}

System.out.println(RichRenderer.boxFooter(useColor, null, WIDTH));
}

private void renderAgeHistogram(AgeDistribution dist, boolean useColor) {
List<AgeDistribution.AgeEntry> entries = dist.entries();

if (entries.isEmpty()) {
System.out.println(RichRenderer.boxLine(
" " + "No age data available. Run with -Xlog:gc+age=debug for live data.", WIDTH));
if (dist.tenuringThreshold() > 0) {
System.out.println(RichRenderer.boxLine(
" Tenuring: " + dist.tenuringThreshold()
+ " / max: " + dist.maxTenuringThreshold(), WIDTH));
}
return;
}

// Header
String bold = AnsiStyle.style(useColor, AnsiStyle.BOLD);
String reset = AnsiStyle.style(useColor, AnsiStyle.RESET);
System.out.println(RichRenderer.boxLine(
" " + bold
+ RichRenderer.padRight("Age", 5)
+ RichRenderer.padLeft("Bytes", 12)
+ RichRenderer.padLeft("Cumulative", 13)
+ " Bar"
+ reset, WIDTH));
System.out.println(RichRenderer.emptyLine(WIDTH));

long total = dist.survivorCapacity();
if (total == 0 && !entries.isEmpty()) total = entries.getLast().cumulativeBytes();

// Group ages >= 6 together
long ageGe6Bytes = 0;
long ageGe6Cumulative = 0;
boolean hasHighAges = false;

for (AgeDistribution.AgeEntry e : entries) {
if (e.age() >= 6) {
ageGe6Bytes += e.bytes();
ageGe6Cumulative = e.cumulativeBytes();
hasHighAges = true;
}
}

for (AgeDistribution.AgeEntry e : entries) {
if (e.age() >= 6) continue;
renderAgeLine(e.age(), String.valueOf(e.age()), e.bytes(), e.cumulativeBytes(),
total, useColor, dist.tenuringThreshold());
}

if (hasHighAges) {
renderAgeLine(-1, "6+", ageGe6Bytes, ageGe6Cumulative, total, useColor, dist.tenuringThreshold());
}

System.out.println(RichRenderer.emptyLine(WIDTH));

// Summary lines
long survivorCap = dist.survivorCapacity();
long desiredSize = dist.desiredSurvivorSize();
int survivorPct = survivorCap > 0 && desiredSize > 0
? (int) Math.min(100, total * 100 / desiredSize) : 0;

System.out.println(RichRenderer.boxLine(
" Tenuring: " + dist.tenuringThreshold() + " / max: " + dist.maxTenuringThreshold(), WIDTH));
if (desiredSize > 0) {
System.out.println(RichRenderer.boxLine(
" Survivor: " + survivorPct + "% ("
+ RichRenderer.formatKB(total / 1024)
+ " / " + RichRenderer.formatKB(desiredSize / 1024) + ")", WIDTH));
}

// Insights
System.out.println(RichRenderer.emptyLine(WIDTH));
if (!entries.isEmpty() && total > 0) {
long age1Bytes = entries.getFirst().age() == 1 ? entries.getFirst().bytes() : 0;
int age1Pct = (int) (age1Bytes * 100 / total);
if (age1Pct >= 50) {
System.out.println(RichRenderer.boxLine(
" \u2192 " + age1Pct + "% die at age 1 (healthy)", WIDTH));
}
}

// MaxTenuringThreshold suggestion
if (dist.tenuringThreshold() > 4 && !entries.isEmpty()) {
// Find age at which 80% is accumulated
long threshold80 = (long) (total * 0.80);
for (AgeDistribution.AgeEntry e : entries) {
if (e.cumulativeBytes() >= threshold80) {
if (e.age() < dist.maxTenuringThreshold()) {
System.out.println(RichRenderer.boxLine(
" \u2192 Consider -XX:MaxTenuringThreshold=" + e.age(), WIDTH));
}
break;
}
}
}
}

private void renderAgeLine(int age, String label, long bytes, long cumulative,
long total, boolean useColor, int tenuringThreshold) {
int pct = total > 0 ? (int) (bytes * 100 / total) : 0;
int barLen = AGE_BAR_WIDTH * pct / 100;
String bar = "\u2588".repeat(Math.max(0, barLen));

boolean atThreshold = age == tenuringThreshold;
String color = atThreshold ? AnsiStyle.style(useColor, AnsiStyle.YELLOW) : "";
String reset = atThreshold ? AnsiStyle.style(useColor, AnsiStyle.RESET) : "";

String line = color
+ RichRenderer.padLeft(label, 3) + " "
+ RichRenderer.padLeft(RichRenderer.formatKB(bytes / 1024), 10) + " "
+ RichRenderer.padLeft(RichRenderer.formatKB(cumulative / 1024), 10) + " "
+ RichRenderer.padRight(bar, AGE_BAR_WIDTH) + " " + pct + "%"
+ reset;
System.out.println(RichRenderer.boxLine(" " + line, WIDTH));
}

private static void printJson(GcNewResult r) {
System.out.println("{\"s0c\":" + r.s0c() + ",\"s1c\":" + r.s1c()
+ ",\"s0u\":" + r.s0u() + ",\"s1u\":" + r.s1u()
Expand Down
Loading
Loading