You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@wayang.apache.org by rp...@apache.org on 2021/08/30 11:02:37 UTC

[incubator-wayang] branch profile-db created (now 5344336)

This is an automated email from the ASF dual-hosted git repository.

rpardomeza pushed a change to branch profile-db
in repository https://gitbox.apache.org/repos/asf/incubator-wayang.git.


      at 5344336  [WAYANG-32] Base structure for Wayang Experiments Storage functionalities

This branch includes the following new commits:

     new 5344336  [WAYANG-32] Base structure for Wayang Experiments Storage functionalities

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


[incubator-wayang] 01/01: [WAYANG-32] Base structure for Wayang Experiments Storage functionalities

Posted by rp...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rpardomeza pushed a commit to branch profile-db
in repository https://gitbox.apache.org/repos/asf/incubator-wayang.git

commit 5344336f68bb9038e701435e9859321b6e8cbcfc
Author: rodrigopardomeza <ro...@gmail.com>
AuthorDate: Mon Aug 30 13:00:09 2021 +0200

    [WAYANG-32] Base structure for Wayang Experiments Storage functionalities
---
 wayang-commons/wayang-utils/pom.xml                |  20 +++
 .../wayang-utils/wayang-profile-db/pom.xml         |  24 +++
 .../wayang-utils/wayang-profile-db/readme.md       |   3 +
 .../src/main/java/profiledb/ProfileDB.java         | 105 +++++++++++
 .../profiledb/json/MeasurementDeserializer.java    |  40 +++++
 .../java/profiledb/json/MeasurementSerializer.java |  23 +++
 .../src/main/java/profiledb/model/Experiment.java  | 163 +++++++++++++++++
 .../src/main/java/profiledb/model/Measurement.java |  54 ++++++
 .../src/main/java/profiledb/model/Subject.java     |  69 +++++++
 .../src/main/java/profiledb/model/Type.java        |  13 ++
 .../model/measurement/TimeMeasurement.java         | 199 +++++++++++++++++++++
 .../main/java/profiledb/storage/FileStorage.java   | 101 +++++++++++
 .../main/java/profiledb/storage/JDBCStorage.java   |  90 ++++++++++
 .../src/main/java/profiledb/storage/Storage.java   | 122 +++++++++++++
 .../src/test/java/profiledb/ProfileDBTest.java     | 199 +++++++++++++++++++++
 .../measurement/TestMemoryMeasurement.java         |  62 +++++++
 .../profiledb/measurement/TestTimeMeasurement.java |  56 ++++++
 17 files changed, 1343 insertions(+)

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