All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.amygdalum.testrecorder.SnapshotManager Maven / Gradle / Ivy

package net.amygdalum.testrecorder;

import static java.lang.System.identityHashCode;
import static java.lang.Thread.currentThread;
import static java.util.stream.Collectors.joining;
import static net.amygdalum.testrecorder.TestrecorderThreadFactory.RECORDING;
import static net.amygdalum.testrecorder.util.Reflections.accessing;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.instrument.Instrumentation;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;

import net.amygdalum.testrecorder.profile.AgentConfiguration;
import net.amygdalum.testrecorder.profile.PerformanceProfile;
import net.amygdalum.testrecorder.profile.SnapshotConsumer;
import net.amygdalum.testrecorder.serializers.SerializerFacade;
import net.amygdalum.testrecorder.types.ContextSnapshot;
import net.amygdalum.testrecorder.types.FieldSignature;
import net.amygdalum.testrecorder.types.MethodSignature;
import net.amygdalum.testrecorder.types.Profile;
import net.amygdalum.testrecorder.types.SerializationException;
import net.amygdalum.testrecorder.types.SerializedField;
import net.amygdalum.testrecorder.types.SerializedInput;
import net.amygdalum.testrecorder.types.SerializedInteraction;
import net.amygdalum.testrecorder.types.SerializedOutput;
import net.amygdalum.testrecorder.types.SerializedValue;
import net.amygdalum.testrecorder.types.SerializerSession;
import net.amygdalum.testrecorder.util.CircularityLock;
import net.amygdalum.testrecorder.util.Logger;
import net.amygdalum.testrecorder.values.SerializedNull;

public class SnapshotManager {

	private static final StackTraceValidator STACKTRACE_VALIDATOR = new StackTraceValidator()
		.invalidate(Logger.class);

	public static volatile SnapshotManager MANAGER;

	private ThreadPoolExecutor snapshotExecutor;
	private CircularityLock lock;

	private MethodContext methodContext;
	private GlobalContext globalContext;

	private ThreadLocal> current;

	private SnapshotConsumer snapshotConsumer;
	private long timeoutInMillis;

	private ConfigurableSerializerFacade facade;

	public SnapshotManager(AgentConfiguration config) {
		this.snapshotConsumer = config.loadConfiguration(SnapshotConsumer.class, config);

		this.current = ThreadLocal.withInitial(() -> newStack());
		this.facade = new ConfigurableSerializerFacade(config);

		PerformanceProfile performanceProfile = config.loadConfiguration(PerformanceProfile.class);

		this.timeoutInMillis = performanceProfile.getTimeoutInMillis();
		this.snapshotExecutor = new ThreadPoolExecutor(0, 1, performanceProfile.getIdleTime(), TimeUnit.MILLISECONDS, new LinkedBlockingQueue(),
			new TestrecorderThreadFactory("$snapshot"));
		this.lock = new CircularityLock();
		this.methodContext = new MethodContext();
		this.globalContext = new GlobalContext();
	}

	private static Class installBridge(Instrumentation inst) {
		try {
			String bridgeClassName = "net.amygdalum.testrecorder.bridge.BridgedSnapshotManager";
			inst.appendToBootstrapClassLoaderSearch(jarfile(bridgeClassName));
			Class bridgedSnapshotManagerClass = Class.forName(bridgeClassName, true, null);
			MethodHandle setupVariables = MethodHandles.lookup().findVirtual(SnapshotManager.class, "setupVariables",
				MethodType.methodType(void.class, Class.class, Object.class, String.class, Object[].class));
			bridgedSnapshotManagerClass.getField("setupVariables").set(null, setupVariables);
			MethodHandle expectVariables = MethodHandles.lookup().findVirtual(SnapshotManager.class, "expectVariables",
				MethodType.methodType(void.class, Object.class, String.class, Object.class, Object[].class));
			bridgedSnapshotManagerClass.getField("expectVariables").set(null, expectVariables);
			MethodHandle expectVariablesVoid = MethodHandles.lookup().findVirtual(SnapshotManager.class, "expectVariables",
				MethodType.methodType(void.class, Object.class, String.class, Object[].class));
			bridgedSnapshotManagerClass.getField("expectVariablesVoid").set(null, expectVariablesVoid);
			MethodHandle throwVariables = MethodHandles.lookup().findVirtual(SnapshotManager.class, "throwVariables",
				MethodType.methodType(void.class, Throwable.class, Object.class, String.class, Object[].class));
			bridgedSnapshotManagerClass.getField("throwVariables").set(null, throwVariables);
			MethodHandle inputVariables = MethodHandles.lookup().findVirtual(SnapshotManager.class, "inputVariables",
				MethodType.methodType(int.class, Object.class, String.class, Type.class, Type[].class));
			bridgedSnapshotManagerClass.getField("inputVariables").set(null, inputVariables);
			MethodHandle inputArguments = MethodHandles.lookup().findVirtual(SnapshotManager.class, "inputArguments",
				MethodType.methodType(void.class, int.class, Object[].class));
			bridgedSnapshotManagerClass.getField("inputArguments").set(null, inputArguments);
			MethodHandle inputResult = MethodHandles.lookup().findVirtual(SnapshotManager.class, "inputResult",
				MethodType.methodType(void.class, int.class, Object.class));
			bridgedSnapshotManagerClass.getField("inputResult").set(null, inputResult);
			MethodHandle inputVoidResult = MethodHandles.lookup().findVirtual(SnapshotManager.class, "inputVoidResult",
				MethodType.methodType(void.class, int.class));
			bridgedSnapshotManagerClass.getField("inputVoidResult").set(null, inputVoidResult);
			MethodHandle outputVariables = MethodHandles.lookup().findVirtual(SnapshotManager.class, "outputVariables",
				MethodType.methodType(int.class, Object.class, String.class, Type.class, Type[].class));
			bridgedSnapshotManagerClass.getField("outputVariables").set(null, outputVariables);
			MethodHandle outputArguments = MethodHandles.lookup().findVirtual(SnapshotManager.class, "outputArguments",
				MethodType.methodType(void.class, int.class, Object[].class));
			bridgedSnapshotManagerClass.getField("outputArguments").set(null, outputArguments);
			MethodHandle outputResult = MethodHandles.lookup().findVirtual(SnapshotManager.class, "outputResult",
				MethodType.methodType(void.class, int.class, Object.class));
			bridgedSnapshotManagerClass.getField("outputResult").set(null, outputResult);
			MethodHandle outputVoidResult = MethodHandles.lookup().findVirtual(SnapshotManager.class, "outputVoidResult",
				MethodType.methodType(void.class, int.class));
			bridgedSnapshotManagerClass.getField("outputVoidResult").set(null, outputVoidResult);
			return bridgedSnapshotManagerClass;
		} catch (ReflectiveOperationException | IOException e) {
			throw new RuntimeException("failed installing fake bridge", e);
		}
	}

	private static JarFile jarfile(String bridgeClassName) throws IOException {
		String bridge = bridgeClassName.replace('.', '/') + ".class";
		InputStream resourceStream = SnapshotManager.class.getResourceAsStream("/" + bridge);
		if (resourceStream == null) {
			throw new FileNotFoundException(bridge);
		}
		try (InputStream inputStream = resourceStream) {
			File agentJar = File.createTempFile("agent", "jar");
			agentJar.deleteOnExit();
			Manifest manifest = new Manifest();
			try (JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(agentJar), manifest)) {

				jarOutputStream.putNextEntry(new JarEntry(bridge));
				byte[] buffer = new byte[4096];
				int index;
				while ((index = inputStream.read(buffer)) != -1) {
					jarOutputStream.write(buffer, 0, index);
				}
				jarOutputStream.closeEntry();
			}
			return new JarFile(agentJar);
		}
	}

	public static SnapshotManager init(AgentConfiguration config, Instrumentation inst) {
		Class bridgeClass = installBridge(inst);
		try {
			MANAGER = new SnapshotManager(config);
			bridgeClass.getField("MANAGER").set(null, MANAGER);
			return MANAGER;
		} catch (ReflectiveOperationException e) {
			throw new RuntimeException("failed initializing bridged manager", e);
		}
	}

	public static void done(AgentConfiguration config, Instrumentation inst) {
		MANAGER = null;
	}

	public SnapshotConsumer getMethodConsumer() {
		return snapshotConsumer;
	}

	public void registerRecordedMethod(String signature, String className, String methodName, String methodDesc) {
		methodContext.add(signature, className, methodName, methodDesc);
	}

	public void registerGlobal(String className, String fieldName) {
		globalContext.add(className, fieldName);
	}

	private boolean matches(Object self, String signature) {
		if (self == null) {
			return true;
		}
		return methodContext.signature(signature, self.getClass().getClassLoader()).validIn(self.getClass());
	}

	public ContextSnapshotTransaction push(String signature, ClassLoader loader) {
		ContextSnapshot snapshot = methodContext.createSnapshot(signature, loader);
		current.get().push(snapshot);
		return new ValidContextSnapshotTransaction(snapshotExecutor, timeoutInMillis, facade, snapshot);
	}

	public ContextSnapshotTransaction pop(String signature) {
		Deque snapshots = current.get();
		while (!snapshots.isEmpty()) {
			ContextSnapshot snapshot = snapshots.pop();
			if (snapshot.matches(signature)) {
				return new ValidContextSnapshotTransaction(snapshotExecutor, timeoutInMillis, facade, snapshot);
			}
			snapshot.invalidate();
		}
		return DummyContextSnapshotTransaction.INVALID;
	}

	public ContextSnapshotTransaction current() {
		Deque stack = current.get();
		if (stack.isEmpty()) {
			return DummyContextSnapshotTransaction.INVALID;
		} else {
			ContextSnapshot snapshot = stack.peek();
			return new ValidContextSnapshotTransaction(snapshotExecutor, timeoutInMillis, facade, snapshot);
		}
	}

	public Queue all() {
		return current.get();
	}

	public Optional peek() {
		return Optional.ofNullable(current.get().peek());
	}

	public void setupVariables(Class selfClass, Object self, String signature, Object... args) {
		boolean aquired = lock.acquire();
		if (!aquired) {
			return;
		}
		try {
			if (!matches(self, signature)) {
				return;
			}
			ClassLoader loader = selfClass.getClassLoader();
			push(signature, loader).to((facade, session, snapshot) -> {

				if (self != null) {
					snapshot.setSetupThis(facade.serialize(self.getClass(), self, session));
				}
				snapshot.setSetupArgs(facade.serialize(snapshot.getArgumentTypes(), args, session));
				snapshot.setSetupGlobals(globalContext.globals(snapshot.getClassLoader()).stream()
					.map(field -> serializedGlobal(session, field))
					.toArray(SerializedField[]::new));
			});
		} finally {
			lock.release();
		}
	}

	public int inputVariables(Object object, String method, Type resultType, Type[] paramTypes) {
		boolean aquired = lock.acquire();
		if (!aquired) {
			return 0;
		}
		try {
			if (isNestedIO() || isInvalidStacktrace()) {
				return 0;
			}
			Class clazz = toClass(object);
			int id = toId(object);

			MethodSignature signature = new MethodSignature(clazz, resultType, method, paramTypes);
			SerializedInput in = facade.serializeInput(id, signature);
			for (ContextSnapshot snapshot : all()) {
				snapshot.addInput(in);
			}
			return in.id();
		} finally {
			lock.release();
		}
	}

	public void inputArguments(int id, Object... arguments) {
		boolean aquired = lock.acquire();
		if (!aquired) {
			return;
		}
		try {
			if (id == 0) {
				return;
			}
			current().to((facade, session, snapshot) -> {
				snapshot.streamInput().filter(in -> in.id() == id).forEach(in -> {
					in.updateArguments(facade.serialize(in.getArgumentTypes(), arguments, session));
				});
			});
		} finally {
			lock.release();
		}
	}

	public void inputResult(int id, Object result) {
		boolean aquired = lock.acquire();
		if (!aquired) {
			return;
		}
		try {
			if (id == 0) {
				return;
			}
			current().to((facade, session, snapshot) -> {
				snapshot.streamInput().filter(in -> in.id() == id).forEach(in -> {
					in.updateResult(facade.serialize(in.getResultType(), result, session));
				});
			});
		} finally {
			lock.release();
		}
	}

	public void inputVoidResult(int id) {
		boolean aquired = lock.acquire();
		if (!aquired) {
			return;
		}
		try {
			if (id == 0) {
				return;
			}
			current().to((facade, session, snapshot) -> {
				snapshot.streamInput().filter(in -> in.id() == id).forEach(in -> {
					in.updateResult(SerializedNull.VOID);
				});
			});
		} finally {
			lock.release();
		}
	}

	public int outputVariables(Object object, String method, Type resultType, Type[] paramTypes) {
		boolean aquired = lock.acquire();
		if (!aquired) {
			return 0;
		}
		try {
			if (isNestedIO() || isInvalidStacktrace()) {
				return 0;
			}
			Class clazz = toClass(object);
			int id = toId(object);

			MethodSignature signature = new MethodSignature(clazz, resultType, method, paramTypes);
			SerializedOutput out = facade.serializeOutput(id, signature);
			for (ContextSnapshot snapshot : all()) {
				snapshot.addOutput(out);
			}
			return out.id();
		} finally {
			lock.release();
		}
	}

	public void outputArguments(int id, Object... arguments) {
		boolean aquired = lock.acquire();
		if (!aquired) {
			return;
		}
		try {
			if (id == 0) {
				return;
			}
			current().to((facade, session, snapshot) -> {
				snapshot.streamOutput().filter(out -> out.id() == id).forEach(out -> {
					out.updateArguments(facade.serialize(out.getArgumentTypes(), arguments, session));
				});
			});
		} finally {
			lock.release();
		}
	}

	public void outputResult(int id, Object result) {
		boolean aquired = lock.acquire();
		if (!aquired) {
			return;
		}
		try {
			if (id == 0) {
				return;
			}
			current().to((facade, session, snapshot) -> {
				snapshot.streamOutput().filter(out -> out.id() == id).forEach(out -> {
					out.updateResult(facade.serialize(out.getResultType(), result, session));
				});
			});
		} finally {
			lock.release();
		}
	}

	public void outputVoidResult(int id) {
		boolean aquired = lock.acquire();
		if (!aquired) {
			return;
		}
		try {
			if (id == 0) {
				return;
			}
			current().to((facade, session, snapshot) -> {
				snapshot.streamOutput().filter(out -> out.id() == id).forEach(out -> {
					out.updateResult(SerializedNull.VOID);
				});
			});
		} finally {
			lock.release();
		}
	}

	private Class toClass(Object object) {
		if (object instanceof Class) {
			return (Class) object;

		}
		return object.getClass();
	}

	private int toId(Object object) {
		return object instanceof Class ? SerializedInteraction.STATIC : identityHashCode(object);
	}

	public void expectVariables(Object self, String signature, Object result, Object... args) {
		boolean aquired = lock.acquire();
		if (!aquired) {
			return;
		}
		try {
			if (!matches(self, signature)) {
				return;
			}
			pop(signature).to((facade, session, snapshot) -> {
				if (self != null) {
					snapshot.setExpectThis(facade.serialize(self.getClass(), self, session));
				}
				snapshot.setExpectResult(facade.serialize(snapshot.getResultType(), result, session));
				snapshot.setExpectArgs(facade.serialize(snapshot.getArgumentTypes(), args, session));
				snapshot.setExpectGlobals(globalContext.globals(snapshot.getClassLoader()).stream()
					.map(field -> serializedGlobal(session, field))
					.toArray(SerializedField[]::new));
			}).andConsume(this::consume);
		} finally {
			lock.release();
		}
	}

	public void expectVariables(Object self, String signature, Object... args) {
		boolean aquired = lock.acquire();
		if (!aquired) {
			return;
		}
		try {
			if (!matches(self, signature)) {
				return;
			}
			pop(signature).to((facade, session, snapshot) -> {
				if (self != null) {
					snapshot.setExpectThis(facade.serialize(self.getClass(), self, session));
				}
				snapshot.setExpectArgs(facade.serialize(snapshot.getArgumentTypes(), args, session));
				snapshot.setExpectGlobals(globalContext.globals(snapshot.getClassLoader()).stream()
					.map(field -> serializedGlobal(session, field))
					.toArray(SerializedField[]::new));
			}).andConsume(this::consume);
		} finally {
			lock.release();
		}
	}

	public void throwVariables(Throwable throwable, Object self, String signature, Object... args) {
		boolean aquired = lock.acquire();
		if (!aquired) {
			return;
		}
		try {
			if (!matches(self, signature)) {
				return;
			}
			pop(signature).to((facade, session, snapshot) -> {
				if (self != null) {
					snapshot.setExpectThis(facade.serialize(self.getClass(), self, session));
				}
				snapshot.setExpectArgs(facade.serialize(snapshot.getArgumentTypes(), args, session));
				snapshot.setExpectException(facade.serialize(throwable.getClass(), throwable, session));
				snapshot.setExpectGlobals(globalContext.globals(snapshot.getClassLoader()).stream()
					.map(field -> serializedGlobal(session, field))
					.toArray(SerializedField[]::new));
			}).andConsume(this::consume);
		} finally {
			lock.release();
		}
	}

	protected void consume(ContextSnapshot snapshot) {
		if (snapshot.isValid()) {
			if (snapshotConsumer != null) {
				snapshotConsumer.accept(snapshot);
			}
		}
	}

	public boolean isInvalidStacktrace() {
		for (StackTraceElement element : currentThread().getStackTrace()) {
			if (STACKTRACE_VALIDATOR.isInvalid(element)) {
				return true;
			}
		}
		return false;
	}

	public boolean isNestedIO() {
		return peek()
			.map(snapshot -> {
				boolean inputPending = snapshot.lastInputSatitisfies(in -> !in.isComplete());
				boolean outputPending = snapshot.lastOutputSatitisfies(out -> !out.isComplete());
				return inputPending || outputPending;
			})
			.orElse(false);
	}

	private static Deque newStack() {
		return new ArrayDeque<>();
	}

	private SerializedField serializedGlobal(SerializerSession session, Field field) {
		Class declaringClass = field.getDeclaringClass();
		String name = field.getName();
		Class type = field.getType();
		FieldSignature signature = new FieldSignature(declaringClass, type, name);
		Object value = globalOf(field);

		SerializedValue serializedValue = facade.serialize(type, value, session);
		return new SerializedField(signature, serializedValue);
	}

	private Object globalOf(Field field) {
		try {
			return accessing(field).call(f -> f.get(null));
		} catch (ReflectiveOperationException e) {
			throw new SerializationException(e);
		}
	}

	public interface ContextSnapshotTransaction {

		ContextSnapshotTransaction to(SerializationTask task);

		void andConsume(Consumer consumer);

	}

	public static class DummyContextSnapshotTransaction implements ContextSnapshotTransaction {

		public static final DummyContextSnapshotTransaction INVALID = new DummyContextSnapshotTransaction();

		@Override
		public ContextSnapshotTransaction to(SerializationTask task) {
			return this;
		}

		@Override
		public void andConsume(Consumer consumer) {
		}

	}

	public static class ValidContextSnapshotTransaction implements ContextSnapshotTransaction {

		private ExecutorService snapshotExecutor;
		private long timeoutInMillis;
		private SerializerFacade facade;

		private ContextSnapshot snapshot;

		public ValidContextSnapshotTransaction(ExecutorService snapshotExecutor, long timeoutInMillis, SerializerFacade facade, ContextSnapshot snapshot) {
			this.snapshotExecutor = snapshotExecutor;
			this.timeoutInMillis = timeoutInMillis;
			this.facade = facade;
			this.snapshot = snapshot;
		}

		@Override
		public ContextSnapshotTransaction to(SerializationTask task) {
			if (currentThread().getThreadGroup() == RECORDING) {
				snapshot.invalidate();
			}
			if (!snapshot.isValid()) {
				return this;
			}
			SerializerSession session = facade.newSession();
			try {
				Future future = snapshotExecutor.submit(() -> {
					task.serialize(facade, session, snapshot);
				});
				future.get(timeoutInMillis, TimeUnit.MILLISECONDS);
				return this;
			} catch (InterruptedException | ExecutionException | TimeoutException | CancellationException e) {
				snapshot.invalidate();
				String profile = session.dumpProfiles().stream()
					.map(Profile::toString)
					.collect(joining("\n\t", "\n\t", ""));
				Logger.error("failed serializing " + snapshot + ", most time consuming types are:" + profile, e);
				return this;
			}
		}

		@Override
		public void andConsume(Consumer consumer) {
			consumer.accept(snapshot);
		}
	}

	public interface SerializationTask {
		void serialize(SerializerFacade facade, SerializerSession session, ContextSnapshot snapshot);
	}

	private static class StackTraceValidator {

		private Set classNames;

		StackTraceValidator() {
			classNames = new HashSet<>();
		}

		public boolean isInvalid(StackTraceElement element) {
			return classNames.contains(element.getClassName());
		}

		public StackTraceValidator invalidate(Class clazz) {
			classNames.add(clazz.getName());
			return this;
		}

	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy