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
2 changes: 2 additions & 0 deletions argus-cli/src/main/java/io/argus/cli/ArgusCli.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
import io.argus.cli.command.ThreadDumpCommand;
import io.argus.cli.command.ThreadsCommand;
import io.argus.cli.command.TopCommand;
import io.argus.cli.command.TraceCommand;
import io.argus.cli.command.TuiCommand;
import io.argus.cli.command.VmFlagCommand;
import io.argus.cli.command.VmLogCommand;
Expand Down Expand Up @@ -210,6 +211,7 @@ public static void main(String[] args) {
register(commands, new PerfCounterCommand());
register(commands, new MBeanCommand());
register(commands, new TopCommand());
register(commands, new TraceCommand());
register(commands, new WatchCommand());
register(commands, new ExplainCommand());
register(commands, new TuiCommand(commands));
Expand Down
409 changes: 409 additions & 0 deletions argus-cli/src/main/java/io/argus/cli/command/TraceCommand.java

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions argus-cli/src/main/resources/messages_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -354,3 +354,10 @@ error.profile.invalid.type=Invalid profiling type: %s. Use: cpu, alloc, lock, wa
gcnew.age.title=Object Age Distribution
gcnew.age.unavailable=Age data unavailable. Enable -Xlog:gc+age=debug for live data.
cmd.gclog.tenuring.desc=Analyze tenuring threshold changes from GC age log
cmd.trace.desc=Method execution tracing via rapid thread sampling
header.trace=Method Trace
desc.trace=Traces method execution by sampling thread dumps at 10/sec. Builds a call tree with timing estimates.
status.trace.sampling=Tracing PID %s for %ss (%s samples)...
status.trace.no.match=No stack traces matched '%s' in %s samples
error.trace.invalid.target=Invalid target format: '%s'. Use class.method (e.g. com.example.OrderService.createOrder)
trace.summary=%s samples, %s matched target method '%s'
7 changes: 7 additions & 0 deletions argus-cli/src/main/resources/messages_ja.properties
Original file line number Diff line number Diff line change
Expand Up @@ -338,3 +338,10 @@ error.profile.invalid.type=\u7121\u52B9\u306A\u30D7\u30ED\u30D5\u30A1\u30A4\u30E
gcnew.age.title=\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u5E74\u9F62\u5206\u5E03
gcnew.age.unavailable=\u5E74\u9F62\u30C7\u30FC\u30BF\u306F\u5229\u7528\u4E0D\u53EF\u3002-Xlog:gc+age=debug\u3067\u6709\u52B9\u5316\u3057\u3066\u304F\u3060\u3055\u3044\u3002
cmd.gclog.tenuring.desc=GC\u30A8\u30FC\u30B8\u30ED\u30B0\u304B\u3089\u30C6\u30CB\u30E5\u30A2\u30EA\u30F3\u30B0\u5909\u5316\u3092\u5206\u6790
cmd.trace.desc=高速スレッドサンプリングによるメソッド実行トレース
header.trace=メソッドトレース
desc.trace=秒間10回のスレッドダンプサンプリングでメソッド実行を追跡します。タイミング推定付きのコールツリーを生成します。
status.trace.sampling=PID %sを%s秒間トレース中 (%sサンプル)...
status.trace.no.match=%sサンプルで'%s'に一致するスタックトレースが見つかりません
error.trace.invalid.target=無効なターゲット形式: '%s'。class.method形式を使用してください (例: com.example.OrderService.createOrder)
trace.summary=%sサンプル、%s件がターゲットメソッド'%s'に一致
7 changes: 7 additions & 0 deletions argus-cli/src/main/resources/messages_ko.properties
Original file line number Diff line number Diff line change
Expand Up @@ -338,3 +338,10 @@ error.profile.invalid.type=\uC798\uBABB\uB41C \uD504\uB85C\uD30C\uC77C\uB9C1 \uD
gcnew.age.title=\uAC1D\uCCB4 \uC5F0\uB839 \uBD84\uD3EC
gcnew.age.unavailable=\uC5F0\uB839 \uB370\uC774\uD130\uB97C \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. -Xlog:gc+age=debug\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.
cmd.gclog.tenuring.desc=GC \uC5D0\uC774\uC9C0 \uB85C\uADF8\uC5D0\uC11C \uD14C\uB274\uC5B4\uB9C1 \uBCC0\uD654 \uBD84\uC11D
cmd.trace.desc=고속 스레드 샘플링을 통한 메서드 실행 추적
header.trace=메서드 추적
desc.trace=초당 10회 스레드 덤프 샘플링으로 메서드 실행을 추적합니다. 타이밍 추정치와 함께 호출 트리를 생성합니다.
status.trace.sampling=PID %s를 %s초 동안 추적 중 (%s개 샘플)...
status.trace.no.match=%s개 샘플에서 '%s'와 일치하는 스택 트레이스가 없습니다
error.trace.invalid.target=잘못된 대상 형식: '%s'. class.method 형식을 사용하세요 (예: com.example.OrderService.createOrder)
trace.summary=%s개 샘플, %s개가 대상 메서드 '%s'와 일치
7 changes: 7 additions & 0 deletions argus-cli/src/main/resources/messages_zh.properties
Original file line number Diff line number Diff line change
Expand Up @@ -335,3 +335,10 @@ error.profile.unsupported.platform=\u6B64\u5E73\u53F0\u4E0D\u652F\u6301async-pro
error.profile.download.failed=async-profiler\u4E0B\u8F7D\u5931\u8D25: %s
error.profile.asprof.failed=async-profiler\u5931\u8D25: %s
error.profile.invalid.type=\u65E0\u6548\u7684\u5206\u6790\u7C7B\u578B: %s\u3002\u8BF7\u4F7F\u7528: cpu, alloc, lock, wall
cmd.trace.desc=通过高速线程采样进行方法执行追踪
header.trace=方法追踪
desc.trace=以每秒10次的频率采样线程转储来追踪方法执行,生成带时间估算的调用树。
status.trace.sampling=正在追踪PID %s,持续%s秒(%s个样本)...
status.trace.no.match=在%s个样本中未找到匹配'%s'的堆栈跟踪
error.trace.invalid.target=无效的目标格式:'%s'。请使用class.method格式(例如:com.example.OrderService.createOrder)
trace.summary=%s个样本,%s个匹配目标方法'%s'
138 changes: 138 additions & 0 deletions argus-cli/src/test/java/io/argus/cli/command/TraceCommandTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package io.argus.cli.command;

import io.argus.cli.command.TraceCommand.CallNode;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

class TraceCommandTest {

// -------------------------------------------------------------------------
// extractMatchingStack
// -------------------------------------------------------------------------

private static final String DUMP_WITH_TARGET =
"\"main\" #1 prio=5 os_prio=0 tid=0x00007f nid=0x1234 runnable\n" +
" java.lang.Thread.State: RUNNABLE\n" +
" at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:100)\n" +
" at com.example.OrderRepository.save(OrderRepository.java:45)\n" +
" at com.example.OrderService.createOrder(OrderService.java:22)\n" +
" at com.example.Controller.handle(Controller.java:10)\n" +
"\n" +
"\"GC task thread\" #2 daemon prio=9 os_prio=0 tid=0x00007f nid=0x5678 runnable\n" +
" java.lang.Thread.State: RUNNABLE\n" +
" at java.lang.Object.wait(Object.java:1)\n";

@Test
void extractMatchingStack_findsTargetFrame() {
List<String> stack = TraceCommand.extractMatchingStack(
DUMP_WITH_TARGET, "com.example.OrderService", "createOrder");
assertNotNull(stack, "Should find matching stack");
assertFalse(stack.isEmpty());
// First element should be the target method
assertTrue(stack.get(0).startsWith("com.example.OrderService.createOrder"),
"First frame should be the target method");
}

@Test
void extractMatchingStack_includesCalleeFrames() {
List<String> stack = TraceCommand.extractMatchingStack(
DUMP_WITH_TARGET, "com.example.OrderService", "createOrder");
assertNotNull(stack);
// Should include callee frames (innermost first)
assertTrue(stack.size() >= 3, "Should include callee frames");
assertTrue(stack.stream().anyMatch(f -> f.startsWith("com.example.OrderRepository.save")));
assertTrue(stack.stream().anyMatch(f -> f.startsWith("org.hibernate.internal.SessionImpl.merge")));
}

@Test
void extractMatchingStack_returnsNullWhenNoMatch() {
List<String> stack = TraceCommand.extractMatchingStack(
DUMP_WITH_TARGET, "com.example.NonExistent", "missing");
assertNull(stack, "Should return null when target not found");
}

@Test
void extractMatchingStack_handlesEmptyDump() {
assertNull(TraceCommand.extractMatchingStack("", "com.example.Foo", "bar"));
assertNull(TraceCommand.extractMatchingStack(null, "com.example.Foo", "bar"));
}

@Test
void extractMatchingStack_matchesOnlyTargetThread() {
// GC thread does not contain the target — only main thread does
List<String> stack = TraceCommand.extractMatchingStack(
DUMP_WITH_TARGET, "com.example.OrderService", "createOrder");
assertNotNull(stack);
// Should not contain Object.wait from GC thread
assertTrue(stack.stream().noneMatch(f -> f.startsWith("java.lang.Object.wait")));
}

// -------------------------------------------------------------------------
// buildCallTree
// -------------------------------------------------------------------------

@Test
void buildCallTree_singleStack() {
List<List<String>> stacks = List.of(
List.of("com.example.OrderService.createOrder(OrderService.java:22)",
"com.example.OrderRepository.save(OrderRepository.java:45)")
);
CallNode root = TraceCommand.buildCallTree(stacks);
assertEquals(1, root.children.size());
CallNode orderServiceNode = root.children.values().iterator().next();
assertEquals(1, orderServiceNode.hits);
assertEquals(1, orderServiceNode.children.size());
}

@Test
void buildCallTree_aggregatesRepeatedStacks() {
List<List<String>> stacks = List.of(
List.of("com.example.OrderService.createOrder(OrderService.java:22)",
"com.example.OrderRepository.save(OrderRepository.java:45)"),
List.of("com.example.OrderService.createOrder(OrderService.java:22)",
"com.example.OrderRepository.save(OrderRepository.java:45)"),
List.of("com.example.OrderService.createOrder(OrderService.java:22)",
"com.example.Validator.validate(Validator.java:10)")
);
CallNode root = TraceCommand.buildCallTree(stacks);
CallNode target = root.children.values().iterator().next();
assertEquals(3, target.hits, "Root target should have 3 hits");
// Should have 2 distinct children
assertEquals(2, target.children.size());
}

@Test
void buildCallTree_emptyStacks() {
CallNode root = TraceCommand.buildCallTree(List.of());
assertTrue(root.children.isEmpty());
}

// -------------------------------------------------------------------------
// shortenFrame
// -------------------------------------------------------------------------

@Test
void shortenFrame_shortFrameUnchanged() {
String frame = "com.example.Foo.bar(Foo.java:10)";
assertEquals(frame, TraceCommand.shortenFrame(frame, 100));
}

@Test
void shortenFrame_stripsSourceInfo() {
String frame = "com.example.OrderService.createOrder(OrderService.java:22)";
String shortened = TraceCommand.shortenFrame(frame, 40);
assertFalse(shortened.contains("(OrderService.java"), "Should strip source file info");
assertTrue(shortened.length() <= 40);
}

@Test
void shortenFrame_truncatesWithEllipsis() {
String frame = "com.example.very.long.package.name.OrderService.createOrder";
String shortened = TraceCommand.shortenFrame(frame, 20);
assertTrue(shortened.length() <= 20);
assertTrue(shortened.startsWith("\u2026"), "Should start with ellipsis when truncated");
}
}
3 changes: 2 additions & 1 deletion completions/argus.bash
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ _argus_completions() {
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"

commands="alert init ps histo threads gc gcutil heap sysprops vmflag nmt classloader profile jfr jfranalyze diff report doctor gclog gclogdiff gcprofile flame suggest watch tui info heapdump heapanalyze deadlock threaddump buffers gcrun logger events compilerqueue sc env compiler finalizer stringtable pool gccause metaspace dynlibs vmset vmlog jmx classstat gcnew symboltable top perfcounter mbean ci compare slowlog explain"
commands="alert init ps histo threads gc gcutil heap sysprops vmflag nmt classloader profile jfr jfranalyze diff report doctor gclog gclogdiff gcprofile flame suggest watch tui info heapdump heapanalyze deadlock threaddump buffers gcrun logger events compilerqueue sc env compiler finalizer stringtable pool gccause metaspace dynlibs vmset vmlog jmx classstat gcnew symboltable top perfcounter mbean ci compare slowlog explain trace"

if [ "$COMP_CWORD" -eq 1 ]; then
COMPREPLY=($(compgen -W "$commands --help --version" -- "$cur"))
Expand Down Expand Up @@ -43,6 +43,7 @@ _argus_completions() {
diff) opts="$opts --top=" ;;
alert) opts="$opts --config= --gc-overhead= --leak --webhook= --interval=" ;;
top) opts="$opts --host= --port= --interval=" ;;
trace) opts="$opts --duration=" ;;
esac
COMPREPLY=($(compgen -W "$opts" -- "$cur"))
fi
Expand Down
4 changes: 4 additions & 0 deletions completions/argus.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ _argus() {
'compare:Compare two JVM snapshots'
'slowlog:Real-time slow method detection'
'explain:Explain JVM metrics, GC causes, and flags in plain English'
'trace:Method execution tracing via rapid thread sampling'
)

_arguments -C \
Expand Down Expand Up @@ -104,6 +105,9 @@ _argus() {
gcutil)
_arguments '--watch=[Refresh interval]'
;;
trace)
_arguments '--duration=[Duration in seconds]'
;;
esac
;;
esac
Expand Down
Loading