tmf: Add proper public methods to internal.tmf.core.statesystem
authorAlexandre Montplaisir <alexmonthy@voxpopuli.im>
Fri, 13 Dec 2013 22:01:09 +0000 (17:01 -0500)
committerAlexandre Montplaisir <alexmonthy@voxpopuli.im>
Fri, 10 Jan 2014 22:59:15 +0000 (17:59 -0500)
Those are already in internal packages, so they will not be API methods,
but by making them public, it makes it possible to access them directly for
unit testing.

Change-Id: I320317302ed6d6ccfc414d3666d2b657c1cd3207
Signed-off-by: Alexandre Montplaisir <alexmonthy@voxpopuli.im>
Reviewed-on: https://git.eclipse.org/r/19884
Tested-by: Hudson CI
Reviewed-by: Matthew Khouzam <matthew.khouzam@ericsson.com>
IP-Clean: Matthew Khouzam <matthew.khouzam@ericsson.com>

org.eclipse.linuxtools.tmf.core/src/org/eclipse/linuxtools/internal/tmf/core/statesystem/Attribute.java
org.eclipse.linuxtools.tmf.core/src/org/eclipse/linuxtools/internal/tmf/core/statesystem/AttributeTree.java
org.eclipse.linuxtools.tmf.core/src/org/eclipse/linuxtools/internal/tmf/core/statesystem/TransientState.java

index fec8dc4c1fe416b17a329c31cf927f25b13c6b0a..1f9859f23f1e798c6d67c86833f21e5d7bb26692 100644 (file)
@@ -30,43 +30,65 @@ import java.util.Map;
  * @author alexmont
  *
  */
-abstract class Attribute {
+public abstract class Attribute {
 
     private final Attribute parent;
     private final String name;
     private final int quark;
+
+    /** The list of sub-attributes */
     protected final List<Attribute> subAttributes;
 
     /**
      * Constructor
+     *
+     * @param parent
+     *            The parent attribute of this one. Can be 'null' to represent
+     *            this attribute is the root node of the tree.
+     * @param name
+     *            Base name of this attribute
+     * @param quark
+     *            The integer representation of this attribute
      */
-    Attribute(Attribute parent, String name, int quark) {
+    public Attribute(Attribute parent, String name, int quark) {
         this.parent = parent;
         this.quark = quark;
         this.name = name;
         this.subAttributes = new ArrayList<>();
     }
 
+    // ------------------------------------------------------------------------
+    // Accessors
+    // ------------------------------------------------------------------------
+
     /**
-     * @name Accessors
+     * Get the quark (integer representation) of this attribute.
+     *
+     * @return The quark of this attribute
      */
-
-    int getQuark() {
+    public int getQuark() {
         return quark;
     }
 
-    Attribute getParent() {
-        return parent;
+    /**
+     * Get the name of this attribute.
+     *
+     * @return The name of this attribute
+     */
+    public String getName() {
+        return name;
     }
 
-    List<Attribute> getSubAttributes() {
+    /**
+     * Get the list of child attributes below this one. This is a read-only
+     * view.
+     *
+     * @return The list of child attributes.
+     */
+    public List<Attribute> getSubAttributes() {
         return Collections.unmodifiableList(subAttributes);
     }
 
-    String getName() {
-        return name;
-    }
-
     /**
      * Get the matching quark for a given path-of-strings
      *
@@ -74,7 +96,7 @@ abstract class Attribute {
      *            The path we are looking for, *relative to this node*.
      * @return The matching quark, or -1 if that attribute does not exist.
      */
-    int getSubAttributeQuark(String... path) {
+    public int getSubAttributeQuark(String... path) {
         return this.getSubAttributeQuark(path, 0);
     }
 
@@ -89,7 +111,7 @@ abstract class Attribute {
      * @return The Node object matching the last element in the path, or "null"
      *         if that attribute does not exist.
      */
-    Attribute getSubAttributeNode(String... path) {
+    public Attribute getSubAttributeNode(String... path) {
         return this.getSubAttributeNode(path, 0);
     }
 
@@ -108,8 +130,25 @@ abstract class Attribute {
     }
 
     /* The methods how to access children are left to derived classes */
-    abstract void addSubAttribute(Attribute newSubAttribute);
-    abstract Attribute getSubAttributeNode(String[] path, int index);
+
+    /**
+     * Add a sub-attribute to this attribute
+     *
+     * @param newSubAttribute The new attribute to add
+     */
+    protected abstract void addSubAttribute(Attribute newSubAttribute);
+
+    /**
+     * Get a sub-attribute from this node's sub-attributes
+     *
+     * @param path
+     *            The *full* path to the attribute
+     * @param index
+     *            The index in 'path' where this attribute is located
+     *            (indicating where to start searching).
+     * @return The requested attribute
+     */
+    protected abstract Attribute getSubAttributeNode(String[] path, int index);
 
     /**
      * Return a String array composed of the full (absolute) path representing
@@ -117,14 +156,14 @@ abstract class Attribute {
      *
      * @return
      */
-    String[] getFullAttribute() {
+    private String[] getFullAttribute() {
         LinkedList<String> list = new LinkedList<>();
         Attribute curNode = this;
 
         /* Add recursive parents to the list, but stop at the root node */
-        while (curNode.getParent() != null) {
+        while (curNode.parent != null) {
             list.addFirst(curNode.getName());
-            curNode = curNode.getParent();
+            curNode = curNode.parent;
         }
 
         return list.toArray(new String[0]);
@@ -134,9 +173,9 @@ abstract class Attribute {
      * Return the absolute path of this attribute, as a single slash-separated
      * String.
      *
-     * @return
+     * @return The full name of this attribute
      */
-    String getFullAttributeName() {
+    public String getFullAttributeName() {
         String[] array = this.getFullAttribute();
         StringBuffer buf = new StringBuffer();
 
@@ -176,7 +215,13 @@ abstract class Attribute {
         return;
     }
 
-    void debugPrint(PrintWriter writer) {
+    /**
+     * Debugging method to print the contents of this attribute
+     *
+     * @param writer
+     *            PrintWriter where to write the information
+     */
+    public void debugPrint(PrintWriter writer) {
         /* Only used for debugging, shouldn't be externalized */
         writer.println("------------------------------"); //$NON-NLS-1$
         writer.println("Attribute tree: (quark)\n"); //$NON-NLS-1$
@@ -196,15 +241,15 @@ abstract class Attribute {
  */
 final class AlphaNumAttribute extends Attribute {
 
-    private Map<String, Integer> subAttributesMap;
+    private final Map<String, Integer> subAttributesMap;
 
-    AlphaNumAttribute(Attribute parent, String name, int quark) {
+    public AlphaNumAttribute(Attribute parent, String name, int quark) {
         super(parent, name, quark);
         this.subAttributesMap = new HashMap<>();
     }
 
     @Override
-    synchronized void addSubAttribute(Attribute newSubAttribute) {
+    protected synchronized void addSubAttribute(Attribute newSubAttribute) {
         assert (newSubAttribute != null);
         assert (newSubAttribute.getName() != null);
         /* This should catch buggy state changing statements */
index 73ced1eec2a04bb98cea4f3e5adf83881568cafc..05142f74c63c6849cb187d5cfab699cf65bf91ce 100644 (file)
@@ -46,7 +46,7 @@ public final class AttributeTree {
      * @param ss
      *            The StateSystem to which this AT is attached
      */
-    AttributeTree(StateSystem ss) {
+    public AttributeTree(StateSystem ss) {
         this.ss = ss;
         this.attributeList = Collections.synchronizedList(new ArrayList<Attribute>());
         this.attributeTreeRoot = new AlphaNumAttribute(null, "root", -1); //$NON-NLS-1$
@@ -62,8 +62,9 @@ public final class AttributeTree {
      *            File stream where to read the AT information. Make sure it's
      *            sought at the right place!
      * @throws IOException
+     *             If there is a problem reading from the file stream
      */
-    AttributeTree(StateSystem ss, FileInputStream fis) throws IOException {
+    public AttributeTree(StateSystem ss, FileInputStream fis) throws IOException {
         this(ss);
         DataInputStream in = new DataInputStream(new BufferedInputStream(fis));
 
@@ -134,15 +135,15 @@ public final class AttributeTree {
     }
 
     /**
-     * Tell the Attribute Tree to write itself somewhere. The passed
-     * FileOutputStream defines where (which file/position).
+     * Tell the Attribute Tree to write itself somewhere in a file.
      *
-     * @param fos
-     *            Where to write. Make sure it's sought at the right position
-     *            you want.
+     * @param file
+     *            The file to write to
+     * @param pos
+     *            The position (in bytes) in the file where to write
      * @return The total number of bytes written.
      */
-    int writeSelf(File file, long pos) {
+    public int writeSelf(File file, long pos) {
         int total = 0;
         byte[] curByteArray;
 
@@ -193,24 +194,26 @@ public final class AttributeTree {
      * this also equals the integer value (quark) the next added attribute will
      * have.
      *
-     * @return
+     * @return The current number of attributes in the tree
      */
-    int getNbAttributes() {
+    public int getNbAttributes() {
         return attributeList.size();
     }
 
     /**
-     * This is the version to specifically add missing attributes.
-     *
-     * If 'numericalNode' is true, all the new attributes created will be of
-     * type 'NumericalNode' instead of 'AlphaNumNode'. Be careful with this, if
-     * you do not want ALL added attributes to be numerical, call this function
-     * first with 'false' to create the parent nodes, then call it again to make
-     * sure only the final node is numerical.
+     * Get the quark for a given attribute path. No new attribute will be
+     * created : if the specified path does not exist, throw an error.
      *
+     * @param startingNodeQuark
+     *            The quark of the attribute from which relative queries will
+     *            start. Use '-1' to start at the root node.
+     * @param subPath
+     *            The path to the attribute, relative to the starting node.
+     * @return The quark of the specified attribute
      * @throws AttributeNotFoundException
+     *             If the specified path was not found
      */
-    int getQuarkDontAdd(int startingNodeQuark, String... subPath)
+    public int getQuarkDontAdd(int startingNodeQuark, String... subPath)
             throws AttributeNotFoundException {
         assert (startingNodeQuark >= -1);
 
@@ -243,9 +246,21 @@ public final class AttributeTree {
         return knownQuark;
     }
 
-    // FIXME synchronized here is probably quite costly... maybe only locking
-    // the "for" would be enough?
-    synchronized int getQuarkAndAdd(int startingNodeQuark, String... subPath) {
+    /**
+     * Get the quark of a given attribute path. If that specified path does not
+     * exist, it will be created (and the quark that was just created will be
+     * returned).
+     *
+     * @param startingNodeQuark
+     *            The quark of the attribute from which relative queries will
+     *            start. Use '-1' to start at the root node.
+     * @param subPath
+     *            The path to the attribute, relative to the starting node.
+     * @return The quark of the attribute represented by the path
+     */
+    public synchronized int getQuarkAndAdd(int startingNodeQuark, String... subPath) {
+        // FIXME synchronized here is probably quite costly... maybe only locking
+        // the "for" would be enough?
         assert (subPath != null && subPath.length > 0);
         assert (startingNodeQuark >= -1);
 
@@ -305,19 +320,21 @@ public final class AttributeTree {
         return knownQuark;
     }
 
-    int getSubAttributesCount(int quark) {
-        return attributeList.get(quark).getSubAttributes().size();
-    }
-
     /**
      * Returns the sub-attributes of the quark passed in parameter
      *
      * @param attributeQuark
+     *            The quark of the attribute to print the sub-attributes of.
      * @param recursive
-     * @return
+     *            Should the query be recursive or not? If false, only children
+     *            one level deep will be returned. If true, all descendants will
+     *            be returned (depth-first search)
+     * @return The list of quarks representing the children attributes
      * @throws AttributeNotFoundException
+     *             If 'attributeQuark' is invalid, or if there is no attrbiute
+     *             associated to it.
      */
-    List<Integer> getSubAttributes(int attributeQuark, boolean recursive)
+    public List<Integer> getSubAttributes(int attributeQuark, boolean recursive)
             throws AttributeNotFoundException {
         List<Integer> listOfChildren = new ArrayList<>();
         Attribute startingAttribute;
@@ -350,19 +367,39 @@ public final class AttributeTree {
         }
     }
 
-    String getAttributeName(int quark) {
+    /**
+     * Get then base name of an attribute specified by a quark.
+     *
+     * @param quark
+     *            The quark of the attribute
+     * @return The (base) name of the attribute
+     */
+    public String getAttributeName(int quark) {
         return attributeList.get(quark).getName();
     }
 
-    String getFullAttributeName(int quark) {
+    /**
+     * Get the full path name of an attribute specified by a quark.
+     *
+     * @param quark
+     *            The quark of the attribute
+     * @return The full path name of the attribute
+     */
+    public String getFullAttributeName(int quark) {
         if (quark >= attributeList.size() || quark < 0) {
             return null;
         }
         return attributeList.get(quark).getFullAttributeName();
     }
 
-    void debugPrint(PrintWriter writer) {
+    /**
+     * Debug-print all the attributes in the tree.
+     *
+     * @param writer
+     *            The writer where to print the output
+     */
+    public void debugPrint(PrintWriter writer) {
         attributeTreeRoot.debugPrint(writer);
     }
 
-}
\ No newline at end of file
+}
index 84bfb247473483351b0d3d3b6b123e88cbf64715..ff08a7c20e8d5bf0b7aa70e4c6d8ebedd8abf398 100644 (file)
@@ -27,19 +27,18 @@ import org.eclipse.linuxtools.tmf.core.statevalue.TmfStateValue;
 import org.eclipse.linuxtools.tmf.core.statevalue.ITmfStateValue.Type;
 
 /**
- * The Transient State is used to build intervals from punctual state changes. It
- * contains a "state info" vector similar to the "current state", except here we
- * also record the start time of every state stored in it.
+ * The Transient State is used to build intervals from punctual state changes.
+ * It contains a "state info" vector similar to the "current state", except here
+ * we also record the start time of every state stored in it.
  *
- * We can then build StateInterval's, to be inserted in the State History when
- * we detect state changes : the "start time" of the interval will be the
- * recorded time we have here, and the "end time" will be the timestamp of the
- * new state-changing event we just read.
- *
- * @author alexmont
+ * We can then build {@link ITmfStateInterval}'s, to be inserted in a
+ * {@link IStateHistoryBackend} when we detect state changes : the "start time"
+ * of the interval will be the recorded time we have here, and the "end time"
+ * will be the timestamp of the new state-changing event we just read.
  *
+ * @author Alexandre Montplaisir
  */
-class TransientState {
+public class TransientState {
 
     /* Indicates where to insert state changes that we generate */
     private final IStateHistoryBackend backend;
@@ -51,7 +50,13 @@ class TransientState {
     private List<Long> ongoingStateStartTimes;
     private List<Type> stateValueTypes;
 
-    TransientState(IStateHistoryBackend backend) {
+    /**
+     * Constructor
+     *
+     * @param backend
+     *            The back-end in which to insert the generated state intervals
+     */
+    public TransientState(IStateHistoryBackend backend) {
         this.backend = backend;
         isActive = true;
         ongoingStateInfo = new ArrayList<>();
@@ -65,34 +70,73 @@ class TransientState {
         }
     }
 
-    long getLatestTime() {
+    /**
+     * Get the latest time we have seen so far.
+     *
+     * @return The latest time seen in the transient state
+     */
+    public long getLatestTime() {
         return latestTime;
     }
 
-    ITmfStateValue getOngoingStateValue(int index) throws AttributeNotFoundException {
-        checkValidAttribute(index);
-        return ongoingStateInfo.get(index);
+    /**
+     * Retrieve the ongoing state value for a given index (attribute quark).
+     *
+     * @param quark
+     *            The quark of the attribute to look for
+     * @return The corresponding state value
+     * @throws AttributeNotFoundException
+     *             If the quark is invalid
+     */
+    public ITmfStateValue getOngoingStateValue(int quark) throws AttributeNotFoundException {
+        checkValidAttribute(quark);
+        return ongoingStateInfo.get(quark);
     }
 
-    long getOngoingStartTime(int index) throws AttributeNotFoundException {
-        checkValidAttribute(index);
-        return ongoingStateStartTimes.get(index);
+    /**
+     * Retrieve the start time of the state in which the given attribute is in.
+     *
+     * @param quark
+     *            The quark of the attribute to look for
+     * @return The start time of the current state for this attribute
+     * @throws AttributeNotFoundException
+     *             If the quark is invalid
+     */
+    public long getOngoingStartTime(int quark) throws AttributeNotFoundException {
+        checkValidAttribute(quark);
+        return ongoingStateStartTimes.get(quark);
     }
 
-    void changeOngoingStateValue(int index, ITmfStateValue newValue)
+    /**
+     * Modify the current state for a given attribute. This will not update the
+     * "ongoing state start time" in any way, so be careful when using this.
+     *
+     * @param quark
+     *            The quark of the attribute to modify
+     * @param newValue
+     *            The state value the attribute should have
+     * @throws AttributeNotFoundException
+     *             If the quark is invalid
+     */
+    public void changeOngoingStateValue(int quark, ITmfStateValue newValue)
             throws AttributeNotFoundException {
-        checkValidAttribute(index);
-        ongoingStateInfo.set(index, newValue);
+        checkValidAttribute(quark);
+        ongoingStateInfo.set(quark, newValue);
     }
 
     /**
-     * Return the "ongoing" value for a given attribute as a dummy interval
-     * whose end time = -1 (since we don't know its real end time yet).
+     * Convenience method to return the "ongoing" value for a given attribute as
+     * a dummy interval whose end time = -1 (since we don't know its real end
+     * time yet).
      *
      * @param quark
+     *            The quark of the attribute
+     * @return An interval representing the current state (but whose end time is
+     *         meaningless)
      * @throws AttributeNotFoundException
+     *             If the quark is invalid
      */
-    ITmfStateInterval getOngoingInterval(int quark) throws AttributeNotFoundException {
+    public ITmfStateInterval getOngoingInterval(int quark) throws AttributeNotFoundException {
         checkValidAttribute(quark);
         return new TmfStateInterval(ongoingStateStartTimes.get(quark), -1, quark,
                 ongoingStateInfo.get(quark));
@@ -106,16 +150,16 @@ class TransientState {
 
     /**
      * More advanced version of {@link #changeOngoingStateValue}. Replaces the
-     * complete {@link #ongoingStateInfo} in one go, and updates the
-     * {@link #ongoingStateStartTimes} and {@link #stateValuesTypes}
-     * accordingly. BE VERY CAREFUL WITH THIS!
+     * complete ongoingStateInfo in one go, and updates the
+     * ongoingStateStartTimes and #stateValuesTypes accordingly. BE VERY CAREFUL
+     * WITH THIS!
      *
      * @param newStateIntervals
      *            The List of intervals that will represent the new
      *            "ongoing state". Their end times don't matter, we will only
      *            check their value and start times.
      */
-    synchronized void replaceOngoingState(List<ITmfStateInterval> newStateIntervals) {
+    public synchronized void replaceOngoingState(List<ITmfStateInterval> newStateIntervals) {
         int size = newStateIntervals.size();
         ongoingStateInfo = new ArrayList<>(size);
         ongoingStateStartTimes = new ArrayList<>(size);
@@ -133,7 +177,7 @@ class TransientState {
      * Ongoing... tables can stay in sync with the number of attributes in the
      * attribute tree, namely when we add sub-path attributes.
      */
-    synchronized void addEmptyEntry() {
+    public synchronized void addEmptyEntry() {
         /*
          * Since this is a new attribute, we suppose it was in the "null state"
          * since the beginning (so we can have intervals covering for all
@@ -165,31 +209,34 @@ class TransientState {
      * @return True if the value is present in the Transient State at this
      *         moment in time, false if it's not
      */
-    boolean hasInfoAboutStateOf(long time, int quark) {
+    public boolean hasInfoAboutStateOf(long time, int quark) {
         return (this.isActive() && time >= ongoingStateStartTimes.get(quark));
     }
 
     /**
-     * This is the lower-level method that will be called by the
-     * StateHistorySystem (with already-built StateValues and timestamps)
+     * Process a state change to be inserted in the history.
      *
-     * @param index
-     *            The index in the vectors (== the quark of the attribute)
-     * @param value
-     *            The new StateValue associated to this attribute
      * @param eventTime
      *            The timestamp associated with this state change
+     * @param value
+     *            The new StateValue associated to this attribute
+     * @param quark
+     *            The quark of the attribute that is being modified
      * @throws TimeRangeException
+     *             If 'eventTime' is invalid
      * @throws AttributeNotFoundException
+     *             IF 'quark' does not represent an existing attribute
      * @throws StateValueTypeException
+     *             If the state value to be inserted is of a different type of
+     *             what was inserted so far for this attribute.
      */
-    synchronized void processStateChange(long eventTime,
-            ITmfStateValue value, int index) throws TimeRangeException,
+    public synchronized void processStateChange(long eventTime,
+            ITmfStateValue value, int quark) throws TimeRangeException,
             AttributeNotFoundException, StateValueTypeException {
         assert (this.isActive);
 
-        Type expectedSvType = stateValueTypes.get(index);
-        checkValidAttribute(index);
+        Type expectedSvType = stateValueTypes.get(quark);
+        checkValidAttribute(quark);
 
         /*
          * Make sure the state value type we're inserting is the same as the
@@ -200,7 +247,7 @@ class TransientState {
              * The value hasn't been used yet, set it to the value
              * we're currently inserting (which might be null/-1 again).
              */
-            stateValueTypes.set(index, value.getType());
+            stateValueTypes.set(quark, value.getType());
         } else if ((value.getType() != Type.NULL) && (value.getType() != expectedSvType)) {
             /*
              * We authorize inserting null values in any type of attribute,
@@ -214,7 +261,7 @@ class TransientState {
             latestTime = eventTime;
         }
 
-        if (ongoingStateInfo.get(index).equals(value)) {
+        if (ongoingStateInfo.get(quark).equals(value)) {
             /*
              * This is the case where the new value and the one already present
              * in the Builder are the same. We do not need to create an
@@ -223,19 +270,19 @@ class TransientState {
             return;
         }
 
-        if (backend != null && ongoingStateStartTimes.get(index) < eventTime) {
+        if (backend != null && ongoingStateStartTimes.get(quark) < eventTime) {
             /*
              * These two conditions are necessary to create an interval and
              * update ongoingStateInfo.
              */
-            backend.insertPastState(ongoingStateStartTimes.get(index),
+            backend.insertPastState(ongoingStateStartTimes.get(quark),
                     eventTime - 1, /* End Time */
-                    index, /* attribute quark */
-                    ongoingStateInfo.get(index)); /* StateValue */
+                    quark, /* attribute quark */
+                    ongoingStateInfo.get(quark)); /* StateValue */
 
-            ongoingStateStartTimes.set(index, eventTime);
+            ongoingStateStartTimes.set(quark, eventTime);
         }
-        ongoingStateInfo.set(index, value);
+        ongoingStateInfo.set(quark, value);
         return;
     }
 
@@ -248,7 +295,7 @@ class TransientState {
      * @param t
      *            The requested timestamp
      */
-    void doQuery(List<ITmfStateInterval> stateInfo, long t) {
+    public void doQuery(List<ITmfStateInterval> stateInfo, long t) {
         ITmfStateInterval interval;
 
         if (!this.isActive) {
@@ -270,11 +317,16 @@ class TransientState {
     }
 
     /**
-     * Close off the Transient State, used for example when we are done reading a
-     * static trace file. All the information currently contained in it will be
-     * converted to intervals and "flushed" to the State History.
+     * Close off the Transient State, used for example when we are done reading
+     * a static trace file. All the information currently contained in it will
+     * be converted to intervals and "flushed" to the state history.
+     *
+     * @param endTime
+     *            The timestamp to use as end time for the state history (since
+     *            it may be different than the timestamp of the last state
+     *            change)
      */
-    void closeTransientState(long endTime) {
+    public void closeTransientState(long endTime) {
         assert (this.isActive);
 
         for (int i = 0; i < ongoingStateInfo.size(); i++) {
@@ -309,22 +361,26 @@ class TransientState {
     /**
      * Simply returns if this Transient State is currently being used or not
      *
-     * @return
+     * @return True if this transient state is active
      */
-    boolean isActive() {
+    public boolean isActive() {
         return this.isActive;
     }
 
-    void setInactive() {
+    /**
+     * Mark this transient state as inactive
+     */
+    public void setInactive() {
         isActive = false;
     }
 
     /**
-     * Debugging method that prints the contents of both 'ongoing...' vectors
+     * Debugging method that prints the contents of the transient state
      *
      * @param writer
+     *            The writer to which the output should be written
      */
-    void debugPrint(PrintWriter writer) {
+    public void debugPrint(PrintWriter writer) {
         /* Only used for debugging, shouldn't be externalized */
         writer.println("------------------------------"); //$NON-NLS-1$
         writer.println("Info stored in the Builder:"); //$NON-NLS-1$
This page took 0.039504 seconds and 5 git commands to generate.