Skip to content

Commit 79cb690

Browse files
Tweak and document
1 parent 5330202 commit 79cb690

11 files changed

Lines changed: 194 additions & 159 deletions

src/main/java/fr/bl/drit/flow/agent/AgentMain.java

Lines changed: 77 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;
77
import static net.bytebuddy.matcher.ElementMatchers.not;
88

9-
import java.io.File;
109
import java.io.IOException;
1110
import java.io.PrintStream;
1211
import java.lang.instrument.Instrumentation;
@@ -22,11 +21,45 @@
2221
import 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
*/
3164
public 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
}

src/main/java/fr/bl/drit/flow/agent/BinaryThreadRecorder.java

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,18 @@
1717
*
1818
* Where METHOD_ID refers to the ID of the method stored... TODO
1919
*/
20-
public class BinaryThreadRecorder implements Recorder {
20+
public class BinaryThreadRecorder implements ThreadRecorder {
21+
22+
// === Event flags ===
2123

22-
// === Event flags
2324
/** Highest bit is 1 (0x80). */
2425
public static final byte F_ENTER = (byte) 0x80;
2526

2627
/** Highest bit is 0 (0x00). */
2728
public static final byte F_EXIT = 0x00;
2829

29-
// === Masks
30+
// === Masks ===
31+
3032
/** Flag bit, the highest bit (0x80). */
3133
public static final byte M_FLAG = (byte) 0x80;
3234

@@ -36,7 +38,7 @@ public class BinaryThreadRecorder implements Recorder {
3638
/** Payload in a varint, the lowest 7 bits (0x7F). */
3739
public static final byte M_PAYLOAD = 0x7F;
3840

39-
/** Rest of payload to encode in following bytes in a varint, all but 7 lowest bits (~0x7FL). */
41+
/** Rest of payload to encode in following varint bytes, all but 7 lowest bits (~0x7FL). */
4042
public static final long M_PAYLOAD_REST = ~0x7FL;
4143

4244
/** Continuation bit in a packed varint, second highest bit (0x40). */
@@ -45,29 +47,34 @@ public class BinaryThreadRecorder implements Recorder {
4547
/** Payload in a packed varint, the lowest 6 bits (0x3F). */
4648
public static final byte M_PACK_PAYLOAD = 0x3F;
4749

50+
// === State ===
51+
52+
/** Output stream for the binary call tree data of the recorder's thread. */
4853
protected final OutputStream out;
4954

55+
protected final String fileName;
56+
5057
/** Additional consecutive exits, 0 means exactly one exit. */
5158
protected long pendingExits = 0L;
5259

53-
// For stats
54-
protected long invocations = 0L;
55-
56-
public BinaryThreadRecorder(Path output) throws IOException {
57-
out =
60+
public BinaryThreadRecorder(Path outputDir) throws IOException {
61+
this.fileName = "thread-" + Thread.currentThread().getId() + ".flow";
62+
this.out =
5863
new BufferedOutputStream(
5964
Files.newOutputStream(
60-
output,
65+
outputDir.resolve(this.fileName),
6166
StandardOpenOption.CREATE,
6267
StandardOpenOption.WRITE,
6368
StandardOpenOption.TRUNCATE_EXISTING),
6469
64 * 1024);
6570
}
6671

72+
public String getFileName() {
73+
return fileName;
74+
}
75+
6776
@Override
6877
public void enter(long methodId) throws IOException {
69-
invocations++; // only for stats
70-
7178
flushPendingExits();
7279
writeFlagAndVarInt(F_ENTER, methodId);
7380
}

src/main/java/fr/bl/drit/flow/agent/JsonlThreadRecorder.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,31 @@
1414
*
1515
* <p>Each line is either: - {"e":"enter","method":"<id>"} - {"e":"exit"}
1616
*/
17-
public final class JsonlThreadRecorder implements Recorder {
17+
public final class JsonlThreadRecorder implements ThreadRecorder {
1818

19+
/** Writer for the call tree data of the recorder's thread. */
1920
private final Writer out;
2021

21-
public JsonlThreadRecorder(Path output) throws IOException {
22+
private final String fileName;
23+
24+
public JsonlThreadRecorder(Path outputDir) throws IOException {
25+
this.fileName = "thread-" + Thread.currentThread().getId() + ".jsonl";
2226
this.out =
2327
new BufferedWriter(
2428
new OutputStreamWriter(
2529
Files.newOutputStream(
26-
output,
30+
outputDir.resolve(this.fileName),
2731
StandardOpenOption.CREATE,
2832
StandardOpenOption.WRITE,
2933
StandardOpenOption.TRUNCATE_EXISTING),
3034
StandardCharsets.UTF_8),
3135
64 * 1024);
3236
}
3337

38+
public String getFileName() {
39+
return fileName;
40+
}
41+
3442
@Override
3543
public void enter(long methodId) throws IOException {
3644
StringBuilder sb = new StringBuilder(128);

0 commit comments

Comments
 (0)