Skip to content

Commit

Permalink
Full async recording mode (#140)
Browse files Browse the repository at this point in the history
* Performance mode
  • Loading branch information
0xaa4eb committed Jul 6, 2024
1 parent c1e3874 commit 1df9f3d
Show file tree
Hide file tree
Showing 13 changed files with 245 additions and 244 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import lombok.Getter;
import org.jetbrains.annotations.Nullable;

import java.nio.file.Paths;
import java.time.Duration;
import java.util.regex.Pattern;

Expand Down Expand Up @@ -66,7 +65,7 @@ private AgentContext() {
.classpath(new Classpath().toList())
.build();
this.typeResolver = ReflectionBasedTypeResolver.getInstance();
this.recordingEventQueue = new RecordingEventQueue(typeResolver, new AgentDataWriter(recordingDataWriter, methodRepository), metrics);
this.recordingEventQueue = new RecordingEventQueue(settings, typeResolver, new AgentDataWriter(recordingDataWriter, methodRepository), metrics);
this.recorder = new Recorder(methodRepository, startRecordingPolicy, recordingEventQueue, metrics);

if (settings.getBindNetworkAddress() != null) {
Expand Down
101 changes: 35 additions & 66 deletions ulyp-agent-core/src/main/java/com/ulyp/agent/Settings.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.ulyp.core.util.TypeMatcher;
import com.ulyp.core.util.CommaSeparatedList;
import com.ulyp.core.util.PackageList;
import lombok.Builder;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand All @@ -19,6 +20,7 @@
* It's only possible to set settings via JMV system properties at the time.
*/
@Getter
@Builder
public class Settings {

public static final boolean TIMESTAMPS_ENABLED;
Expand All @@ -37,7 +39,7 @@ public class Settings {
public static final String INSTRUMENT_LAMBDAS_PROPERTY = "ulyp.lambdas";
public static final String INSTRUMENT_TYPE_INITIALIZERS = "ulyp.type-initializers";
public static final String RECORD_COLLECTIONS_PROPERTY = "ulyp.collections";
public static final String AGGRESSIVE_PROPERTY = "ulyp.aggressive";
public static final String PERFORMANCE_PROPERTY = "ulyp.performance";
public static final String TIMESTAMPS_ENABLED_PROPERTY = "ulyp.timestamps";
public static final String TYPE_VALIDATION_ENABLED_PROPERTY = "ulyp.type-validation";
public static final String AGENT_DISABLED_PROPERTY = "ulyp.off";
Expand All @@ -48,11 +50,9 @@ public class Settings {
TIMESTAMPS_ENABLED = System.getProperty(TIMESTAMPS_ENABLED_PROPERTY) != null;
}

@NotNull
private final String recordingDataFilePath;
private final PackageList instrumentatedPackages;
private final PackageList excludedFromInstrumentationPackages;
@NotNull
private final StartRecordingMethods startRecordingMethods;
private final Pattern startRecordingThreads;
private final List<TypeMatcher> excludeFromInstrumentationClasses;
Expand All @@ -67,43 +67,11 @@ public class Settings {
private final boolean timestampsEnabled;
private final boolean metricsEnabled;
private final boolean typeValidationEnabled;

public Settings(
@NotNull String recordingDataFilePath,
PackageList instrumentedPackages,
PackageList excludedFromInstrumentationPackages,
@NotNull StartRecordingMethods startRecordingMethods,
Pattern startRecordingThreads,
boolean instrumentConstructorsEnabled,
boolean instrumentLambdasEnabled,
boolean instrumentTypeInitializers,
CollectionsRecordingMode collectionsRecordingMode,
Set<TypeMatcher> typesToPrint,
String startRecordingPolicyPropertyValue,
List<TypeMatcher> excludeFromInstrumentationClasses,
String bindNetworkAddress,
boolean agentEnabled,
boolean timestampsEnabled,
boolean metricsEnabled,
boolean typeValidationEnabled) {
this.recordingDataFilePath = recordingDataFilePath;
this.instrumentatedPackages = instrumentedPackages;
this.excludedFromInstrumentationPackages = excludedFromInstrumentationPackages;
this.startRecordingMethods = startRecordingMethods;
this.startRecordingThreads = startRecordingThreads;
this.instrumentConstructorsEnabled = instrumentConstructorsEnabled;
this.instrumentLambdasEnabled = instrumentLambdasEnabled;
this.instrumentTypeInitializers = instrumentTypeInitializers;
this.collectionsRecordingMode = collectionsRecordingMode;
this.typesToPrint = typesToPrint;
this.startRecordingPolicyPropertyValue = startRecordingPolicyPropertyValue;
this.excludeFromInstrumentationClasses = excludeFromInstrumentationClasses;
this.bindNetworkAddress = bindNetworkAddress;
this.agentEnabled = agentEnabled;
this.timestampsEnabled = timestampsEnabled;
this.metricsEnabled = metricsEnabled;
this.typeValidationEnabled = typeValidationEnabled;
}
/**
* In performance mode collection and array recorders are disabled, and most objects are passed by reference to the
* background thread. This may lower performance impact on application's client threads
*/
private final boolean performanceModeEnabled;

public static Settings fromSystemProperties() {

Expand All @@ -119,7 +87,7 @@ public static Settings fromSystemProperties() {

String methodsToRecordRaw = System.getProperty(START_RECORDING_METHODS_PROPERTY, "");
String excludeMethodsToRecordRaw = System.getProperty(EXCLUDE_RECORDING_METHODS_PROPERTY, "");
StartRecordingMethods recordingStartMethods = StartRecordingMethods.parse(methodsToRecordRaw, excludeMethodsToRecordRaw);
StartRecordingMethods startRecordingMethods = StartRecordingMethods.parse(methodsToRecordRaw, excludeMethodsToRecordRaw);

String recordingDataFilePath = System.getProperty(FILE_PATH_PROPERTY);
if (recordingDataFilePath == null) {
Expand All @@ -130,15 +98,15 @@ public static Settings fromSystemProperties() {
.map(Pattern::compile)
.orElse(null);

boolean aggressive = System.getProperty(AGGRESSIVE_PROPERTY) != null;
boolean recordConstructors = aggressive || System.getProperty(INSTRUMENT_CONSTRUCTORS_PROPERTY) != null;
boolean instrumentLambdas = aggressive || System.getProperty(INSTRUMENT_LAMBDAS_PROPERTY) != null;
boolean instrumentTypeInitializers = aggressive || System.getProperty(INSTRUMENT_TYPE_INITIALIZERS) != null;
boolean performanceModeEnabled = System.getProperty(PERFORMANCE_PROPERTY) != null;
boolean recordConstructors = System.getProperty(INSTRUMENT_CONSTRUCTORS_PROPERTY) != null;
boolean instrumentLambdas = System.getProperty(INSTRUMENT_LAMBDAS_PROPERTY) != null;
boolean instrumentTypeInitializers = System.getProperty(INSTRUMENT_TYPE_INITIALIZERS) != null;
boolean timestampsEnabled = System.getProperty(TIMESTAMPS_ENABLED_PROPERTY) != null;

String recordCollectionsProp;
if (aggressive) {
recordCollectionsProp = CollectionsRecordingMode.JAVA.name();
if (performanceModeEnabled) {
recordCollectionsProp = CollectionsRecordingMode.NONE.name();
} else {
recordCollectionsProp = System.getProperty(RECORD_COLLECTIONS_PROPERTY, CollectionsRecordingMode.NONE.name());
if (recordCollectionsProp.isEmpty()) {
Expand All @@ -157,25 +125,26 @@ public static Settings fromSystemProperties() {
.map(TypeMatcher::parse)
.collect(Collectors.toSet());

return new Settings(
recordingDataFilePath,
instrumentationPackages,
excludedPackages,
recordingStartMethods,
recordThreads,
recordConstructors,
instrumentLambdas,
instrumentTypeInitializers,
collectionsRecordingMode,
typesToPrint,
startRecordingPolicy,
excludeClassesFromInstrumentation,
bindNetworkAddress,
agentEnabled,
timestampsEnabled,
metricsEnabled,
typeValidationEnabled
);
return Settings.builder()
.recordingDataFilePath(recordingDataFilePath)
.instrumentatedPackages(instrumentationPackages)
.excludedFromInstrumentationPackages(excludedPackages)
.startRecordingMethods(startRecordingMethods)
.startRecordingThreads(recordThreads)
.instrumentConstructorsEnabled(recordConstructors)
.instrumentLambdasEnabled(instrumentLambdas)
.instrumentTypeInitializers(instrumentTypeInitializers)
.collectionsRecordingMode(collectionsRecordingMode)
.typesToPrint(typesToPrint)
.startRecordingPolicyPropertyValue(startRecordingPolicy)
.excludeFromInstrumentationClasses(excludeClassesFromInstrumentation)
.bindNetworkAddress(bindNetworkAddress)
.agentEnabled(agentEnabled)
.timestampsEnabled(timestampsEnabled)
.metricsEnabled(metricsEnabled)
.typeValidationEnabled(typeValidationEnabled)
.performanceModeEnabled(performanceModeEnabled)
.build();
}

@Nullable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.util.concurrent.locks.LockSupport;

import com.ulyp.agent.AgentDataWriter;
import com.ulyp.agent.Settings;
import com.ulyp.agent.queue.disruptor.RecordingQueueDisruptor;
import com.ulyp.agent.queue.events.*;
import com.ulyp.core.metrics.Metrics;
Expand All @@ -32,7 +33,6 @@
* Other objects like strings and numbers are immutable, so we can only pass a reference to object and
* avoid serialization/recording. The state of object is then recorded to bytes in the background. For objects like collections
* we must record in the caller thread and pass a buffer.
* Currently, it has fixed capacity which should be addressed in near future
*/
@Slf4j
public class RecordingEventQueue implements AutoCloseable {
Expand All @@ -41,18 +41,19 @@ public class RecordingEventQueue implements AutoCloseable {
private static final int TMP_BUFFER_SIZE = SystemPropertyUtil.getInt("ulyp.recording-queue.tmp-buffer.size", 16 * 1024);
private static final int TMP_BUFFER_ENTRIES = SystemPropertyUtil.getInt("ulyp.recording-queue.tmp-buffer.entries", 8);

private final boolean performanceMode;
private final TypeResolver typeResolver;
private final RecordingQueueDisruptor disruptor;
private final ScheduledExecutorService scheduledExecutorService;
private final QueueBatchEventProcessorFactory eventProcessorFactory;
private final ConcurrentSimpleObjectPool<byte[]> bufferPool;
private final ThreadLocal<RecordingEventBatch> batchEventThreadLocal = ThreadLocal.withInitial(() -> {
RecordingEventBatch eventBatch = new RecordingEventBatch();
eventBatch.resetForUpcomingEvents();
return eventBatch;
});
private final ConcurrentSimpleObjectPool<byte[]> bufferPool;
private final TypeResolver typeResolver;
private final RecordingQueueDisruptor disruptor;
private final ScheduledExecutorService scheduledExecutorService;
private final QueueBatchEventProcessorFactory eventProcessorFactory;

public RecordingEventQueue(TypeResolver typeResolver, AgentDataWriter agentDataWriter, Metrics metrics) {
public RecordingEventQueue(Settings settings, TypeResolver typeResolver, AgentDataWriter agentDataWriter, Metrics metrics) {
this.typeResolver = typeResolver;
this.bufferPool = new ConcurrentSimpleObjectPool<>(TMP_BUFFER_ENTRIES, () -> new byte[TMP_BUFFER_SIZE]);
this.disruptor = new RecordingQueueDisruptor(
Expand All @@ -63,6 +64,7 @@ public RecordingEventQueue(TypeResolver typeResolver, AgentDataWriter agentDataW
metrics
);
this.eventProcessorFactory = new QueueBatchEventProcessorFactory(typeResolver, agentDataWriter);
this.performanceMode = settings.isPerformanceModeEnabled();
this.scheduledExecutorService = Executors.newScheduledThreadPool(
1,
NamedThreadFactory.builder().name("ulyp-recorder-queue-stats-reporter").daemon(true).build()
Expand Down Expand Up @@ -94,12 +96,9 @@ public void enqueueMethodEnter(int recordingId, int callId, int methodId, @Nulla
calleeIdentityHashCode = 0;
}

appendEvent(recordingId, new EnterMethodRecordingEvent(
callId,
methodId,
calleeTypeId,
calleeIdentityHashCode,
convert(args)));
Object[] argsPrepared = prepareArgs(args);

appendEvent(recordingId, new EnterMethodRecordingEvent(callId, methodId, calleeTypeId, calleeIdentityHashCode, argsPrepared));
}

public void enqueueMethodEnter(int recordingId, int callId, int methodId, @Nullable Object callee, Object[] args, long nanoTime) {
Expand All @@ -112,21 +111,25 @@ public void enqueueMethodEnter(int recordingId, int callId, int methodId, @Nulla
calleeTypeId = -1;
calleeIdentityHashCode = 0;
}
Object[] argsPrepared = prepareArgs(args);

appendEvent(recordingId, new TimestampedEnterMethodRecordingEvent(
callId,
methodId,
calleeTypeId,
calleeIdentityHashCode,
convert(args),
argsPrepared,
nanoTime));
}

public void enqueueMethodExit(int recordingId, int callId, Object returnValue, boolean thrown) {
appendEvent(recordingId, new ExitMethodRecordingEvent(callId, convert(returnValue), thrown));
Object returnValuePrepared = prepareReturnValue(returnValue);
appendEvent(recordingId, new ExitMethodRecordingEvent(callId, returnValuePrepared, thrown));
}

public void enqueueMethodExit(int recordingId, int callId, Object returnValue, boolean thrown, long nanoTime) {
appendEvent(recordingId, new TimestampedExitMethodRecordingEvent(callId, convert(returnValue), thrown, nanoTime));
Object returnValuePrepared = prepareReturnValue(returnValue);
appendEvent(recordingId, new TimestampedExitMethodRecordingEvent(callId, returnValuePrepared, thrown, nanoTime));
}

public void flush(int recordingId) {
Expand All @@ -148,13 +151,38 @@ private void appendEvent(int recordingId, RecordingEvent event) {
}
}

private Object prepareReturnValue(Object returnValue) {
Object returnValuePrepared;
if (performanceMode) {
returnValuePrepared = returnValue;
} else {
returnValuePrepared = convert(returnValue);
}
return returnValuePrepared;
}

private Object[] prepareArgs(Object[] args) {
Object[] argsPrepared;
if (performanceMode) {
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public String foo(Integer s) {
private final MethodRepository methodRepository = new MethodRepository();
private final TypeResolver typeResolver = new ReflectionBasedTypeResolver();
private final StatsRecordingDataWriter recordingDataWriter = new StatsRecordingDataWriter(new NullMetrics(), new BlackholeRecordingDataWriter());
private final RecordingEventQueue recordingEventQueue = new RecordingEventQueue(typeResolver, new AgentDataWriter(recordingDataWriter, methodRepository), new NullMetrics());
private final RecordingEventQueue recordingEventQueue = new RecordingEventQueue(Settings.builder().build(), typeResolver, new AgentDataWriter(recordingDataWriter, methodRepository), new NullMetrics());
private final Recorder recorder = new Recorder(methodRepository, new EnabledRecordingPolicy(), recordingEventQueue, new NullMetrics());
private final ReflectionBasedMethodResolver methodResolver = new ReflectionBasedMethodResolver();
private Method method;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ public String foo(Integer s) {
private final MethodRepository methodRepository = new MethodRepository();
private final HeapRecordingDataWrtiter storage = new HeapRecordingDataWrtiter();
private final TypeResolver typeResolver = new ReflectionBasedTypeResolver();
private final RecordingEventQueue callRecordQueue = new RecordingEventQueue(typeResolver, new AgentDataWriter(storage, methodRepository), new NullMetrics());
private final RecordingEventQueue callRecordQueue = new RecordingEventQueue(
Settings.builder().build(),
typeResolver,
new AgentDataWriter(storage, methodRepository),
new NullMetrics()
);
private final Recorder recorder = new Recorder(
methodRepository,
new EnabledRecordingPolicy(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.ulyp.core.recorders.collections.CollectionRecord;
import com.ulyp.core.recorders.collections.CollectionsRecordingMode;
import com.ulyp.storage.tree.CallRecord;
import org.hamcrest.CoreMatchers;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.jetbrains.annotations.NotNull;
Expand All @@ -18,6 +19,7 @@

import static org.hamcrest.MatcherAssert.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;

class CollectionRecorderTest extends AbstractInstrumentationTest {

Expand All @@ -42,6 +44,20 @@ void shouldRecordSimpleItemsProperly() {
assertEquals("b", secondItemRepr.value());
}

@Test
void shouldNotRecordCollectionsInPerformanceMode() {

CallRecord root = runSubprocessAndReadFile(
new ForkProcessBuilder()
.withMainClassName(TestCase.class)
.withMethodToRecord("returnArrayListOfString")
.withRecordCollections(CollectionsRecordingMode.ALL)
.withPerformanceMode(true)
);

assertThat(root.getReturnValue(), CoreMatchers.instanceOf(IdentityObjectRecord.class));
}

@Test
void shouldRecordSimpleListIfAllCollectionsAreRecorded() {

Expand Down
Loading

0 comments on commit 1df9f3d

Please sign in to comment.