Skip to content

Commit 0772398

Browse files
committed
perf: optimize Java tracing agent serialization and writes
- Reuse ThreadLocal Kryo Output buffers (eliminates #1 allocation hotspot) - Fast-path inline serialization for safe arg types (bypasses executor) - Skip verification roundtrip for known-safe containers (ArrayList, HashMap, etc.) - Batch SQLite inserts (256/txn) with permanent autocommit-off - Switch to ArrayBlockingQueue (no per-element Node allocation) - Add opt-in in-memory SQLite mode (VACUUM INTO at shutdown), enabled in CI - Add timing instrumentation (onEntry, serialization, writes, dump) - Add ProfilingWorkload fixture for benchmarking Benchmark (50k captures): onEntry 5200ms→1200ms (4.3x), avg/capture 0.43ms→0.02ms (21x), writes 3200ms→900ms (3.5x) with in-memory mode.
1 parent 08aa94c commit 0772398

7 files changed

Lines changed: 355 additions & 67 deletions

File tree

codeflash-java-runtime/src/main/java/com/codeflash/Serializer.java

Lines changed: 101 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import com.esotericsoftware.kryo.util.DefaultInstantiatorStrategy;
77
import org.objenesis.strategy.StdInstantiatorStrategy;
88

9-
import java.io.ByteArrayOutputStream;
109
import java.io.InputStream;
1110
import java.io.OutputStream;
1211
import java.lang.reflect.Field;
@@ -36,7 +35,11 @@ public final class Serializer {
3635
private static final int MAX_COLLECTION_SIZE = 1000;
3736
private static final int BUFFER_SIZE = 4096;
3837

39-
// Thread-local Kryo instances (Kryo is not thread-safe)
38+
// Thread-local Kryo, Output, and IdentityHashMap instances for reuse
39+
private static final ThreadLocal<Output> OUTPUT = ThreadLocal.withInitial(() -> new Output(BUFFER_SIZE, -1));
40+
private static final ThreadLocal<IdentityHashMap<Object, Object>> SEEN =
41+
ThreadLocal.withInitial(IdentityHashMap::new);
42+
4043
private static final ThreadLocal<Kryo> KRYO = ThreadLocal.withInitial(() -> {
4144
Kryo kryo = new Kryo();
4245
kryo.setRegistrationRequired(false);
@@ -89,10 +92,78 @@ private Serializer() {
8992
* @return Serialized bytes (may contain KryoPlaceholder for unserializable parts)
9093
*/
9194
public static byte[] serialize(Object obj) {
92-
Object processed = recursiveProcess(obj, new IdentityHashMap<>(), 0, "");
95+
// Fast path: if args are all safe types, skip recursive processing entirely
96+
if (obj instanceof Object[] && isSafeArgs((Object[]) obj)) {
97+
return directSerialize(obj);
98+
}
99+
100+
IdentityHashMap<Object, Object> seen = SEEN.get();
101+
seen.clear();
102+
Object processed = recursiveProcess(obj, seen, 0, "");
93103
return directSerialize(processed);
94104
}
95105

106+
/**
107+
* Attempt fast-path serialization for args that are all known-safe types.
108+
* Returns serialized bytes if all args are safe, or null if the slow path is needed.
109+
* Callers can use this to avoid executor submission overhead for simple arguments.
110+
*/
111+
public static byte[] serializeFast(Object obj) {
112+
if (obj instanceof Object[] && isSafeArgs((Object[]) obj)) {
113+
return directSerialize(obj);
114+
}
115+
return null;
116+
}
117+
118+
/**
119+
* Check if all elements of an args array can be serialized directly without recursive processing.
120+
*/
121+
private static boolean isSafeArgs(Object[] args) {
122+
for (Object arg : args) {
123+
if (!isSafeForDirectSerialization(arg)) {
124+
return false;
125+
}
126+
}
127+
return true;
128+
}
129+
130+
/**
131+
* Check if an object is safe to serialize directly without recursive processing.
132+
* Covers: null, simple types, primitive arrays, and safe containers (up to 3 levels deep).
133+
*/
134+
private static boolean isSafeForDirectSerialization(Object obj) {
135+
return isSafeForDirectSerialization(obj, 3);
136+
}
137+
138+
private static boolean isSafeForDirectSerialization(Object obj, int depthLeft) {
139+
if (obj == null || isSimpleType(obj)) {
140+
return true;
141+
}
142+
if (depthLeft <= 0) {
143+
return false;
144+
}
145+
Class<?> clazz = obj.getClass();
146+
if (clazz.isArray() && clazz.getComponentType().isPrimitive()) {
147+
return true;
148+
}
149+
if (isSafeContainerType(clazz)) {
150+
if (obj instanceof Collection) {
151+
for (Object item : (Collection<?>) obj) {
152+
if (!isSafeForDirectSerialization(item, depthLeft - 1)) return false;
153+
}
154+
return true;
155+
}
156+
if (obj instanceof Map) {
157+
for (Map.Entry<?, ?> e : ((Map<?, ?>) obj).entrySet()) {
158+
if (!isSafeForDirectSerialization(e.getKey(), depthLeft - 1) ||
159+
!isSafeForDirectSerialization(e.getValue(), depthLeft - 1)) return false;
160+
}
161+
return true;
162+
}
163+
}
164+
return false;
165+
}
166+
96167
/**
97168
* Deserialize bytes back to an object.
98169
* The returned object may contain KryoPlaceholder instances for parts
@@ -141,14 +212,15 @@ public static byte[] serializeException(Throwable error) {
141212

142213
/**
143214
* Direct serialization without recursive processing.
215+
* Reuses a ThreadLocal Output buffer to avoid per-call allocation.
144216
*/
145217
private static byte[] directSerialize(Object obj) {
146218
Kryo kryo = KRYO.get();
147-
ByteArrayOutputStream baos = new ByteArrayOutputStream(BUFFER_SIZE);
148-
try (Output output = new Output(baos)) {
149-
kryo.writeClassAndObject(output, obj);
150-
}
151-
return baos.toByteArray();
219+
Output output = OUTPUT.get();
220+
output.reset();
221+
kryo.writeClassAndObject(output, obj);
222+
output.flush();
223+
return output.toBytes();
152224
}
153225

154226
/**
@@ -201,37 +273,23 @@ private static Object recursiveProcess(Object obj, IdentityHashMap<Object, Objec
201273
// unserializable types, recursively process to catch and replace unserializable objects.
202274
if (obj instanceof Map) {
203275
Map<?, ?> map = (Map<?, ?>) obj;
204-
if (containsOnlySimpleTypes(map)) {
205-
// Simple map - try direct serialization to preserve full size
206-
byte[] serialized = tryDirectSerialize(obj);
207-
if (serialized != null) {
208-
try {
209-
deserialize(serialized);
210-
return obj; // Success - return original
211-
} catch (Exception e) {
212-
// Fall through to recursive handling
213-
}
214-
}
276+
if (isSafeContainerType(clazz) && containsOnlySimpleTypes(map)) {
277+
return obj;
215278
}
216279
return handleMap(map, seen, depth, path);
217280
}
218281
if (obj instanceof Collection) {
219282
Collection<?> collection = (Collection<?>) obj;
220-
if (containsOnlySimpleTypes(collection)) {
221-
// Simple collection - try direct serialization to preserve full size
222-
byte[] serialized = tryDirectSerialize(obj);
223-
if (serialized != null) {
224-
try {
225-
deserialize(serialized);
226-
return obj; // Success - return original
227-
} catch (Exception e) {
228-
// Fall through to recursive handling
229-
}
230-
}
283+
if (isSafeContainerType(clazz) && containsOnlySimpleTypes(collection)) {
284+
return obj;
231285
}
232286
return handleCollection(collection, seen, depth, path);
233287
}
234288
if (clazz.isArray()) {
289+
// Primitive arrays (int[], double[], etc.) are directly serializable by Kryo
290+
if (clazz.getComponentType().isPrimitive()) {
291+
return obj;
292+
}
235293
return handleArray(obj, seen, depth, path);
236294
}
237295

@@ -255,6 +313,19 @@ private static Object recursiveProcess(Object obj, IdentityHashMap<Object, Objec
255313
}
256314
}
257315

316+
/**
317+
* Check if a container type is known to round-trip safely through Kryo without verification.
318+
* Only includes types registered with Kryo that are known to serialize/deserialize correctly.
319+
*/
320+
private static boolean isSafeContainerType(Class<?> clazz) {
321+
return clazz == ArrayList.class ||
322+
clazz == LinkedList.class ||
323+
clazz == HashMap.class ||
324+
clazz == LinkedHashMap.class ||
325+
clazz == HashSet.class ||
326+
clazz == LinkedHashSet.class;
327+
}
328+
258329
/**
259330
* Check if a class is known to be unserializable.
260331
*/

codeflash-java-runtime/src/main/java/com/codeflash/tracer/TraceRecorder.java

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.util.concurrent.TimeUnit;
1313
import java.util.concurrent.TimeoutException;
1414
import java.util.concurrent.atomic.AtomicInteger;
15+
import java.util.concurrent.atomic.AtomicLong;
1516

1617
public final class TraceRecorder {
1718

@@ -23,6 +24,8 @@ public final class TraceRecorder {
2324
private final TraceWriter writer;
2425
private final ConcurrentHashMap<String, AtomicInteger> functionCounts = new ConcurrentHashMap<>();
2526
private final AtomicInteger droppedCaptures = new AtomicInteger(0);
27+
private final AtomicLong totalOnEntryNs = new AtomicLong(0);
28+
private final AtomicLong totalSerializationNs = new AtomicLong(0);
2629
private final int maxFunctionCount;
2730
private final ExecutorService serializerExecutor;
2831

@@ -31,7 +34,7 @@ public final class TraceRecorder {
3134

3235
private TraceRecorder(TracerConfig config) {
3336
this.config = config;
34-
this.writer = new TraceWriter(config.getDbPath());
37+
this.writer = new TraceWriter(config.getDbPath(), config.isInMemoryDb());
3538
this.maxFunctionCount = config.getMaxFunctionCount();
3639
this.serializerExecutor = Executors.newCachedThreadPool(r -> {
3740
Thread t = new Thread(r, "codeflash-serializer");
@@ -68,6 +71,8 @@ public void onEntry(String className, String methodName, String descriptor,
6871

6972
private void onEntryImpl(String className, String methodName, String descriptor,
7073
int lineNumber, String sourceFile, Object[] args) {
74+
long entryStart = System.nanoTime();
75+
7176
String qualifiedName = className + "." + methodName + descriptor;
7277

7378
// Check per-method count limit
@@ -76,30 +81,38 @@ private void onEntryImpl(String className, String methodName, String descriptor,
7681
return;
7782
}
7883

79-
// Serialize args with timeout to prevent deep object graph traversal from blocking
84+
// Serialize args — try inline fast path first, fall back to async with timeout
8085
byte[] argsBlob;
81-
Future<byte[]> future = serializerExecutor.submit(() -> Serializer.serialize(args));
82-
try {
83-
argsBlob = future.get(SERIALIZATION_TIMEOUT_MS, TimeUnit.MILLISECONDS);
84-
} catch (TimeoutException e) {
85-
future.cancel(true);
86-
droppedCaptures.incrementAndGet();
87-
System.err.println("[codeflash-tracer] Serialization timed out for " + className + "."
88-
+ methodName);
89-
return;
90-
} catch (Exception e) {
91-
Throwable cause = e.getCause() != null ? e.getCause() : e;
92-
droppedCaptures.incrementAndGet();
93-
System.err.println("[codeflash-tracer] Serialization failed for " + className + "."
94-
+ methodName + ": " + cause.getClass().getSimpleName() + ": " + cause.getMessage());
95-
return;
86+
long serStart = System.nanoTime();
87+
argsBlob = Serializer.serializeFast(args);
88+
if (argsBlob == null) {
89+
// Slow path: async serialization with timeout for complex/unknown types
90+
Future<byte[]> future = serializerExecutor.submit(() -> Serializer.serialize(args));
91+
try {
92+
argsBlob = future.get(SERIALIZATION_TIMEOUT_MS, TimeUnit.MILLISECONDS);
93+
} catch (TimeoutException e) {
94+
future.cancel(true);
95+
droppedCaptures.incrementAndGet();
96+
System.err.println("[codeflash-tracer] Serialization timed out for " + className + "."
97+
+ methodName);
98+
return;
99+
} catch (Exception e) {
100+
Throwable cause = e.getCause() != null ? e.getCause() : e;
101+
droppedCaptures.incrementAndGet();
102+
System.err.println("[codeflash-tracer] Serialization failed for " + className + "."
103+
+ methodName + ": " + cause.getClass().getSimpleName() + ": " + cause.getMessage());
104+
return;
105+
}
96106
}
107+
totalSerializationNs.addAndGet(System.nanoTime() - serStart);
97108

98109
long timeNs = System.nanoTime();
99110
count.incrementAndGet();
100111

101112
writer.recordFunctionCall("call", methodName, className, sourceFile,
102113
lineNumber, descriptor, timeNs, argsBlob);
114+
115+
totalOnEntryNs.addAndGet(System.nanoTime() - entryStart);
103116
}
104117

105118
public void flush() {
@@ -126,5 +139,16 @@ public void flush() {
126139
System.err.println("[codeflash-tracer] Captured " + totalCaptures
127140
+ " invocations across " + functionCounts.size() + " methods"
128141
+ (dropped > 0 ? " (" + dropped + " dropped due to serialization timeout/failure)" : ""));
142+
143+
// Timing summary
144+
long onEntryMs = totalOnEntryNs.get() / 1_000_000;
145+
long serMs = totalSerializationNs.get() / 1_000_000;
146+
String writerSummary = writer.getTimingSummary();
147+
System.err.println("[codeflash-tracer] Timing: onEntry=" + onEntryMs + "ms"
148+
+ " (serialization=" + serMs + "ms)"
149+
+ (totalCaptures > 0
150+
? " avg=" + String.format("%.2f", (double) onEntryMs / totalCaptures) + "ms/capture"
151+
: "")
152+
+ " " + writerSummary);
129153
}
130154
}

0 commit comments

Comments
 (0)