diff --git a/ulyp-agent-core/src/main/java/com/ulyp/agent/ByTypeRecordedObjectConverter.java b/ulyp-agent-core/src/main/java/com/ulyp/agent/ByTypeRecordedObjectConverter.java new file mode 100644 index 00000000..75144fa8 --- /dev/null +++ b/ulyp-agent-core/src/main/java/com/ulyp/agent/ByTypeRecordedObjectConverter.java @@ -0,0 +1,77 @@ +package com.ulyp.agent; + +import com.ulyp.core.Type; +import com.ulyp.core.TypeResolver; +import com.ulyp.core.bytes.BufferBytesOut; +import com.ulyp.core.recorders.*; +import com.ulyp.core.util.LoggingSettings; +import com.ulyp.core.util.SystemPropertyUtil; +import lombok.extern.slf4j.Slf4j; +import org.agrona.concurrent.UnsafeBuffer; + + +@Slf4j +public class ByTypeRecordedObjectConverter implements RecordedObjectConverter { + + private static final int TMP_BUFFER_SIZE = SystemPropertyUtil.getInt("ulyp.recording.tmp-buffer.size", 16 * 1024); + + private final TypeResolver typeResolver; + private byte[] tmpBuffer; + + public ByTypeRecordedObjectConverter(TypeResolver typeResolver) { + this.typeResolver = typeResolver; + } + + @Override + public Object[] prepare(Object[] args) { + if (args == null) { + return null; + } + for (int i = 0; i < args.length; i++) { + args[i] = prepare(args[i]); + } + return args; + } + + /** + * Resolves type for an object, then checks if it can be recorded asynchronously in a background thread. Most objects + * have only their identity hash code and type id recorded, so it can be safely done concurrently in some other thread. + * Collections have a few of their items recorded (if enabled), so the recording must happen here. + */ + public Object prepare(Object value) { + if (value == null) { + return null; + } + Type type = typeResolver.get(value); + ObjectRecorder recorder = type.getRecorderHint(); + if (recorder == null) { + recorder = RecorderChooser.getInstance().chooseForType(value.getClass()); + type.setRecorderHint(recorder); + } + if (recorder.supportsAsyncRecording()) { + if (recorder instanceof IdentityRecorder) { + return new QueuedIdentityObject(type.getId(), value); + } else { + return value; + } + } else { + BufferBytesOut output = new BufferBytesOut(new UnsafeBuffer(getTmpBuffer())); + try { + recorder.write(value, output, typeResolver); + return new QueuedRecordedObject(type, recorder.getId(), output.copy()); + } catch (Exception e) { + if (LoggingSettings.DEBUG_ENABLED) { + log.debug("Error while recording object", e); + } + return new QueuedIdentityObject(type.getId(), value); + } + } + } + + private byte[] getTmpBuffer() { + if (tmpBuffer == null) { + tmpBuffer = new byte[TMP_BUFFER_SIZE]; + } + return tmpBuffer; + } +} diff --git a/ulyp-agent-core/src/main/java/com/ulyp/agent/PassByRefRecordedObjectConverter.java b/ulyp-agent-core/src/main/java/com/ulyp/agent/PassByRefRecordedObjectConverter.java new file mode 100644 index 00000000..0426b36a --- /dev/null +++ b/ulyp-agent-core/src/main/java/com/ulyp/agent/PassByRefRecordedObjectConverter.java @@ -0,0 +1,21 @@ +package com.ulyp.agent; + +import lombok.extern.slf4j.Slf4j; + +/** + * Passes all objects to the background thread by reference. Can only be enabled if collections and arrays recording + * is turned off. + */ +@Slf4j +public class PassByRefRecordedObjectConverter implements RecordedObjectConverter { + + public static final RecordedObjectConverter INSTANCE = new PassByRefRecordedObjectConverter(); + + public Object prepare(Object arg) { + return arg; + } + + public Object[] prepare(Object[] args) { + return args; + } +} diff --git a/ulyp-agent-core/src/main/java/com/ulyp/agent/RecordedObjectConverter.java b/ulyp-agent-core/src/main/java/com/ulyp/agent/RecordedObjectConverter.java new file mode 100644 index 00000000..e47378f8 --- /dev/null +++ b/ulyp-agent-core/src/main/java/com/ulyp/agent/RecordedObjectConverter.java @@ -0,0 +1,18 @@ +package com.ulyp.agent; + +/** + * Prepares (and possibly converts) object for sending to the background thread. The background thread does actual recording using the recorders. + * Most of the time we just pass reference to an object to the background thread which calls recorders. This allows us to + * offload some work from the client app threads to the background thread. + * This works since most objects are either: + * 1) immutable + * 2) we only record their type id and identity hash code. + * So, we can record their values (i.e. access their fields) in some other threads. + * All other objects like collections and arrays must be recorded immediately in the client app thread. + */ +public interface RecordedObjectConverter { + + Object prepare(Object arg); + + Object[] prepare(Object[] args); +} diff --git a/ulyp-agent-core/src/main/java/com/ulyp/agent/Recorder.java b/ulyp-agent-core/src/main/java/com/ulyp/agent/Recorder.java index 3f376880..03dad21d 100644 --- a/ulyp-agent-core/src/main/java/com/ulyp/agent/Recorder.java +++ b/ulyp-agent-core/src/main/java/com/ulyp/agent/Recorder.java @@ -82,7 +82,7 @@ public void disableRecording() { if (recordingCtx != null) { recordingCtx.setEnabled(false); } else { - recordingCtx = new RecordingThreadLocalContext(); + recordingCtx = new RecordingThreadLocalContext(options, typeResolver); recordingCtx.setEnabled(false); threadLocalRecordingCtx.set(recordingCtx); } @@ -118,13 +118,13 @@ public long startRecordingOnMethodEnter(int methodId, @Nullable Object callee, O private RecordingThreadLocalContext initializeRecordingCtx(int methodId) { RecordingThreadLocalContext recordingCtx = threadLocalRecordingCtx.get(); if (recordingCtx == null) { - recordingCtx = new RecordingThreadLocalContext(); + recordingCtx = new RecordingThreadLocalContext(options, typeResolver); recordingCtx.setEnabled(false); int recordingId = recordingContextStore.add(recordingCtx); RecordingMetadata recordingMetadata = generateRecordingMetadata(recordingId); recordingCtx.setRecordingMetadata(recordingMetadata); threadLocalRecordingCtx.set(recordingCtx); - RecordingEventBuffer recordingEventBuffer = new RecordingEventBuffer(recordingMetadata.getId(), options, typeResolver); + RecordingEventBuffer recordingEventBuffer = new RecordingEventBuffer(recordingMetadata.getId()); recordingCtx.setEventBuffer(recordingEventBuffer); currentRecordingSessionCount.incrementAndGet(); @@ -152,12 +152,13 @@ public long onMethodEnter(RecordingThreadLocalContext recordingCtx, int methodId try { recordingCtx.setEnabled(false); + RecordedObjectConverter objectConverter = recordingCtx.getObjectConverter(); int callId = recordingCtx.nextCallId(); RecordingEventBuffer eventBuffer = recordingCtx.getEventBuffer(); if (AgentOptions.TIMESTAMPS_ENABLED) { - eventBuffer.appendMethodEnterEvent(methodId, callee, args, System.nanoTime()); + eventBuffer.appendMethodEnterEvent(methodId, callee, objectConverter.prepare(args), System.nanoTime()); } else { - eventBuffer.appendMethodEnterEvent(methodId, callee, args); + eventBuffer.appendMethodEnterEvent(methodId, callee, objectConverter.prepare(args)); } dropIfFull(eventBuffer); return BitUtil.longFromInts(recordingCtx.getRecordingId(), callId); @@ -184,12 +185,13 @@ public long onMethodEnter(RecordingThreadLocalContext recordingCtx, int methodId try { recordingCtx.setEnabled(false); + RecordedObjectConverter objectConverter = recordingCtx.getObjectConverter(); int callId = recordingCtx.nextCallId(); RecordingEventBuffer eventBuffer = recordingCtx.getEventBuffer(); if (AgentOptions.TIMESTAMPS_ENABLED) { - eventBuffer.appendMethodEnterEvent(methodId, callee, arg, System.nanoTime()); + eventBuffer.appendMethodEnterEvent(methodId, callee, objectConverter.prepare(arg), System.nanoTime()); } else { - eventBuffer.appendMethodEnterEvent(methodId, callee, arg); + eventBuffer.appendMethodEnterEvent(methodId, callee, objectConverter.prepare(arg)); } dropIfFull(eventBuffer); return BitUtil.longFromInts(recordingCtx.getRecordingId(), callId); @@ -216,12 +218,13 @@ public long onMethodEnter(RecordingThreadLocalContext recordingCtx, int methodId try { recordingCtx.setEnabled(false); + RecordedObjectConverter objectConverter = recordingCtx.getObjectConverter(); int callId = recordingCtx.nextCallId(); RecordingEventBuffer eventBuffer = recordingCtx.getEventBuffer(); if (AgentOptions.TIMESTAMPS_ENABLED) { - eventBuffer.appendMethodEnterEvent(methodId, callee, arg1, arg2, System.nanoTime()); + eventBuffer.appendMethodEnterEvent(methodId, callee, objectConverter.prepare(arg1), objectConverter.prepare(arg2), System.nanoTime()); } else { - eventBuffer.appendMethodEnterEvent(methodId, callee, arg1, arg2); + eventBuffer.appendMethodEnterEvent(methodId, callee, objectConverter.prepare(arg1), objectConverter.prepare(arg2)); } dropIfFull(eventBuffer); return BitUtil.longFromInts(recordingCtx.getRecordingId(), callId); @@ -248,12 +251,26 @@ public long onMethodEnter(RecordingThreadLocalContext recordingCtx, int methodId try { recordingCtx.setEnabled(false); + RecordedObjectConverter objectConverter = recordingCtx.getObjectConverter(); int callId = recordingCtx.nextCallId(); RecordingEventBuffer eventBuffer = recordingCtx.getEventBuffer(); if (AgentOptions.TIMESTAMPS_ENABLED) { - eventBuffer.appendMethodEnterEvent(methodId, callee, arg1, arg2, arg3, System.nanoTime()); + eventBuffer.appendMethodEnterEvent( + methodId, + callee, + objectConverter.prepare(arg1), + objectConverter.prepare(arg2), + objectConverter.prepare(arg3), + System.nanoTime() + ); } else { - eventBuffer.appendMethodEnterEvent(methodId, callee, arg1, arg2, arg3); + eventBuffer.appendMethodEnterEvent( + methodId, + callee, + objectConverter.prepare(arg1), + objectConverter.prepare(arg2), + objectConverter.prepare(arg3) + ); } dropIfFull(eventBuffer); return BitUtil.longFromInts(recordingCtx.getRecordingId(), callId); @@ -313,11 +330,12 @@ public void onMethodExit(int methodId, Object result, Throwable thrown, long cal try { recordingCtx.setEnabled(false); + RecordedObjectConverter objectConverter = recordingCtx.getObjectConverter(); RecordingEventBuffer eventBuffer = recordingCtx.getEventBuffer(); if (AgentOptions.TIMESTAMPS_ENABLED) { - eventBuffer.appendMethodExitEvent(callId, thrown != null ? thrown : result, thrown != null, System.nanoTime()); + eventBuffer.appendMethodExitEvent(callId, objectConverter.prepare(thrown != null ? thrown : result), thrown != null, System.nanoTime()); } else { - eventBuffer.appendMethodExitEvent(callId, thrown != null ? thrown : result, thrown != null); + eventBuffer.appendMethodExitEvent(callId, objectConverter.prepare(thrown != null ? thrown : result), thrown != null); } if (callId == RecordingThreadLocalContext.ROOT_CALL_RECORDING_ID) { diff --git a/ulyp-agent-core/src/main/java/com/ulyp/agent/RecordingEventBuffer.java b/ulyp-agent-core/src/main/java/com/ulyp/agent/RecordingEventBuffer.java index 13da3724..cd5f2e9c 100644 --- a/ulyp-agent-core/src/main/java/com/ulyp/agent/RecordingEventBuffer.java +++ b/ulyp-agent-core/src/main/java/com/ulyp/agent/RecordingEventBuffer.java @@ -1,25 +1,17 @@ package com.ulyp.agent; -import com.ulyp.agent.options.AgentOptions; import com.ulyp.agent.queue.events.*; import com.ulyp.core.RecordingMetadata; -import com.ulyp.core.Type; -import com.ulyp.core.TypeResolver; -import com.ulyp.core.bytes.BufferBytesOut; -import com.ulyp.core.recorders.*; -import com.ulyp.core.recorders.collections.CollectionsRecordingMode; -import com.ulyp.core.util.LoggingSettings; import com.ulyp.core.util.SystemPropertyUtil; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.agrona.concurrent.UnsafeBuffer; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; /** - * Thread-local buffer for recording events. Recording threads gather some number of + * Thread-local buffer for recording events. Client app threads gather some number of * events in into such buffers and post them to the background thread. */ @Getter @@ -27,20 +19,14 @@ public class RecordingEventBuffer { private static final int MAX_BUFFER_SIZE = SystemPropertyUtil.getInt("ulyp.recording.max-buffer-size", 256); - private static final int TMP_BUFFER_SIZE = SystemPropertyUtil.getInt("ulyp.recording.tmp-buffer.size", 16 * 1024); @Getter private final int recordingId; - private final boolean alwaysPassByRef; - private final TypeResolver typeResolver; @Getter private List events; - private byte[] tmpBuffer; - public RecordingEventBuffer(int recordingId, AgentOptions options, TypeResolver typeResolver) { + public RecordingEventBuffer(int recordingId) { this.recordingId = recordingId; - this.alwaysPassByRef = options.getCollectionsRecordingMode().get() == CollectionsRecordingMode.NONE; - this.typeResolver = typeResolver; this.events = new ArrayList<>(MAX_BUFFER_SIZE); } @@ -48,10 +34,6 @@ public void reset() { events = new ArrayList<>(MAX_BUFFER_SIZE); } - public boolean isEmpty() { - return events.isEmpty(); - } - public boolean isFull() { return events.size() >= MAX_BUFFER_SIZE; } @@ -69,21 +51,19 @@ public void appendRecordingFinishedEvent(long recordingFinishedTimeMillis) { } public void appendMethodEnterEvent(int methodId, @Nullable Object callee, Object[] args) { - Object[] argsPrepared = prepareArgs(args); - - events.add(new EnterMethodRecordingEvent(methodId, callee, argsPrepared)); + events.add(new EnterMethodRecordingEvent(methodId, callee, args)); } public void appendMethodEnterEvent(int methodId, @Nullable Object callee, Object arg) { - events.add(new EnterMethodOneArgRecordingEvent(methodId, callee, prepareArg(arg))); + events.add(new EnterMethodOneArgRecordingEvent(methodId, callee, arg)); } public void appendMethodEnterEvent(int methodId, @Nullable Object callee, Object arg1, Object arg2) { - events.add(new EnterMethodTwoArgsRecordingEvent(methodId, callee, prepareArg(arg1), prepareArg(arg2))); + events.add(new EnterMethodTwoArgsRecordingEvent(methodId, callee, arg1, arg2)); } public void appendMethodEnterEvent(int methodId, @Nullable Object callee, Object arg1, Object arg2, Object arg3) { - events.add(new EnterMethodThreeArgsRecordingEvent(methodId, callee, prepareArg(arg1), prepareArg(arg2), prepareArg(arg3))); + events.add(new EnterMethodThreeArgsRecordingEvent(methodId, callee, arg1, arg2, arg3)); } public void appendMethodEnterEvent(int methodId, @Nullable Object callee) { @@ -95,109 +75,26 @@ public void appendMethodEnterEvent(int methodId, @Nullable Object callee, long n } public void appendMethodEnterEvent(int methodId, @Nullable Object callee, Object arg, long nanoTime) { - events.add(new TimestampedEnterMethodOneArgRecordingEvent(methodId, callee, prepareArg(arg), nanoTime)); + events.add(new TimestampedEnterMethodOneArgRecordingEvent(methodId, callee, arg, nanoTime)); } public void appendMethodEnterEvent(int methodId, @Nullable Object callee, Object arg1, Object arg2, long nanoTime) { - events.add(new TimestampedEnterMethodTwoArgsRecordingEvent(methodId, callee, prepareArg(arg1), prepareArg(arg2), nanoTime)); + events.add(new TimestampedEnterMethodTwoArgsRecordingEvent(methodId, callee, arg1, arg2, nanoTime)); } public void appendMethodEnterEvent(int methodId, @Nullable Object callee, Object arg1, Object arg2, Object arg3, long nanoTime) { - events.add(new TimestampedEnterMethodThreeArgsRecordingEvent(methodId, callee, prepareArg(arg1), prepareArg(arg2), prepareArg(arg3), nanoTime)); + events.add(new TimestampedEnterMethodThreeArgsRecordingEvent(methodId, callee, arg1, arg2, arg3, nanoTime)); } public void appendMethodEnterEvent(int methodId, @Nullable Object callee, Object[] args, long nanoTime) { - Object[] argsPrepared = prepareArgs(args); - - events.add(new TimestampedEnterMethodRecordingEvent(methodId, callee, argsPrepared, nanoTime)); + events.add(new TimestampedEnterMethodRecordingEvent(methodId, callee, args, nanoTime)); } public void appendMethodExitEvent(int callId, Object returnValue, boolean thrown) { - Object returnValuePrepared = prepareReturnValue(returnValue); - events.add(new ExitMethodRecordingEvent(callId, returnValuePrepared, thrown)); + events.add(new ExitMethodRecordingEvent(callId, returnValue, thrown)); } public void appendMethodExitEvent(int callId, Object returnValue, boolean thrown, long nanoTime) { - Object returnValuePrepared = prepareReturnValue(returnValue); - events.add(new TimestampedExitMethodRecordingEvent(callId, returnValuePrepared, thrown, nanoTime)); - } - - private Object prepareReturnValue(Object returnValue) { - Object returnValuePrepared; - if (alwaysPassByRef) { - returnValuePrepared = returnValue; - } else { - returnValuePrepared = convert(returnValue); - } - return returnValuePrepared; - } - - private Object prepareArg(Object arg) { - if (alwaysPassByRef) { - return arg; - } else { - return convert(arg); - } - } - - private Object[] prepareArgs(Object[] args) { - if (args == null) { - return null; - } - Object[] argsPrepared; - if (alwaysPassByRef) { - argsPrepared = args; - } else { - argsPrepared = convert(args); - } - return argsPrepared; - } - - private Object[] convert(Object[] args) { - for (int i = 0; i < args.length; i++) { - args[i] = convert(args[i]); - } - return args; - } - - /** - * Resolves type for an object, then checks if it can be recorded asynchronously in a background thread. Most objects - * have only their identity hash code and type id recorded, so it can be safely done concurrently in some other thread. - * Collections have a few of their items recorded (if enabled), so the recording must happen here. - */ - private Object convert(Object value) { - Type type = typeResolver.get(value); - ObjectRecorder recorder = type.getRecorderHint(); - if (value != null && recorder == null) { - recorder = RecorderChooser.getInstance().chooseForType(value.getClass()); - type.setRecorderHint(recorder); - } - if (value == null || recorder.supportsAsyncRecording()) { - if (value != null && recorder instanceof IdentityRecorder) { - return new QueuedIdentityObject(type.getId(), value); - } else { - return value; - } - } else { - BufferBytesOut output = new BufferBytesOut(new UnsafeBuffer(getTmpBuffer())); - try { - recorder.write(value, output, typeResolver); - return new QueuedRecordedObject(type, recorder.getId(), output.copy()); - } catch (Exception e) { - if (LoggingSettings.DEBUG_ENABLED) { - log.debug("Error while recording object", e); - } - return new QueuedIdentityObject(type.getId(), value); - } - } - } - - private byte[] getTmpBuffer() { - if (tmpBuffer != null) { - return tmpBuffer; - } else { - tmpBuffer = new byte[TMP_BUFFER_SIZE]; - return tmpBuffer; - } + events.add(new TimestampedExitMethodRecordingEvent(callId, returnValue, thrown, nanoTime)); } } diff --git a/ulyp-agent-core/src/main/java/com/ulyp/agent/RecordingThreadLocalContext.java b/ulyp-agent-core/src/main/java/com/ulyp/agent/RecordingThreadLocalContext.java index a0b53020..6c8afcba 100644 --- a/ulyp-agent-core/src/main/java/com/ulyp/agent/RecordingThreadLocalContext.java +++ b/ulyp-agent-core/src/main/java/com/ulyp/agent/RecordingThreadLocalContext.java @@ -2,8 +2,11 @@ import javax.annotation.Nullable; +import com.ulyp.agent.options.AgentOptions; import com.ulyp.core.RecordingMetadata; +import com.ulyp.core.TypeResolver; +import com.ulyp.core.recorders.collections.CollectionsRecordingMode; import lombok.Getter; import lombok.Setter; @@ -21,6 +24,8 @@ public class RecordingThreadLocalContext { private int recordingId = -1; @Nullable private RecordingMetadata recordingMetadata; + @Getter + private RecordedObjectConverter objectConverter; private int callId = ROOT_CALL_RECORDING_ID; @Setter private boolean enabled; @@ -28,8 +33,12 @@ public class RecordingThreadLocalContext { @Setter private RecordingEventBuffer eventBuffer; - public RecordingThreadLocalContext() { - + public RecordingThreadLocalContext(AgentOptions options, TypeResolver typeResolver) { + if (options.getCollectionsRecordingMode().get() == CollectionsRecordingMode.NONE && !options.getArraysRecordingOption().get()) { + this.objectConverter = PassByRefRecordedObjectConverter.INSTANCE; + } else { + this.objectConverter = new ByTypeRecordedObjectConverter(typeResolver); + } } public int nextCallId() {