[WAYANG-32] Base structure for Wayang Experiments Storage functionalities
diff --git a/wayang-commons/wayang-utils/pom.xml b/wayang-commons/wayang-utils/pom.xml
new file mode 100644
index 0000000..7768f19
--- /dev/null
+++ b/wayang-commons/wayang-utils/pom.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>wayang-commons</artifactId>
+ <groupId>org.apache.wayang</groupId>
+ <version>0.6.0-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>wayang-utils</artifactId>
+ <packaging>pom</packaging>
+
+ <modules>
+ <module>wayang-profile-db</module>
+ </modules>
+
+
+</project>
\ No newline at end of file
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/pom.xml b/wayang-commons/wayang-utils/wayang-profile-db/pom.xml
new file mode 100644
index 0000000..45bfaf9
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/pom.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>wayang-utils</artifactId>
+ <groupId>org.apache.wayang</groupId>
+ <version>0.6.0-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>wayang-profile-db</artifactId>
+
+ <dependencies>
+ <!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
+ <dependency>
+ <groupId>com.google.code.gson</groupId>
+ <artifactId>gson</artifactId>
+ </dependency>
+
+ </dependencies>
+
+
+</project>
\ No newline at end of file
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/readme.md b/wayang-commons/wayang-utils/wayang-profile-db/readme.md
new file mode 100644
index 0000000..8c14afc
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/readme.md
@@ -0,0 +1,3 @@
+Base on
+
+https://github.com/sekruse/profiledb-java.git
\ No newline at end of file
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/ProfileDB.java b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/ProfileDB.java
new file mode 100644
index 0000000..cd90d40
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/ProfileDB.java
@@ -0,0 +1,105 @@
+package profiledb;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import profiledb.json.MeasurementDeserializer;
+import profiledb.json.MeasurementSerializer;
+import profiledb.model.Experiment;
+import profiledb.model.Measurement;
+import profiledb.storage.Storage;
+
+import java.io.*;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * This class provides facilities to save and load {@link Experiment}s.
+ */
+public class ProfileDB {
+
+ /**
+ * Maintains the full list of {@link Class}es for {@link Measurement}s. Which are required for deserialization.
+ */
+ private List<Class<? extends Measurement>> measurementClasses = new LinkedList<>();
+
+ /**
+ * Controls how conducted experiments will be persisted and loaded
+ */
+ private Storage storage;
+
+ /**
+ * Maintains actions to preparate {@link Gson}.
+ */
+ private List<Consumer<GsonBuilder>> gsonPreparationSteps = new LinkedList<>();
+
+ /**
+ * Maintains a {@link Gson} object for efficiency. It will be dropped on changes, though.
+ */
+ private Gson gson;
+
+ /**
+ * Creates a new instance.
+ */
+ public ProfileDB(Storage storage) {
+
+ this.storage = storage;
+ this.storage.setContext(this);
+ //this.measurementClasses.add(TimeMeasurement.class);
+ }
+
+ /**
+ * To work with storage object provided to persist or load experiments
+ *
+ * @return Storage object proportioned for this instance
+ */
+ public Storage getStorage() {
+ return storage;
+ }
+
+ /**
+ * Register a {@link Measurement} type. This is required before being able to load that type.
+ *
+ * @param measurementClass the {@link Measurement} {@link Class}
+ * @return this instance
+ */
+ public ProfileDB registerMeasurementClass(Class<? extends Measurement> measurementClass) {
+ this.measurementClasses.add(measurementClass);
+ this.gson = null;
+ return this;
+ }
+
+ /**
+ * Apply any changes necessary to {@link Gson} so that it can be used for de/serialization of custom objects.
+ *
+ * @param preparation a preparatory step performed on a {@link GsonBuilder}
+ * @return this instance
+ */
+ public ProfileDB withGsonPreparation(Consumer<GsonBuilder> preparation) {
+ this.gsonPreparationSteps.add(preparation);
+ this.gson = null;
+ return this;
+ }
+
+ /**
+ * Provide a {@link Gson} object.
+ *
+ * @return the {@link Gson} object
+ */
+ public Gson getGson() {
+ if (this.gson == null) {
+ MeasurementSerializer measurementSerializer = new MeasurementSerializer();
+ MeasurementDeserializer measurementDeserializer = new MeasurementDeserializer();
+ this.measurementClasses.forEach(measurementDeserializer::register);
+ final GsonBuilder gsonBuilder = new GsonBuilder()
+ .registerTypeAdapter(Measurement.class, measurementDeserializer)
+ .registerTypeAdapter(Measurement.class, measurementSerializer);
+ this.gsonPreparationSteps.forEach(step -> step.accept(gsonBuilder));
+ this.gson = gsonBuilder.create();
+ }
+ return this.gson;
+ }
+
+}
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/json/MeasurementDeserializer.java b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/json/MeasurementDeserializer.java
new file mode 100644
index 0000000..cedecd8
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/json/MeasurementDeserializer.java
@@ -0,0 +1,40 @@
+package profiledb.json;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import profiledb.model.Measurement;
+
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Custom deserializer for {@link Measurement}s
+ * Detects actual subclass of serialized instances and then delegates the deserialization to that subtype.
+ */
+public class MeasurementDeserializer implements JsonDeserializer<Measurement> {
+
+ private final Map<String, Class<? extends Measurement>> measurementTypes = new HashMap<>();
+
+ public void register(Class<? extends Measurement> measurementClass) {
+ String typeName = Measurement.getTypeName(measurementClass);
+ this.measurementTypes.put(typeName, measurementClass);
+ }
+
+ @Override
+ public Measurement deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
+ final JsonElement typeElement = jsonElement.getAsJsonObject().get("type");
+ if (typeElement == null) {
+ throw new IllegalArgumentException("Missing type in " + jsonElement);
+ }
+ final String typeName = typeElement.getAsString();
+ final Class<? extends Measurement> measurementClass = this.measurementTypes.get(typeName);
+ if (measurementClass == null) {
+ throw new JsonParseException("Unknown measurement type: " + typeName);
+ }
+ return jsonDeserializationContext.deserialize(jsonElement, measurementClass);
+ }
+
+}
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/json/MeasurementSerializer.java b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/json/MeasurementSerializer.java
new file mode 100644
index 0000000..4f820e8
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/json/MeasurementSerializer.java
@@ -0,0 +1,23 @@
+package profiledb.json;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import profiledb.model.Measurement;
+
+import java.lang.reflect.Type;
+
+/**
+ * Custom serializer for {@link Measurement}s
+ * Detects actual subclass of given instances, encodes this class membership, and then delegates serialization to that subtype.
+ */
+public class MeasurementSerializer implements JsonSerializer<Measurement> {
+
+ @Override
+ public JsonElement serialize(Measurement measurement, Type type, JsonSerializationContext jsonSerializationContext) {
+ final JsonObject jsonObject = (JsonObject) jsonSerializationContext.serialize(measurement);
+ jsonObject.addProperty("type", measurement.getType());
+ return jsonObject;
+ }
+}
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/Experiment.java b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/Experiment.java
new file mode 100644
index 0000000..1222a75
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/Experiment.java
@@ -0,0 +1,163 @@
+package profiledb.model;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.Objects;
+
+/**
+ * An experiment comprises {@link Measurement}s from one specific {@link Subject} execution.
+ */
+public class Experiment {
+
+ /**
+ * Identifier for this instance.
+ */
+ private String id;
+
+ /**
+ * Description for this instance. (Optional)
+ */
+ private String description;
+
+ /**
+ * When this experiment has been started.
+ */
+ private long startTime;
+
+ /**
+ * Tags to group multiple Experiment instances. (Optional)
+ */
+ private Collection<String> tags;
+
+ /**
+ * {@link Measurement}s captured for this instance.
+ */
+ private Collection<Measurement> measurements;
+
+ /**
+ * The {@link Subject} being experimented with.
+ */
+ private Subject subject;
+
+ /**
+ * For deserialization.
+ */
+ private Experiment() {
+ }
+
+ /**
+ * Create a new instance that is starting right now.
+ *
+ * @param id Identifier for the new instance
+ * @param subject the {@link Subject}
+ * @param tags tags to group several experiments
+ */
+ public Experiment(String id, Subject subject, String... tags) {
+ this(id, subject, System.currentTimeMillis(), tags);
+ }
+
+ /**
+ * Create a new instance.
+ *
+ * @param id Identifier for the new instance
+ * @param subject the {@link Subject} of this experiment
+ * @param startTime start timestamp of this experiment
+ * @param tags tags to group several experiments
+ */
+ public Experiment(String id, Subject subject, long startTime, String... tags) {
+ this.id = id;
+ this.subject = subject;
+ this.startTime = startTime;
+ this.tags = Arrays.asList(tags);
+ this.measurements = new LinkedList<>();
+ }
+
+ /**
+ * Adds a description for this instance.
+ *
+ * @param description the description
+ * @return this instance
+ */
+ public Experiment withDescription(String description) {
+ this.description = description;
+ return this;
+ }
+
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public long getStartTime() {
+ return startTime;
+ }
+
+ public void setStartTime(long startTime) {
+ this.startTime = startTime;
+ }
+
+ public Collection<String> getTags() {
+ return tags;
+ }
+
+ public void setTags(Collection<String> tags) {
+ this.tags = tags;
+ }
+
+ public void addMeasurement(Measurement measurement) {
+ this.measurements.add(measurement);
+ }
+
+ public Collection<Measurement> getMeasurements() {
+ return measurements;
+ }
+
+ public Subject getSubject() {
+ return this.subject;
+ }
+
+ public void setSubject(Subject subject) {
+ this.subject = subject;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Experiment that = (Experiment) o;
+ return startTime == that.startTime &&
+ Objects.equals(id, that.id) &&
+ Objects.equals(description, that.description) &&
+ Objects.equals(tags, that.tags) &&
+ Objects.equals(subject, that.subject);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, startTime);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "%s[%s, %d tags, %d measurements]",
+ this.getClass().getSimpleName(),
+ this.id,
+ this.tags.size(),
+ this.measurements.size()
+ );
+ }
+}
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/Measurement.java b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/Measurement.java
new file mode 100644
index 0000000..23567a1
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/Measurement.java
@@ -0,0 +1,54 @@
+package profiledb.model;
+
+import java.util.Objects;
+
+/**
+ *
+ * Measurement captures the value of a metric at a specific time
+ */
+public abstract class Measurement {
+
+ private String id;
+
+ /**
+ * Returns implementation Class of this Measurement
+ */
+ public static String getTypeName(Class<? extends Measurement> measurementClass) {
+ return measurementClass.getDeclaredAnnotation(Type.class).value();
+ }
+
+ /**
+ * Deserialization constructor.
+ */
+ protected Measurement() {
+ }
+
+ public Measurement(String id) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getType() {
+ return getTypeName(this.getClass());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Measurement that = (Measurement) o;
+ return Objects.equals(id, that.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id);
+ }
+}
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/Subject.java b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/Subject.java
new file mode 100644
index 0000000..cfb6710
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/Subject.java
@@ -0,0 +1,69 @@
+package profiledb.model;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * The subject of an {@link Experiment}, e.g., an application or algorithm.
+ */
+public class Subject {
+
+ /**
+ * Identifier for the subject.
+ */
+ private String id;
+
+ /**
+ * Version of the subject.
+ */
+ private String version;
+
+ /**
+ * Configuration of this object.
+ */
+ private Map<String, Object> configuration = new HashMap<>();
+
+ /**
+ * Creates a new instance.
+ *
+ * @param id Identifier for the subject
+ * @param version To distinguish different versions among instances with the same {@code id}
+ */
+ public Subject(String id, String version) {
+ this.id = id;
+ this.version = version;
+ }
+
+ /**
+ * Adds a configuration.
+ *
+ * @param key Key of the configuration entry
+ * @param value Value for the new configuration entry; must be JSON-compatible, e.g. {@link Integer} or {@link String}
+ * @return this instance
+ */
+ public Subject addConfiguration(String key, Object value) {
+ this.configuration.put(key, value);
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s[%s:%s]", this.getClass().getSimpleName(), this.id, this.version);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Subject subject = (Subject) o;
+ return Objects.equals(id, subject.id) &&
+ Objects.equals(version, subject.version) &&
+ Objects.equals(configuration, subject.configuration);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, version, configuration);
+ }
+}
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/Type.java b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/Type.java
new file mode 100644
index 0000000..f96e5f2
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/Type.java
@@ -0,0 +1,13 @@
+package profiledb.model;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface Type {
+
+ String value();
+}
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/measurement/TimeMeasurement.java b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/measurement/TimeMeasurement.java
new file mode 100644
index 0000000..e73caa9
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/model/measurement/TimeMeasurement.java
@@ -0,0 +1,199 @@
+package profiledb.model.measurement;
+
+import profiledb.model.Measurement;
+import profiledb.model.Type;
+
+import java.util.Collection;
+import java.util.LinkedList;
+
+/**
+ * A {@link Measurement} that captures a certain amount of time in milliseconds. Instances can be nested within
+ * each other.
+ * <p>Besides storing those data, it also provides utility functionality to obtain measurements.</p>
+ */
+@Type("time")
+public class TimeMeasurement extends Measurement {
+
+ /**
+ * The measured time in milliseconds.
+ */
+ private long millis = 0L;
+
+ /**
+ * Keeps track on measurement starts.
+ */
+ private transient long startTime = -1L;
+
+ /**
+ * Sub-{@link TimeMeasurement}s of this instance.
+ */
+ private Collection<TimeMeasurement> rounds = new LinkedList<>();
+
+ /**
+ * Serialization constructor.
+ */
+ @SuppressWarnings("unused")
+ private TimeMeasurement() {
+ super();
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ * @param id the ID of the new instance
+ */
+ public TimeMeasurement(String id) {
+ super(id);
+ }
+
+ /**
+ * Start measuring time for this instance.
+ */
+ public void start() {
+ this.startTime = System.currentTimeMillis();
+ }
+
+ /**
+ * Ensure that this instance has its timer started.
+ */
+ private void ensureStarted() {
+ if (this.startTime == -1L) {
+ this.startTime = System.currentTimeMillis();
+ }
+ }
+
+ /**
+ * Start a (potentially new) sub-{@link TimeMeasurement}.
+ *
+ * @param identifiers identifies the target {@link TimeMeasurement} as a path of IDs
+ * @return the started instance
+ */
+ public TimeMeasurement start(String... identifiers) {
+ return this.start(identifiers, 0);
+ }
+
+ /**
+ * Start a (potentially new) sub-{@link TimeMeasurement}.
+ *
+ * @param identifiers identifies the target {@link TimeMeasurement} as a path of IDs
+ * @param index the index of this instance within {@code identifiers}
+ * @return the started instance
+ */
+ private TimeMeasurement start(String[] identifiers, int index) {
+ if (index >= identifiers.length) {
+ this.start();
+ return this;
+ } else {
+ this.ensureStarted();
+ TimeMeasurement round = this.getOrCreateRound(identifiers[index]);
+ return round.start(identifiers, index + 1);
+ }
+ }
+
+ /**
+ * Retrieves an existing {@link TimeMeasurement} from {@link #rounds} with the given {@code id} or creates and stores a new one.
+ *
+ * @param id the ID of the {@link TimeMeasurement}
+ * @return the {@link TimeMeasurement}
+ */
+ public TimeMeasurement getOrCreateRound(String id) {
+ TimeMeasurement round = this.getRound(id);
+ if (round != null) return round;
+
+ round = new TimeMeasurement(id);
+ this.rounds.add(round);
+ return round;
+ }
+
+ /**
+ * Retrieves an existing {@link TimeMeasurement} from {@link #rounds} with the given {@code id}.
+ *
+ * @param id the ID of the {@link TimeMeasurement}
+ * @return the {@link TimeMeasurement} or {@code null} if it does not exist
+ */
+ private TimeMeasurement getRound(String id) {
+ for (TimeMeasurement round : this.rounds) {
+ if (id.equals(round.getId())) {
+ return round;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Stop a measurement that has been started via {@link #start()} or derivatives.
+ */
+ public void stop() {
+ this.stop(System.currentTimeMillis());
+ }
+
+
+ /**
+ * Stop a measurement that has been started via {@link #start()} or derivatives.
+ *
+ * @param stopTime at which the measurement has been stopped
+ */
+ private void stop(long stopTime) {
+ if (this.startTime != -1L) {
+ this.millis += (stopTime - this.startTime);
+ this.startTime = -1L;
+ }
+ for (TimeMeasurement round : this.rounds) {
+ round.stop(stopTime);
+ }
+ }
+
+ /**
+ * Stop a measurement that has been started via {@link #start(String...)} or related.
+ *
+ * @param identfiers identify the target {@link TimeMeasurement} as a path of IDs
+ */
+ public void stop(String... identfiers) {
+ long stopTime = System.currentTimeMillis();
+ TimeMeasurement round = this;
+ for (String identfier : identfiers) {
+ round = round.getRound(identfier);
+ if (round == null) return;
+ }
+ round.stop(stopTime);
+ }
+
+ public long getMillis() {
+ return millis;
+ }
+
+ public void setMillis(long millis) {
+ this.millis = millis;
+ }
+
+ public Collection<TimeMeasurement> getRounds() {
+ return rounds;
+ }
+
+ public void addRounds(TimeMeasurement round) {
+ this.rounds.add(round);
+ }
+
+ /**
+ * Formats the given milliseconds as {@code h:MM:ss.mmm}.
+ *
+ * @param millis the milliseconds to format
+ * @return the formatted milliseconds
+ */
+ public static String formatDuration(long millis) {
+ if (millis < 0) return "-" + formatDuration(-millis);
+ long ms = millis % 1000;
+ millis /= 1000;
+ long s = millis % 60;
+ millis /= 60;
+ long m = millis % 60;
+ millis /= 60;
+ long h = millis % 60;
+ return String.format("%d:%02d:%02d.%03d", h, m, s, ms);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s[%s, %s, %d subs]", this.getClass().getSimpleName(), this.getId(), formatDuration(this.millis), this.rounds.size());
+ }
+}
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/storage/FileStorage.java b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/storage/FileStorage.java
new file mode 100644
index 0000000..4182dc5
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/storage/FileStorage.java
@@ -0,0 +1,101 @@
+package profiledb.storage;
+
+import profiledb.model.Experiment;
+
+import java.io.*;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedList;
+
+public class FileStorage extends Storage {
+
+ /**
+ * File where {@link Experiment}s will be written
+ */
+ private File file;
+
+ /**
+ * Assigns File where {@link Experiment}s will be written regarding to given URI
+ *
+ * @param uri URI where experiments are persisted
+ */
+ public FileStorage(URI uri) {
+
+ super(uri);
+ this.file = new File(uri);
+ }
+
+ /**
+ * To change target URI during execution
+ *
+ * @param uri determines new URI where {@link Experiment}s will be persisted
+ */
+ @Override
+ public void changeLocation(URI uri){
+
+ super.changeLocation(uri);
+ this.file = new File(uri);
+ }
+
+ /**
+ * Write {@link Experiment}s to a {@link File}. Existing file contents will be overwritten.
+ *
+ * @param experiments the {@link Experiment}s
+ * @throws IOException if the writing fails
+ */
+ @Override
+ public void save(Collection<Experiment> experiments) throws IOException {
+ this.file.getAbsoluteFile().getParentFile().mkdirs();
+ try (FileOutputStream fos = new FileOutputStream(this.file, false)) {
+ this.save(experiments, fos);
+ }
+ }
+
+ /**
+ * Write {@link Experiment}s to a {@link File}. Existing file contents will be overwritten.
+ *
+ * @param experiments the {@link Experiment}s
+ * @throws IOException if the writing fails
+ */
+ @Override
+ public void save(Experiment... experiments) throws IOException {
+ this.save(Arrays.asList(experiments));
+ }
+
+ /**
+ * Load {@link Experiment}s from a {@link File}.
+ *
+ * @return the {@link Experiment}s
+ */
+ @Override
+ public Collection<Experiment> load() throws IOException {
+ return load(new FileInputStream(this.file));
+ }
+
+ /**
+ * Append {@link Experiment}s to a {@link File}. Existing file contents will be preserved.
+ *
+ * @param experiments the {@link Experiment}s
+ * @throws IOException if the writing fails
+ */
+ @Override
+ public void append(Collection<Experiment> experiments) throws IOException {
+ this.file.getAbsoluteFile().getParentFile().mkdirs();
+ try (FileOutputStream fos = new FileOutputStream(this.file, true)) {
+ this.save(experiments, fos);
+ }
+ }
+
+ /**
+ * Append {@link Experiment}s to a {@link File}. Existing file contents will be preserved.
+ *
+ * @param experiments the {@link Experiment}s
+ * @throws IOException if the writing fails
+ */
+ @Override
+ public void append(Experiment... experiments) throws IOException {
+ this.append(Arrays.asList(experiments));
+ }
+
+}
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/storage/JDBCStorage.java b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/storage/JDBCStorage.java
new file mode 100644
index 0000000..ba847c1
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/storage/JDBCStorage.java
@@ -0,0 +1,90 @@
+package profiledb.storage;
+
+import profiledb.model.Experiment;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collection;
+
+public class JDBCStorage extends Storage {
+
+ //TODO: Implement JDBC connection
+
+ private File file;
+
+ public JDBCStorage(URI uri) {
+ super(uri);
+ this.file = new File(uri);
+ }
+
+ @Override
+ public void changeLocation(URI uri){
+
+ super.changeLocation(uri);
+ this.file = new File(uri);
+ }
+
+ /**
+ * Write {@link Experiment}s to a {@link File}. Existing file contents will be overwritten.
+ *
+ * @param experiments the {@link Experiment}s
+ * @throws IOException if the writing fails
+ */
+ @Override
+ public void save(Collection<Experiment> experiments) throws IOException {
+ this.file.getAbsoluteFile().getParentFile().mkdirs();
+ try (FileOutputStream fos = new FileOutputStream(this.file, false)) {
+ this.save(experiments, fos);
+ }
+ }
+
+ /**
+ * Write {@link Experiment}s to a {@link File}. Existing file contents will be overwritten.
+ *
+ * @param experiments the {@link Experiment}s
+ * @throws IOException if the writing fails
+ */
+ @Override
+ public void save(Experiment... experiments) throws IOException {
+ this.save(Arrays.asList(experiments));
+ }
+
+ /**
+ * Load {@link Experiment}s from a {@link File}.
+ *
+ * @return the {@link Experiment}s
+ */
+ @Override
+ public Collection<Experiment> load() throws IOException {
+ return load(new FileInputStream(this.file));
+ }
+
+ /**
+ * Append {@link Experiment}s to a {@link File}. Existing file contents will be preserved.
+ *
+ * @param experiments the {@link Experiment}s
+ * @throws IOException if the writing fails
+ */
+ @Override
+ public void append(Collection<Experiment> experiments) throws IOException {
+ this.file.getAbsoluteFile().getParentFile().mkdirs();
+ try (FileOutputStream fos = new FileOutputStream(this.file, true)) {
+ this.save(experiments, fos);
+ }
+ }
+
+ /**
+ * Append {@link Experiment}s to a {@link File}. Existing file contents will be preserved.
+ *
+ * @param experiments the {@link Experiment}s
+ * @throws IOException if the writing fails
+ */
+ @Override
+ public void append(Experiment... experiments) throws IOException {
+ this.append(Arrays.asList(experiments));
+ }
+}
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/storage/Storage.java b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/storage/Storage.java
new file mode 100644
index 0000000..cdda15b
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/main/java/profiledb/storage/Storage.java
@@ -0,0 +1,122 @@
+package profiledb.storage;
+
+import com.google.gson.Gson;
+import profiledb.ProfileDB;
+import profiledb.model.Experiment;
+
+import java.io.*;
+import java.net.URI;
+import java.util.Collection;
+import java.util.LinkedList;
+
+/**
+ * Controls how conducted experiments will be persisted and loaded
+ */
+public abstract class Storage {
+
+ /**
+ * Object or URI where experiments are persisted
+ */
+ private URI storageFile;
+
+ /**
+ * To access profileDB general serialization functions
+ */
+ private ProfileDB context;
+
+ /**
+ * Creates a new instance.
+ * @param uri Object or URI where experiments are persisted
+ */
+ public Storage(URI uri){
+ this.storageFile = uri;
+ }
+
+ /**
+ * Sets the ProfileDB for this instance that manages all the Measurement subclasses
+ * */
+ public void setContext(ProfileDB context) {
+ this.context = context;
+ }
+
+ /**
+ * Allows to change where future experiments will be persisted and loaded
+ * @param uri
+ */
+ public void changeLocation(URI uri){
+ this.storageFile = uri;
+ }
+
+ public void save(Experiment... experiments) throws IOException {}
+
+ public void save(Collection<Experiment> experiments) throws IOException {}
+
+ public void append(Experiment... experiments) throws IOException {}
+
+ public void append(Collection<Experiment> experiments) throws IOException {}
+
+ public Collection<Experiment> load() throws IOException { return null; }
+
+ /**
+ * Write {@link Experiment}s to an {@link OutputStream}.
+ *
+ * @param outputStream the {@link OutputStream}
+ */
+ public void save(Collection<Experiment> experiments, OutputStream outputStream) throws IOException {
+ try {
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, "UTF-8"));
+ this.save(experiments, writer);
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("Unexpectedly, UTF-8 is not supported.");
+ }
+ }
+
+ /**
+ * Write {@link Experiment}s to a {@link Writer}.
+ *
+ * @param writer the {@link Writer}
+ */
+ public void save(Collection<Experiment> experiments, Writer writer) throws IOException {
+ try {
+ Gson gson = context.getGson();
+ for (Experiment experiment : experiments) {
+ gson.toJson(experiment, writer);
+ writer.append('\n');
+ }
+ writer.flush();
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("Unexpectedly, UTF-8 is not supported.");
+ }
+ }
+
+ /**
+ * Load {@link Experiment}s from an {@link InputStream}.
+ *
+ * @param inputStream the {@link InputStream}
+ * @return the {@link Experiment}s
+ */
+ public Collection<Experiment> load(InputStream inputStream) throws IOException {
+ try {
+ return load(new BufferedReader(new InputStreamReader(inputStream, "UTF-8")));
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("Unexpectedly, UTF-8 is not supported.");
+ }
+ }
+
+ /**
+ * Load {@link Experiment}s from an {@link Reader}.
+ *
+ * @param reader the {@link Reader}
+ * @return the {@link Experiment}s
+ */
+ public Collection<Experiment> load(BufferedReader reader) throws IOException {
+ Collection<Experiment> experiments = new LinkedList<>();
+ Gson gson = context.getGson();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ Experiment experiment = gson.fromJson(line, Experiment.class);
+ experiments.add(experiment);
+ }
+ return experiments;
+ }
+}
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/test/java/profiledb/ProfileDBTest.java b/wayang-commons/wayang-utils/wayang-profile-db/src/test/java/profiledb/ProfileDBTest.java
new file mode 100644
index 0000000..da05470
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/test/java/profiledb/ProfileDBTest.java
@@ -0,0 +1,199 @@
+package profiledb;
+
+import org.junit.Assert;
+import org.junit.Test;
+import profiledb.measurement.TestMemoryMeasurement;
+import profiledb.measurement.TestTimeMeasurement;
+import profiledb.model.Experiment;
+import profiledb.model.Measurement;
+import profiledb.model.Subject;
+import profiledb.storage.FileStorage;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.util.*;
+
+public class ProfileDBTest {
+
+ @Test
+ public void testPolymorphSaveAndLoad() throws IOException {
+
+ try {
+ URI uri = new URI("file:///Users/rodrigopardomeza/Desktop/random/myfile.txt");
+ FileStorage store = new FileStorage(uri);
+
+ ProfileDB profileDB = new ProfileDB(store)
+ .registerMeasurementClass(TestMemoryMeasurement.class)
+ .registerMeasurementClass(TestTimeMeasurement.class);
+
+ /**
+ * Esto es lo que se espera del codigo del cliente
+ * Tiene que usar la API para registrar medidas
+ */
+ // crea un experimento falso
+ final Experiment experiment = new Experiment("test-xp", new Subject("PageRank", "1.0"), "test experiment");
+
+ // Agrega medidas falsas hardcoded
+ Measurement timeMeasurement = new TestTimeMeasurement("exec-time", 12345L);
+ Measurement memoryMeasurement = new TestMemoryMeasurement("exec-time", System.currentTimeMillis(), 54321L);
+
+ /*Agrega las medidas al experimento*/
+ experiment.addMeasurement(timeMeasurement);
+ experiment.addMeasurement(memoryMeasurement);
+
+ // Save the experiment.
+ /**
+ * Guarda el experimento en memoria
+ */
+ byte[] buffer;
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ profileDB.getStorage().save(Collections.singleton(experiment), bos);
+ bos.close();
+ buffer = bos.toByteArray();
+ System.out.println("Buffer contents: " + new String(buffer, "UTF-8"));
+
+ // Load the experiment.
+ /**
+ * Lee el experimento desde el buffer en memoria
+ */
+ ByteArrayInputStream bis = new ByteArrayInputStream(buffer);
+ Collection<Experiment> loadedExperiments = profileDB.getStorage().load(bis);
+
+ // Compare the experiments.
+ Assert.assertEquals(1, loadedExperiments.size());
+ Experiment loadedExperiment = loadedExperiments.iterator().next();
+ Assert.assertEquals(experiment, loadedExperiment);
+
+ // Compare the measurements.
+ Assert.assertEquals(2, loadedExperiment.getMeasurements().size());
+ Set<Measurement> expectedMeasurements = new HashSet<>(2);
+ expectedMeasurements.add(timeMeasurement);
+ expectedMeasurements.add(memoryMeasurement);
+ Set<Measurement> loadedMeasurements = new HashSet<>(loadedExperiment.getMeasurements());
+ Assert.assertEquals(expectedMeasurements, loadedMeasurements);
+
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Test
+ public void testRecursiveSaveAndLoad() throws IOException {
+ try {
+ URI uri = new URI("file:///Users/rodrigopardomeza/Desktop/random/myfile.txt");
+ FileStorage store = new FileStorage(uri);
+
+ ProfileDB profileDB = new ProfileDB(store)
+ .registerMeasurementClass(TestMemoryMeasurement.class)
+ .registerMeasurementClass(TestTimeMeasurement.class);
+
+ // Create an example experiment.
+ final Experiment experiment = new Experiment("test-xp", new Subject("PageRank", "1.0"), "test experiment");
+ TestTimeMeasurement topLevelMeasurement = new TestTimeMeasurement("exec-time", 12345L);
+ TestTimeMeasurement childMeasurement = new TestTimeMeasurement("sub-exec-time", 2345L);
+ topLevelMeasurement.addSubmeasurements(childMeasurement);
+ experiment.addMeasurement(topLevelMeasurement);
+
+ // Save the experiment.
+ byte[] buffer;
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ profileDB.getStorage().save(Collections.singleton(experiment), bos);
+ bos.close();
+ buffer = bos.toByteArray();
+ System.out.println("Buffer contents: " + new String(buffer, "UTF-8"));
+
+ // Load the experiment.
+ ByteArrayInputStream bis = new ByteArrayInputStream(buffer);
+ Collection<Experiment> loadedExperiments = profileDB.getStorage().load(bis);
+
+ // Compare the experiments.
+ Assert.assertEquals(1, loadedExperiments.size());
+ Experiment loadedExperiment = loadedExperiments.iterator().next();
+ Assert.assertEquals(experiment, loadedExperiment);
+
+ // Compare the measurements.
+ Assert.assertEquals(1, loadedExperiment.getMeasurements().size());
+ final Measurement loadedMeasurement = loadedExperiment.getMeasurements().iterator().next();
+ Assert.assertEquals(topLevelMeasurement, loadedMeasurement);
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Test
+ public void testFileOperations() throws IOException {
+
+ try {
+ URI uri = new URI("file:///Users/rodrigopardomeza/Desktop/random/myfile.txt");
+ FileStorage store = new FileStorage(uri);
+
+ ProfileDB profileDB = new ProfileDB(store)
+ .registerMeasurementClass(TestMemoryMeasurement.class)
+ .registerMeasurementClass(TestTimeMeasurement.class);
+
+ // Create example experiments.
+ final Experiment experiment1 = new Experiment("xp1", new Subject("PageRank", "1.0"), "test experiment 1");
+ experiment1.addMeasurement(new TestTimeMeasurement("exec-time", 1L));
+ final Experiment experiment2 = new Experiment("xp2", new Subject("KMeans", "1.1"), "test experiment 2");
+ experiment2.addMeasurement(new TestTimeMeasurement("exec-time", 2L));
+ final Experiment experiment3 = new Experiment("xp3", new Subject("Apriori", "2.0"), "test experiment 3");
+ experiment3.addMeasurement(new TestMemoryMeasurement("ram", System.currentTimeMillis(), 3L));
+
+ // Save the experiments.
+ File tempDir = Files.createTempDirectory("profiledb").toFile();
+ File file = new File(tempDir, "profiledb.json");
+ profileDB.getStorage().save(experiment1);
+ profileDB.getStorage().append(experiment2, experiment3);
+
+ Files.lines(file.toPath()).forEach(System.out::println);
+
+ // Load and compare.
+ final Set<Experiment> loadedExperiments = new HashSet<>(profileDB.getStorage().load());
+ final List<Experiment> expectedExperiments = Arrays.asList(experiment1, experiment2, experiment3);
+ Assert.assertEquals(expectedExperiments.size(), loadedExperiments.size());
+ Assert.assertEquals(new HashSet<>(expectedExperiments), new HashSet<>(loadedExperiments));
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Test
+ public void testAppendOnNonExistentFile() throws IOException {
+
+ try {
+ URI uri = new URI("file:///Users/rodrigopardomeza/Desktop/random/myfile.txt");
+ FileStorage store = new FileStorage(uri);
+
+ // This seems to be an issue on Linux.
+ ProfileDB profileDB = new ProfileDB(store)
+ .registerMeasurementClass(TestMemoryMeasurement.class)
+ .registerMeasurementClass(TestTimeMeasurement.class);
+
+ // Create example experiments.
+ final Experiment experiment1 = new Experiment("xp1", new Subject("PageRank", "1.0"), "test experiment 1");
+ experiment1.addMeasurement(new TestTimeMeasurement("exec-time", 1L));
+
+ // Save the experiments.
+ File tempDir = Files.createTempDirectory("profiledb").toFile();
+ File file = new File(tempDir, "new-profiledb.json");
+ Assert.assertTrue(!file.exists() || file.delete());
+ profileDB.getStorage().append(experiment1);
+
+ Files.lines(file.toPath()).forEach(System.out::println);
+
+ // Load and compare.
+ final Set<Experiment> loadedExperiments = new HashSet<>(profileDB.getStorage().load());
+ final List<Experiment> expectedExperiments = Collections.singletonList(experiment1);
+ Assert.assertEquals(expectedExperiments.size(), loadedExperiments.size());
+ Assert.assertEquals(new HashSet<>(expectedExperiments), new HashSet<>(loadedExperiments));
+ } catch (URISyntaxException e) {
+ e.printStackTrace();
+ }
+ }
+
+}
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/test/java/profiledb/measurement/TestMemoryMeasurement.java b/wayang-commons/wayang-utils/wayang-profile-db/src/test/java/profiledb/measurement/TestMemoryMeasurement.java
new file mode 100644
index 0000000..200a661
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/test/java/profiledb/measurement/TestMemoryMeasurement.java
@@ -0,0 +1,62 @@
+package profiledb.measurement;
+
+import profiledb.model.Measurement;
+import profiledb.model.Type;
+
+import java.util.Objects;
+
+/**
+ * {@link Measurement} implementation for test purposes.
+ */
+@Type("test-mem")
+public class TestMemoryMeasurement extends Measurement {
+
+ private long timestamp;
+
+ private long usedMb;
+
+ public TestMemoryMeasurement(String id, long timestamp, long usedMb) {
+ super(id);
+ this.timestamp = timestamp;
+ this.usedMb = usedMb;
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(long timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public long getUsedMb() {
+ return usedMb;
+ }
+
+ public void setUsedMb(long usedMb) {
+ this.usedMb = usedMb;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ if (!super.equals(o)) return false;
+ TestMemoryMeasurement that = (TestMemoryMeasurement) o;
+ return timestamp == that.timestamp &&
+ usedMb == that.usedMb;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), timestamp, usedMb);
+ }
+
+ @Override
+ public String toString() {
+ return "TestMemoryMeasurement{" +
+ "timestamp=" + timestamp +
+ ", usedMb=" + usedMb +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/wayang-commons/wayang-utils/wayang-profile-db/src/test/java/profiledb/measurement/TestTimeMeasurement.java b/wayang-commons/wayang-utils/wayang-profile-db/src/test/java/profiledb/measurement/TestTimeMeasurement.java
new file mode 100644
index 0000000..2f2b037
--- /dev/null
+++ b/wayang-commons/wayang-utils/wayang-profile-db/src/test/java/profiledb/measurement/TestTimeMeasurement.java
@@ -0,0 +1,56 @@
+package profiledb.measurement;
+
+import profiledb.model.Measurement;
+import profiledb.model.Type;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.Objects;
+
+/**
+ * {@link Measurement} implementation for test purposes.
+ */
+@Type("test-time")
+public class TestTimeMeasurement extends Measurement {
+
+ private long millis;
+
+ private Collection<Measurement> submeasurements;
+
+ public TestTimeMeasurement(String id, long millis) {
+ super(id);
+ this.millis = millis;
+ this.submeasurements = new LinkedList<>();
+ }
+
+ public long getMillis() {
+ return millis;
+ }
+
+ public void setMillis(long millis) {
+ this.millis = millis;
+ }
+
+ public Collection<Measurement> getSubmeasurements() {
+ return submeasurements;
+ }
+
+ public void addSubmeasurements(TestTimeMeasurement submeasurements) {
+ this.submeasurements.add(submeasurements);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ if (!super.equals(o)) return false;
+ TestTimeMeasurement that = (TestTimeMeasurement) o;
+ return millis == that.millis &&
+ Objects.equals(submeasurements, that.submeasurements);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), millis, submeasurements);
+ }
+}
\ No newline at end of file