Skip to content

Commit 7388781

Browse files
authored
Merge pull request #148 from rlaope/feat/argus-trace
feat: argus trace — method execution tracing (#130)
2 parents d0a8349 + d64b28e commit 7388781

9 files changed

Lines changed: 583 additions & 1 deletion

File tree

argus-cli/src/main/java/io/argus/cli/ArgusCli.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import io.argus.cli.command.ThreadDumpCommand;
5757
import io.argus.cli.command.ThreadsCommand;
5858
import io.argus.cli.command.TopCommand;
59+
import io.argus.cli.command.TraceCommand;
5960
import io.argus.cli.command.TuiCommand;
6061
import io.argus.cli.command.VmFlagCommand;
6162
import io.argus.cli.command.VmLogCommand;
@@ -210,6 +211,7 @@ public static void main(String[] args) {
210211
register(commands, new PerfCounterCommand());
211212
register(commands, new MBeanCommand());
212213
register(commands, new TopCommand());
214+
register(commands, new TraceCommand());
213215
register(commands, new WatchCommand());
214216
register(commands, new ExplainCommand());
215217
register(commands, new TuiCommand(commands));

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

Lines changed: 409 additions & 0 deletions
Large diffs are not rendered by default.

argus-cli/src/main/resources/messages_en.properties

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,3 +354,10 @@ error.profile.invalid.type=Invalid profiling type: %s. Use: cpu, alloc, lock, wa
354354
gcnew.age.title=Object Age Distribution
355355
gcnew.age.unavailable=Age data unavailable. Enable -Xlog:gc+age=debug for live data.
356356
cmd.gclog.tenuring.desc=Analyze tenuring threshold changes from GC age log
357+
cmd.trace.desc=Method execution tracing via rapid thread sampling
358+
header.trace=Method Trace
359+
desc.trace=Traces method execution by sampling thread dumps at 10/sec. Builds a call tree with timing estimates.
360+
status.trace.sampling=Tracing PID %s for %ss (%s samples)...
361+
status.trace.no.match=No stack traces matched '%s' in %s samples
362+
error.trace.invalid.target=Invalid target format: '%s'. Use class.method (e.g. com.example.OrderService.createOrder)
363+
trace.summary=%s samples, %s matched target method '%s'

argus-cli/src/main/resources/messages_ja.properties

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,3 +338,10 @@ error.profile.invalid.type=\u7121\u52B9\u306A\u30D7\u30ED\u30D5\u30A1\u30A4\u30E
338338
gcnew.age.title=\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u5E74\u9F62\u5206\u5E03
339339
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
340340
cmd.gclog.tenuring.desc=GC\u30A8\u30FC\u30B8\u30ED\u30B0\u304B\u3089\u30C6\u30CB\u30E5\u30A2\u30EA\u30F3\u30B0\u5909\u5316\u3092\u5206\u6790
341+
cmd.trace.desc=高速スレッドサンプリングによるメソッド実行トレース
342+
header.trace=メソッドトレース
343+
desc.trace=秒間10回のスレッドダンプサンプリングでメソッド実行を追跡します。タイミング推定付きのコールツリーを生成します。
344+
status.trace.sampling=PID %sを%s秒間トレース中 (%sサンプル)...
345+
status.trace.no.match=%sサンプルで'%s'に一致するスタックトレースが見つかりません
346+
error.trace.invalid.target=無効なターゲット形式: '%s'。class.method形式を使用してください (例: com.example.OrderService.createOrder)
347+
trace.summary=%sサンプル、%s件がターゲットメソッド'%s'に一致

argus-cli/src/main/resources/messages_ko.properties

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,3 +338,10 @@ error.profile.invalid.type=\uC798\uBABB\uB41C \uD504\uB85C\uD30C\uC77C\uB9C1 \uD
338338
gcnew.age.title=\uAC1D\uCCB4 \uC5F0\uB839 \uBD84\uD3EC
339339
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.
340340
cmd.gclog.tenuring.desc=GC \uC5D0\uC774\uC9C0 \uB85C\uADF8\uC5D0\uC11C \uD14C\uB274\uC5B4\uB9C1 \uBCC0\uD654 \uBD84\uC11D
341+
cmd.trace.desc=고속 스레드 샘플링을 통한 메서드 실행 추적
342+
header.trace=메서드 추적
343+
desc.trace=초당 10회 스레드 덤프 샘플링으로 메서드 실행을 추적합니다. 타이밍 추정치와 함께 호출 트리를 생성합니다.
344+
status.trace.sampling=PID %s를 %s초 동안 추적 중 (%s개 샘플)...
345+
status.trace.no.match=%s개 샘플에서 '%s'와 일치하는 스택 트레이스가 없습니다
346+
error.trace.invalid.target=잘못된 대상 형식: '%s'. class.method 형식을 사용하세요 (예: com.example.OrderService.createOrder)
347+
trace.summary=%s개 샘플, %s개가 대상 메서드 '%s'와 일치

argus-cli/src/main/resources/messages_zh.properties

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,3 +335,10 @@ error.profile.unsupported.platform=\u6B64\u5E73\u53F0\u4E0D\u652F\u6301async-pro
335335
error.profile.download.failed=async-profiler\u4E0B\u8F7D\u5931\u8D25: %s
336336
error.profile.asprof.failed=async-profiler\u5931\u8D25: %s
337337
error.profile.invalid.type=\u65E0\u6548\u7684\u5206\u6790\u7C7B\u578B: %s\u3002\u8BF7\u4F7F\u7528: cpu, alloc, lock, wall
338+
cmd.trace.desc=通过高速线程采样进行方法执行追踪
339+
header.trace=方法追踪
340+
desc.trace=以每秒10次的频率采样线程转储来追踪方法执行,生成带时间估算的调用树。
341+
status.trace.sampling=正在追踪PID %s,持续%s秒(%s个样本)...
342+
status.trace.no.match=在%s个样本中未找到匹配'%s'的堆栈跟踪
343+
error.trace.invalid.target=无效的目标格式:'%s'。请使用class.method格式(例如:com.example.OrderService.createOrder)
344+
trace.summary=%s个样本,%s个匹配目标方法'%s'
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package io.argus.cli.command;
2+
3+
import io.argus.cli.command.TraceCommand.CallNode;
4+
import org.junit.jupiter.api.Test;
5+
6+
import java.util.List;
7+
8+
import static org.junit.jupiter.api.Assertions.*;
9+
10+
class TraceCommandTest {
11+
12+
// -------------------------------------------------------------------------
13+
// extractMatchingStack
14+
// -------------------------------------------------------------------------
15+
16+
private static final String DUMP_WITH_TARGET =
17+
"\"main\" #1 prio=5 os_prio=0 tid=0x00007f nid=0x1234 runnable\n" +
18+
" java.lang.Thread.State: RUNNABLE\n" +
19+
" at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:100)\n" +
20+
" at com.example.OrderRepository.save(OrderRepository.java:45)\n" +
21+
" at com.example.OrderService.createOrder(OrderService.java:22)\n" +
22+
" at com.example.Controller.handle(Controller.java:10)\n" +
23+
"\n" +
24+
"\"GC task thread\" #2 daemon prio=9 os_prio=0 tid=0x00007f nid=0x5678 runnable\n" +
25+
" java.lang.Thread.State: RUNNABLE\n" +
26+
" at java.lang.Object.wait(Object.java:1)\n";
27+
28+
@Test
29+
void extractMatchingStack_findsTargetFrame() {
30+
List<String> stack = TraceCommand.extractMatchingStack(
31+
DUMP_WITH_TARGET, "com.example.OrderService", "createOrder");
32+
assertNotNull(stack, "Should find matching stack");
33+
assertFalse(stack.isEmpty());
34+
// First element should be the target method
35+
assertTrue(stack.get(0).startsWith("com.example.OrderService.createOrder"),
36+
"First frame should be the target method");
37+
}
38+
39+
@Test
40+
void extractMatchingStack_includesCalleeFrames() {
41+
List<String> stack = TraceCommand.extractMatchingStack(
42+
DUMP_WITH_TARGET, "com.example.OrderService", "createOrder");
43+
assertNotNull(stack);
44+
// Should include callee frames (innermost first)
45+
assertTrue(stack.size() >= 3, "Should include callee frames");
46+
assertTrue(stack.stream().anyMatch(f -> f.startsWith("com.example.OrderRepository.save")));
47+
assertTrue(stack.stream().anyMatch(f -> f.startsWith("org.hibernate.internal.SessionImpl.merge")));
48+
}
49+
50+
@Test
51+
void extractMatchingStack_returnsNullWhenNoMatch() {
52+
List<String> stack = TraceCommand.extractMatchingStack(
53+
DUMP_WITH_TARGET, "com.example.NonExistent", "missing");
54+
assertNull(stack, "Should return null when target not found");
55+
}
56+
57+
@Test
58+
void extractMatchingStack_handlesEmptyDump() {
59+
assertNull(TraceCommand.extractMatchingStack("", "com.example.Foo", "bar"));
60+
assertNull(TraceCommand.extractMatchingStack(null, "com.example.Foo", "bar"));
61+
}
62+
63+
@Test
64+
void extractMatchingStack_matchesOnlyTargetThread() {
65+
// GC thread does not contain the target — only main thread does
66+
List<String> stack = TraceCommand.extractMatchingStack(
67+
DUMP_WITH_TARGET, "com.example.OrderService", "createOrder");
68+
assertNotNull(stack);
69+
// Should not contain Object.wait from GC thread
70+
assertTrue(stack.stream().noneMatch(f -> f.startsWith("java.lang.Object.wait")));
71+
}
72+
73+
// -------------------------------------------------------------------------
74+
// buildCallTree
75+
// -------------------------------------------------------------------------
76+
77+
@Test
78+
void buildCallTree_singleStack() {
79+
List<List<String>> stacks = List.of(
80+
List.of("com.example.OrderService.createOrder(OrderService.java:22)",
81+
"com.example.OrderRepository.save(OrderRepository.java:45)")
82+
);
83+
CallNode root = TraceCommand.buildCallTree(stacks);
84+
assertEquals(1, root.children.size());
85+
CallNode orderServiceNode = root.children.values().iterator().next();
86+
assertEquals(1, orderServiceNode.hits);
87+
assertEquals(1, orderServiceNode.children.size());
88+
}
89+
90+
@Test
91+
void buildCallTree_aggregatesRepeatedStacks() {
92+
List<List<String>> stacks = List.of(
93+
List.of("com.example.OrderService.createOrder(OrderService.java:22)",
94+
"com.example.OrderRepository.save(OrderRepository.java:45)"),
95+
List.of("com.example.OrderService.createOrder(OrderService.java:22)",
96+
"com.example.OrderRepository.save(OrderRepository.java:45)"),
97+
List.of("com.example.OrderService.createOrder(OrderService.java:22)",
98+
"com.example.Validator.validate(Validator.java:10)")
99+
);
100+
CallNode root = TraceCommand.buildCallTree(stacks);
101+
CallNode target = root.children.values().iterator().next();
102+
assertEquals(3, target.hits, "Root target should have 3 hits");
103+
// Should have 2 distinct children
104+
assertEquals(2, target.children.size());
105+
}
106+
107+
@Test
108+
void buildCallTree_emptyStacks() {
109+
CallNode root = TraceCommand.buildCallTree(List.of());
110+
assertTrue(root.children.isEmpty());
111+
}
112+
113+
// -------------------------------------------------------------------------
114+
// shortenFrame
115+
// -------------------------------------------------------------------------
116+
117+
@Test
118+
void shortenFrame_shortFrameUnchanged() {
119+
String frame = "com.example.Foo.bar(Foo.java:10)";
120+
assertEquals(frame, TraceCommand.shortenFrame(frame, 100));
121+
}
122+
123+
@Test
124+
void shortenFrame_stripsSourceInfo() {
125+
String frame = "com.example.OrderService.createOrder(OrderService.java:22)";
126+
String shortened = TraceCommand.shortenFrame(frame, 40);
127+
assertFalse(shortened.contains("(OrderService.java"), "Should strip source file info");
128+
assertTrue(shortened.length() <= 40);
129+
}
130+
131+
@Test
132+
void shortenFrame_truncatesWithEllipsis() {
133+
String frame = "com.example.very.long.package.name.OrderService.createOrder";
134+
String shortened = TraceCommand.shortenFrame(frame, 20);
135+
assertTrue(shortened.length() <= 20);
136+
assertTrue(shortened.startsWith("\u2026"), "Should start with ellipsis when truncated");
137+
}
138+
}

completions/argus.bash

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ _argus_completions() {
33
cur="${COMP_WORDS[COMP_CWORD]}"
44
prev="${COMP_WORDS[COMP_CWORD-1]}"
55

6-
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"
6+
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"
77

88
if [ "$COMP_CWORD" -eq 1 ]; then
99
COMPREPLY=($(compgen -W "$commands --help --version" -- "$cur"))
@@ -43,6 +43,7 @@ _argus_completions() {
4343
diff) opts="$opts --top=" ;;
4444
alert) opts="$opts --config= --gc-overhead= --leak --webhook= --interval=" ;;
4545
top) opts="$opts --host= --port= --interval=" ;;
46+
trace) opts="$opts --duration=" ;;
4647
esac
4748
COMPREPLY=($(compgen -W "$opts" -- "$cur"))
4849
fi

completions/argus.zsh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ _argus() {
5959
'compare:Compare two JVM snapshots'
6060
'slowlog:Real-time slow method detection'
6161
'explain:Explain JVM metrics, GC causes, and flags in plain English'
62+
'trace:Method execution tracing via rapid thread sampling'
6263
)
6364

6465
_arguments -C \
@@ -104,6 +105,9 @@ _argus() {
104105
gcutil)
105106
_arguments '--watch=[Refresh interval]'
106107
;;
108+
trace)
109+
_arguments '--duration=[Duration in seconds]'
110+
;;
107111
esac
108112
;;
109113
esac

0 commit comments

Comments
 (0)