66import static net .bytebuddy .matcher .ElementMatchers .nameStartsWith ;
77import static net .bytebuddy .matcher .ElementMatchers .not ;
88
9- import java .io .File ;
109import java .io .IOException ;
1110import java .io .PrintStream ;
1211import java .lang .instrument .Instrumentation ;
2221import net .bytebuddy .matcher .ElementMatcher ;
2322
2423/**
25- * Java agent entrypoint.
24+ * The Java flow agent entry point. It records method call trees for target classes and writes them
25+ * to files in a supplied directory. The directory will contain a method ID mapping file and a call
26+ * tree file for each thread. The call tree file format can be configured using the format argument,
27+ * which currently supports two formats: a compact binary format (.flow) and a more verbose JSON
28+ * Lines format (.jsonl). Both formats consist of two types of events: method entry and method exit.
29+ * Method entries are recorded with the ID of the entered method, which can be found in the method
30+ * ID mapping.
2631 *
27- * <p>agentArgs are comma-separated key=value pairs, e.g. target=com.myapp.,out=/tmp/flow/
28- * <li>{@code target} is required.
29- * <li>{@code out} is required.
32+ * <p>Arguments are comma-separated key=value pairs. The supported arguments are:
33+ *
34+ * <ul>
35+ * <li>target (required) -> '+'-separated list of class name prefixes to instrument
36+ * <li>out (required) -> output directory, will contain method ID mapping and per-thread call tree
37+ * files
38+ * <li>format (optional) -> "binary" (default) or "jsonl"
39+ * <li>optimize (optional) -> path to flow directory to optimize method ID mapping
40+ * <li>ids (optional) -> path to existing method ID mapping file
41+ * </ul>
42+ *
43+ * <pre>
44+ * <code class="language-properties">
45+ * Minimal example: target=com.myapp.,out=/tmp/flow/
46+ * </code>
47+ * </pre>
48+ *
49+ * Use the {@code optimize} argument to optimize method IDs by leveraging an existing mapping of
50+ * method IDs and flow files. Method IDs are natural numbers. Those that are called more frequently
51+ * will have smaller IDs. Using the {@code binary} format with variable-length integer encoding
52+ * significantly reduces the size of the recorded call tree data. The optimized mapping will be
53+ * written to the output directory and can be reused in subsequent runs by specifying its location
54+ * using the {@code ids} argument. Refer to the {@link MethodIdRemapper} class comment for more
55+ * details about the optimization process.
56+ *
57+ * <pre>
58+ * <code class="language-properties">
59+ * Example with optimization: target=com.myapp.,optimize=/tmp/flow/,out=/tmp/optimized-flow/
60+ * Then reuse optimized mapping: target=com.myapp.,ids=/tmp/optimized-flow/ids.properties,out=/tmp/optimized-flow-2/
61+ * </code>
62+ * </pre>
3063 */
3164public class AgentMain {
3265
@@ -41,12 +74,12 @@ public static void agentmain(String agentArgs, Instrumentation inst) {
4174 private static void init (String agentArgs , Instrumentation inst ) {
4275 // === parse arguments ===
4376
44- final Map <String , String > args = parseAgentArgs (agentArgs );
77+ final Map <String , String > args = parseArgs (agentArgs );
4578 final String target = args .get ("target" );
4679 final String outputPath = args .get ("out" );
4780 final String format = args .getOrDefault ("format" , "binary" );
4881 final String optimizePath = args .get ("optimize" );
49- String registryPath = args .get ("registry " );
82+ String mappingPath = args .get ("ids " ); // can be overwritten if optimize is used
5083
5184 if (target .isEmpty ()) {
5285 System .err .println (
@@ -65,9 +98,9 @@ private static void init(String agentArgs, Instrumentation inst) {
6598 // === process arguments ===
6699
67100 // target classes
68- ElementMatcher <TypeDescription > typeMatcher = typeMatcher (target );
101+ final ElementMatcher <TypeDescription > typeMatcher = typeMatcher (target );
69102 if (typeMatcher == null ) {
70- System .err .println ("[flow-agent] No valid prefixes found in 'target' argument." );
103+ System .err .println ("[flow-agent] No prefixes found in 'target' argument." );
71104 return ;
72105 }
73106 System .out .println ("[flow-agent] Instrumenting classes starting with: " + target );
@@ -92,32 +125,30 @@ private static void init(String agentArgs, Instrumentation inst) {
92125 };
93126 System .out .println ("[flow-agent] Using call tree format: " + format );
94127
95- // optimize method ID registry , set or overwrite 'registryPath ' argument
128+ // optimize method ID mapping , set or overwrite 'ids ' argument
96129 if (optimizePath != null ) {
97130 try {
98- Path optimizedRegistry =
99- MethodIdRemapper .optimizeFromFlowDirectory (Paths .get (optimizePath ), outputDir );
100- registryPath = optimizedRegistry .toAbsolutePath ().normalize ().toString ();
101- System .out .println ("[flow-agent] Optimized method registry: " + registryPath );
131+ Path optimizedMapping = MethodIdRemapper .optimize (Paths .get (optimizePath ), outputDir );
132+ mappingPath = optimizedMapping .toAbsolutePath ().normalize ().toString ();
133+ System .out .println ("[flow-agent] Optimized method mapping: " + mappingPath );
102134 } catch (IOException e ) {
103- System .err .println ("[flow-agent] Failed to optimize method registry : " + e );
135+ System .err .println ("[flow-agent] Failed to optimize method mapping : " + e );
104136 }
105137 }
106138
107- // method ID registry
108- final MethodIdRegistry idRegistry ;
109- if (registryPath != null ) {
139+ // method ID mapping
140+ final MethodIdMapping idMapping ;
141+ if (mappingPath != null ) {
110142 try {
111- idRegistry = new MethodIdRegistry ( new File ( registryPath ));
143+ idMapping = new MethodIdMapping ( Paths . get ( mappingPath ));
112144 System .out .println (
113- "[flow-agent] Loaded " + idRegistry .size () + " method IDs from " + registryPath );
145+ "[flow-agent] Loaded " + idMapping .size () + " method IDs from " + mappingPath );
114146 } catch (IOException e ) {
115- System .err .println (
116- "[flow-agent] Failed to load method IDs from " + registryPath + ": " + e );
147+ System .err .println ("[flow-agent] Failed to load method IDs from " + mappingPath + ": " + e );
117148 return ;
118149 }
119150 } else {
120- idRegistry = new MethodIdRegistry ();
151+ idMapping = new MethodIdMapping ();
121152 }
122153
123154 // === recorder setup ===
@@ -129,7 +160,7 @@ private static void init(String agentArgs, Instrumentation inst) {
129160 e .printStackTrace ();
130161 }
131162
132- final String finalRegistryPath = registryPath ;
163+ final String finalMappingPath = mappingPath ;
133164
134165 // register shutdown hook to close recorder
135166 Runtime .getRuntime ()
@@ -139,8 +170,8 @@ private static void init(String agentArgs, Instrumentation inst) {
139170 try {
140171 Singletons .RECORDER .close ();
141172
142- if (finalRegistryPath == null ) {
143- idRegistry .dump (outputDir .resolve ("ids.properties" ));
173+ if (finalMappingPath == null ) {
174+ idMapping .dump (outputDir .resolve ("ids.properties" ));
144175 }
145176
146177 System .out .println ("[flow-agent] Flow written to " + outputDir );
@@ -154,7 +185,7 @@ private static void init(String agentArgs, Instrumentation inst) {
154185
155186 Advice advice =
156187 Advice .withCustomMapping ()
157- .bind (MethodId .class , new MethodIdOffsetMapping (idRegistry ))
188+ .bind (MethodId .class , new MethodIdOffsetMapping (idMapping ))
158189 .to (FlowAdvice .class );
159190
160191 ElementMatcher <MethodDescription > methodMatcher =
@@ -171,61 +202,54 @@ private static void init(String agentArgs, Instrumentation inst) {
171202 (builder , typeDescription , classLoader , module , protectionDomain ) ->
172203 builder .visit (advice .on (methodMatcher )));
173204
174- // agentBuilder = agentBuilder.with(AgentBuilder.Listener.StreamWriting.toSystemOut());
205+ // agentBuilder =
206+ // agentBuilder.with(AgentBuilder.Listener.StreamWriting.toSystemOut());
175207
176208 agentBuilder .installOn (inst );
177209 }
178210
179211 /**
180- * Parse agentArgs using: - top-level pair separator: ',' - key/value separator: '='
212+ * Parse agent arguments into a dictionary using:
181213 *
182- * <p>Supported keys: - target (required) -> '+'-separated list - out (optional)
183- *
184- * <p>Example: target=com.myapp.+org.example.service,out=/tmp/cg
214+ * <ul>
215+ * <li>argument separator: ','
216+ * <li>key/value separator: '='
217+ * </ul>
185218 */
186- private static Map <String , String > parseAgentArgs (String agentArgs ) {
219+ private static Map <String , String > parseArgs (String args ) {
187220 Map <String , String > map = new HashMap <>();
188221
189- if (agentArgs == null || agentArgs .trim ().isEmpty ()) {
222+ if (args == null || args .trim ().isEmpty ()) {
190223 return map ;
191224 }
192225
193- String [] pairs = agentArgs .split ("," );
226+ String [] pairs = args .split ("," );
194227 for (String pair : pairs ) {
195228 String trimmed = pair .trim ();
196229 if (trimmed .isEmpty ()) continue ;
197230
198231 String [] kv = trimmed .split ("=" , 2 );
199232 if (kv .length != 2 ) {
200233 System .err .println (
201- "[flow-agent] Invalid agent argument entry (expected key=value): " + trimmed );
234+ "[flow-agent] Ignoring invalid agent argument entry (expected key=value): " + trimmed );
202235 continue ;
203236 }
204237
205238 String key = kv [0 ].trim ();
206239 String value = kv [1 ].trim ();
207240
208241 if (key .isEmpty () || value .isEmpty ()) {
209- System .err .println ("[flow-agent] Invalid key/value in agent argument: " + trimmed );
242+ System .err .println ("[flow-agent] Ignoring empty key/value in agent argument: " + trimmed );
210243 continue ;
211244 }
212245
213246 map .put (key , value );
214247 }
215248
216- if (!map .containsKey ("target" )) {
217- System .err .println ("[flow-agent] Missing required 'target' argument." );
218- }
219-
220249 return map ;
221250 }
222251
223252 private static ElementMatcher <TypeDescription > typeMatcher (String targetValue ) {
224- if (targetValue .isEmpty ()) {
225- System .err .println ("[flow-agent] 'target' is empty; nothing to instrument." );
226- return null ;
227- }
228-
229253 // split on '+' and build an OR matcher:
230254 // nameStartsWith(t1).or(nameStartsWith(t2))...
231255 String [] tokens = targetValue .split ("\\ +" );
@@ -243,6 +267,12 @@ private static ElementMatcher<TypeDescription> typeMatcher(String targetValue) {
243267 }
244268
245269 private static void printUsage (PrintStream out ) {
246- out .println ("[flow-agent] Provide agent arguments like: target=com.myapp.,out=/tmp/flow" );
270+ out .println (
271+ "[flow-agent] Usage: target=<prefix[+prefix...]>,out=<dir>[,format=binary|jsonl][,optimize=<dir>][,ids=<file>]" );
272+ out .println (" target : '+'-separated list of class name prefixes to instrument (required)" );
273+ out .println (" out : output directory for flow files and method ID mapping (required)" );
274+ out .println (" format : call tree format: 'binary' (default) or 'jsonl'" );
275+ out .println (" optimize : path to existing flow directory to optimize method IDs (optional)" );
276+ out .println (" ids : path to existing method ID mapping file to reuse IDs (optional)" );
247277 }
248278}
0 commit comments