You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by md...@apache.org on 2015/10/27 19:49:36 UTC

svn commit: r1710862 - in /jackrabbit/oak/trunk: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/file/ oak-run/ oak-run/src/main/java/org/apache/jackrabbit/oak/plugins/s...

Author: mduerig
Date: Tue Oct 27 18:49:35 2015
New Revision: 1710862

URL: http://svn.apache.org/viewvc?rev=1710862&view=rev
Log:
OAK-3560: Tooling for writing segment graphs to a file
Add graph run mode to oak-run dumping a file store segment graph to a text file in Guess GDF format

Modified:
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/Segment.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/file/FileStore.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/file/TarReader.java
    jackrabbit/oak/trunk/oak-run/README.md
    jackrabbit/oak/trunk/oak-run/pom.xml
    jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/plugins/segment/FileStoreHelper.java
    jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/Main.java

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/Segment.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/Segment.java?rev=1710862&r1=1710861&r2=1710862&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/Segment.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/Segment.java Tue Oct 27 18:49:35 2015
@@ -36,6 +36,7 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.ConcurrentMap;
 
+import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 
 import com.google.common.base.Charsets;
@@ -273,6 +274,30 @@ public class Segment {
                 << RECORD_ALIGN_BITS;
     }
 
+    /**
+     * Returns the segment meta data of this segment or {@code null} if none is present.
+     * <p>
+     * The segment meta data is a string of the format {@code "{wid=W,sno=S,gc=G,t=T}"}
+     * where:
+     * <ul>
+     * <li>{@code W} is the writer id {@code wid}, </li>
+     * <li>{@code S} is a unique, increasing sequence number corresponding to the allocation order
+     * of the segments in this store, </li>
+     * <li>{@code G} is the garbage collection generation (i.e. the number of compaction cycles
+     * that have been run),</li>
+     * <li>{@code T} is a time stamp according to {@link System#currentTimeMillis()}.</li>
+     * </ul>
+     * @return the segment meta data
+     */
+    @CheckForNull
+    public String getSegmentInfo() {
+        if (getRootCount() == 0) {
+            return null;
+        } else {
+            return readString(getRootOffset(0));
+        }
+    }
+
     SegmentId getRefId(int index) {
         if (refids == null || index >= refids.length) {
             String type = "data";

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/file/FileStore.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/file/FileStore.java?rev=1710862&r1=1710861&r2=1710862&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/file/FileStore.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/file/FileStore.java Tue Oct 27 18:49:35 2015
@@ -1357,6 +1357,19 @@ public class FileStore implements Segmen
             super.setRevision(revision);
         }
 
+        /**
+         * Build the graph of segments reachable from an initial set of segments
+         * @param referencedIds  the initial set of segments
+         * @throws IOException
+         */
+        public Map<UUID, Set<UUID>> getSegmentGraph(Set<UUID> referencedIds) throws IOException {
+            Map<UUID, Set<UUID>> graph = newHashMap();
+            for (TarReader reader : super.readers) {
+                graph.putAll(reader.getReferenceGraph(referencedIds));
+            }
+            return graph;
+        }
+
         @Override
         public boolean setHead(SegmentNodeState base, SegmentNodeState head) {
             throw new UnsupportedOperationException("Read Only Store");

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/file/TarReader.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/file/TarReader.java?rev=1710862&r1=1710861&r2=1710862&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/file/TarReader.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/segment/file/TarReader.java Tue Oct 27 18:49:35 2015
@@ -36,6 +36,7 @@ import java.io.RandomAccessFile;
 import java.nio.ByteBuffer;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -46,6 +47,9 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.zip.CRC32;
 
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+
 import org.apache.commons.io.FileUtils;
 import org.apache.jackrabbit.oak.plugins.segment.CompactionMap;
 import org.slf4j.Logger;
@@ -630,6 +634,75 @@ class TarReader implements Closeable {
         return -1;
     }
 
+    @Nonnull
+    private TarEntry[] getEntries() {
+        TarEntry[] entries = new TarEntry[index.remaining() / 24];
+        int position = index.position();
+        for (int i = 0; position < index.limit(); i++) {
+            entries[i]  = new TarEntry(
+                    index.getLong(position),
+                    index.getLong(position + 8),
+                    index.getInt(position + 16),
+                    index.getInt(position + 20));
+            position += 24;
+        }
+        Arrays.sort(entries, TarEntry.OFFSET_ORDER);
+        return entries;
+    }
+
+    @CheckForNull
+    private List<UUID> getReferences(TarEntry entry, UUID id, Map<UUID, List<UUID>> graph) throws IOException {
+        if (graph != null) {
+            return graph.get(id);
+        } else {
+            // a pre-compiled graph is not available, so read the
+            // references directly from this segment
+            ByteBuffer segment = access.read(
+                    entry.offset(),
+                    Math.min(entry.size(), 16 * 256));
+            int pos = segment.position();
+            int refCount = segment.get(pos + REF_COUNT_OFFSET) & 0xff;
+            int refEnd = pos + 16 * (refCount + 1);
+            List<UUID> refIds = newArrayList();
+            for (int refPos = pos + 16; refPos < refEnd; refPos += 16) {
+                refIds.add(new UUID(
+                        segment.getLong(refPos),
+                        segment.getLong(refPos + 8)));
+            }
+            return refIds;
+        }
+    }
+
+    /**
+     * Build the graph of segments reachable from an initial set of segments
+     * @param referencedIds  the initial set of segments
+     * @throws IOException
+     */
+    Map<UUID, Set<UUID>> getReferenceGraph(Set<UUID> referencedIds) throws IOException {
+        Map<UUID, List<UUID>> graph = getGraph();
+        Map<UUID, Set<UUID>> refGraph = newHashMap();
+
+        TarEntry[] entries = getEntries();
+        for (int i = entries.length - 1; i >= 0; i--) {
+            TarEntry entry = entries[i];
+            UUID id = new UUID(entry.msb(), entry.lsb());
+            if (!referencedIds.remove(id)) {
+                // this segment is not referenced anywhere
+                entries[i] = null;
+            } else {
+                if (isDataSegmentId(entry.lsb())) {
+                    // this is a referenced data segment, so follow the graph
+                    List<UUID> refIds = getReferences(entry, id, graph);
+                    if (refIds != null) {
+                        refGraph.put(id, new HashSet<UUID>(refIds));
+                        referencedIds.addAll(refIds);
+                    }
+                }
+            }
+        }
+        return refGraph;
+    }
+
     /**
      * Garbage collects segments in this file. First it collects the set of
      * segments that are referenced / reachable, then (if more than 25% is
@@ -652,52 +725,25 @@ class TarReader implements Closeable {
 
         Set<UUID> cleaned = newHashSet();
         Map<UUID, List<UUID>> graph = getGraph();
-
-        TarEntry[] sorted = new TarEntry[index.remaining() / 24];
-        int position = index.position();
-        for (int i = 0; position < index.limit(); i++) {
-            sorted[i]  = new TarEntry(
-                    index.getLong(position),
-                    index.getLong(position + 8),
-                    index.getInt(position + 16),
-                    index.getInt(position + 20));
-            position += 24;
-        }
-        Arrays.sort(sorted, TarEntry.OFFSET_ORDER);
+        TarEntry[] entries = getEntries();
 
         int size = 0;
         int count = 0;
-        for (int i = sorted.length - 1; i >= 0; i--) {
-            TarEntry entry = sorted[i];
+        for (int i = entries.length - 1; i >= 0; i--) {
+            TarEntry entry = entries[i];
             UUID id = new UUID(entry.msb(), entry.lsb());
             if (!referencedIds.remove(id)) {
                 // this segment is not referenced anywhere
                 cleaned.add(id);
-                sorted[i] = null;
+                entries[i] = null;
             } else {
                 size += getEntrySize(entry.size());
                 count += 1;
                 if (isDataSegmentId(entry.lsb())) {
                     // this is a referenced data segment, so follow the graph
-                    if (graph != null) {
-                        List<UUID> refids = graph.get(id);
-                        if (refids != null) {
-                            referencedIds.addAll(refids);
-                        }
-                    } else {
-                        // a pre-compiled graph is not available, so read the
-                        // references directly from this segment
-                        ByteBuffer segment = access.read(
-                                entry.offset(),
-                                Math.min(entry.size(), 16 * 256));
-                        int pos = segment.position();
-                        int refcount = segment.get(pos + REF_COUNT_OFFSET) & 0xff;
-                        int refend = pos + 16 * (refcount + 1);
-                        for (int refpos = pos + 16; refpos < refend; refpos += 16) {
-                            referencedIds.add(new UUID(
-                                    segment.getLong(refpos),
-                                    segment.getLong(refpos + 8)));
-                        }
+                    List<UUID> refIds = getReferences(entry, id, graph);
+                    if (refIds != null) {
+                        referencedIds.addAll(refIds);
                     }
                 }
             }
@@ -733,7 +779,7 @@ class TarReader implements Closeable {
 
         log.debug("Writing new generation {}", newFile.getName());
         TarWriter writer = new TarWriter(newFile);
-        for (TarEntry entry : sorted) {
+        for (TarEntry entry : entries) {
             if (entry != null) {
                 byte[] data = new byte[entry.size()];
                 access.read(entry.offset(), entry.size()).get(data);

Modified: jackrabbit/oak/trunk/oak-run/README.md
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-run/README.md?rev=1710862&r1=1710861&r2=1710862&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-run/README.md (original)
+++ jackrabbit/oak/trunk/oak-run/README.md Tue Oct 27 18:49:35 2015
@@ -14,6 +14,7 @@ The following runmodes are currently ava
     * server      : Run the Oak Server.
     * console     : Start an interactive console.
     * explore     : Starts a GUI browser based on java swing.
+    * graph       : Export the segment graph of a segment store to a file.
     * check       : Check the FileStore for inconsistencies
     * primary     : Run a TarMK Cold Standby primary instance
     * standby     : Run a TarMK Cold Standby standby instance
@@ -95,6 +96,29 @@ browsing of an existing oak repository.
 
     $ java -jar oak-run-*.jar explore /path/to/oak/repository [skip-size-check]
 
+Graph
+-----
+
+The 'graph' mode export the segment graph of a file store to a text file in the
+[Guess GDF format](https://gephi.github.io/users/supported-graph-formats/gdf-format/),
+which is easily imported into [Gephi](https://gephi.github.io).
+
+As the GDF format only supports integer values but the segment time stamps are encoded as long
+values an optional 'epoch' argument can be specified. If no epoch is given on the command line
+the start of the day of the last modified date of the 'journal.log' is used. The epoch specifies
+a negative offset translating all timestamps into a valid int range.
+
+    $ java -jar oak-run-*.jar graph [File] <options>
+
+    [File] -- Path to segment store (required)
+
+    Option           Description
+    ------           -----------
+    --epoch <Long>   Epoch of the segment time stamps
+                       (derived from journal.log if not
+                       given)
+    --output <File>  Output file (default: segments.gdf)
+
 Check
 -----
 

Modified: jackrabbit/oak/trunk/oak-run/pom.xml
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-run/pom.xml?rev=1710862&r1=1710861&r2=1710862&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-run/pom.xml (original)
+++ jackrabbit/oak/trunk/oak-run/pom.xml Tue Oct 27 18:49:35 2015
@@ -361,6 +361,11 @@
       <version>1.0.6</version>
       <scope>compile</scope>
     </dependency>
+    <dependency>
+      <groupId>com.google.code.gson</groupId>
+      <artifactId>gson</artifactId>
+      <version>2.2.4</version>
+    </dependency>
 
     <dependency>
       <groupId>org.apache.tika</groupId>

Modified: jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/plugins/segment/FileStoreHelper.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/plugins/segment/FileStoreHelper.java?rev=1710862&r1=1710861&r2=1710862&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/plugins/segment/FileStoreHelper.java (original)
+++ jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/plugins/segment/FileStoreHelper.java Tue Oct 27 18:49:35 2015
@@ -18,27 +18,40 @@
  */
 package org.apache.jackrabbit.oak.plugins.segment;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.collect.Lists.newArrayList;
+import static com.google.common.collect.Maps.newHashMap;
 import static com.google.common.collect.Sets.newHashSet;
 import static java.util.Collections.reverseOrder;
+import static java.util.Collections.singleton;
 import static java.util.Collections.sort;
+import static org.apache.jackrabbit.oak.plugins.segment.SegmentId.isDataSegmentId;
 
 import java.io.File;
 import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
 import java.util.AbstractMap.SimpleImmutableEntry;
 import java.util.ArrayDeque;
+import java.util.Date;
 import java.util.Deque;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
 import java.util.UUID;
 
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import org.apache.jackrabbit.oak.api.PropertyState;
 import org.apache.jackrabbit.oak.plugins.segment.file.FileStore;
+import org.apache.jackrabbit.oak.plugins.segment.file.FileStore.ReadOnlyStore;
+import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
 
-public class FileStoreHelper {
+public final class FileStoreHelper {
 
-    public final static String newline = "\n";
+    public static final String newline = "\n";
 
     private FileStoreHelper() {
     }
@@ -94,4 +107,120 @@ public class FileStoreHelper {
         }
     }
 
+    /**
+     * Write the segment graph of a file store to a stream.
+     * <p>
+     * The graph is written in
+     * <a href="https://gephi.github.io/users/supported-graph-formats/gdf-format/">the Guess GDF format</a>,
+     * which is easily imported into <a href="https://gephi.github.io/">Gephi</a>.
+     * As GDF only supports integers but the segment time stamps are encoded as long
+     * the {@code epoch} argument is used as a negative offset translating all timestamps
+     * into a valid int range.
+     *
+     * @param fileStore     file store to graph
+     * @param out           stream to write the graph to
+     * @param epoch         epoch (in milliseconds)
+     * @throws Exception
+     */
+    public static void writeSegmentGraph(ReadOnlyStore fileStore, OutputStream out, Date epoch) throws Exception {
+        PrintWriter writer = new PrintWriter(out);
+        try {
+            SegmentNodeState root = fileStore.getHead();
+
+            // Segment graph starting from the segment containing root
+            Map<UUID, Set<UUID>> segmentGraph = fileStore.getSegmentGraph(new HashSet<UUID>(singleton(root.getRecordId().asUUID())));
+
+            // All segment in the segment graph
+            Set<UUID> segments = newHashSet();
+            segments.addAll(segmentGraph.keySet());
+            for (Set<UUID> tos : segmentGraph.values()) {
+                segments.addAll(tos);
+            }
+
+            // Graph of segments containing the head state
+            Map<UUID, Set<UUID>> headGraph = newHashMap();
+            collectSegments(root, headGraph);
+
+            // All segments containing the head state
+            Set<UUID> headSegments = newHashSet();
+            for (Entry<UUID, Set<UUID>> entry : headGraph.entrySet()) {
+                headSegments.add(entry.getKey());
+                headSegments.addAll(entry.getValue());
+            }
+
+            writer.write("nodedef>name VARCHAR, label VARCHAR, type VARCHAR, wid VARCHAR, gc INT, t INT, head BOOLEAN\n");
+            for (UUID segment : segments) {
+                writeNode(segment, writer, headSegments.contains(segment), epoch, fileStore.getTracker());
+            }
+
+            writer.write("edgedef>node1 VARCHAR, node2 VARCHAR, head BOOLEAN\n");
+            for (Entry<UUID, Set<UUID>> edge : segmentGraph.entrySet()) {
+                UUID from = edge.getKey();
+                for (UUID to : edge.getValue()) {
+                    Set<UUID> he = headGraph.get(from);
+                    boolean inHead = he != null && he.contains(to);
+                    writer.write(from + "," + to + "," + inHead + "\n");
+                }
+            }
+        } finally {
+            writer.close();
+        }
+    }
+
+    private static void collectSegments(SegmentNodeState root, Map<UUID, Set<UUID>> graph) {
+        UUID nodeId = root.getRecordId().asUUID();
+        Set<UUID> refs = graph.get(nodeId);
+        if (refs == null) {
+            refs = newHashSet();
+            graph.put(nodeId, refs);
+        }
+
+        for (PropertyState propertyState : root.getProperties()) {
+            if (propertyState instanceof SegmentPropertyState) {
+                SegmentPropertyState sps = (SegmentPropertyState) propertyState;
+                refs.add(sps.getRecordId().getSegmentId().asUUID());
+            }
+
+        }
+
+        for (ChildNodeEntry childNodeEntry : root.getChildNodeEntries()) {
+            if (childNodeEntry.getNodeState() instanceof SegmentNodeState) {
+                SegmentNodeState child = (SegmentNodeState) childNodeEntry.getNodeState();
+                refs.add(child.getRecordId().getSegmentId().asUUID());
+                collectSegments(child, graph);
+            }
+        }
+    }
+
+    private static void writeNode(UUID node, PrintWriter writer, boolean inHead, Date epoch, SegmentTracker tracker) {
+        JsonObject sInfo = getSegmentInfo(node, tracker);
+        if (sInfo == null) {
+            writer.write(node + ",b,bulk,b,-1,-1," + inHead + "\n");
+        } else {
+            long t = sInfo.get("t").getAsLong();
+            long ts = t - epoch.getTime();
+            checkArgument(ts >= Integer.MIN_VALUE && ts <= Integer.MAX_VALUE,
+                    "Time stamp (" + new Date(t) + ") not in epoch (" +
+                    new Date(epoch.getTime() + Integer.MIN_VALUE) + " - " +
+                    new Date(epoch.getTime() + Integer.MAX_VALUE) + ")");
+            writer.write(node +
+                    "," + sInfo.get("sno").getAsString() +
+                    ",data" +
+                    "," + sInfo.get("wid").getAsString() +
+                    "," + sInfo.get("gc").getAsString() +
+                    "," + ts +
+                    "," + inHead + "\n");
+        }
+    }
+
+    private static JsonObject getSegmentInfo(UUID node, SegmentTracker tracker) {
+        if (isDataSegmentId(node.getLeastSignificantBits())) {
+            SegmentId id = tracker.getSegmentId(node.getMostSignificantBits(), node.getLeastSignificantBits());
+            String info = id.getSegment().getSegmentInfo();
+            return info == null ? null : new JsonParser().parse(info).getAsJsonObject();
+        } else {
+            return null;
+        }
+    }
+
 }

Modified: jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/Main.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/Main.java?rev=1710862&r1=1710861&r2=1710862&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/Main.java (original)
+++ jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/Main.java Tue Oct 27 18:49:35 2015
@@ -27,6 +27,7 @@ import static org.slf4j.LoggerFactory.ge
 
 import java.io.Closeable;
 import java.io.File;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.RandomAccessFile;
@@ -34,6 +35,7 @@ import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -67,6 +69,7 @@ import joptsimple.OptionParser;
 import joptsimple.OptionSet;
 import joptsimple.OptionSpec;
 import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.time.DateUtils;
 import org.apache.jackrabbit.oak.Oak;
 import org.apache.jackrabbit.oak.api.ContentRepository;
 import org.apache.jackrabbit.oak.benchmark.BenchmarkRunner;
@@ -95,6 +98,7 @@ import org.apache.jackrabbit.oak.plugins
 import org.apache.jackrabbit.oak.plugins.document.util.MapDBMapFactory;
 import org.apache.jackrabbit.oak.plugins.document.util.MapFactory;
 import org.apache.jackrabbit.oak.plugins.document.util.MongoConnection;
+import org.apache.jackrabbit.oak.plugins.segment.FileStoreHelper;
 import org.apache.jackrabbit.oak.plugins.segment.RecordId;
 import org.apache.jackrabbit.oak.plugins.segment.RecordUsageAnalyser;
 import org.apache.jackrabbit.oak.plugins.segment.Segment;
@@ -105,6 +109,7 @@ import org.apache.jackrabbit.oak.plugins
 import org.apache.jackrabbit.oak.plugins.segment.compaction.CompactionStrategy;
 import org.apache.jackrabbit.oak.plugins.segment.compaction.CompactionStrategy.CleanupType;
 import org.apache.jackrabbit.oak.plugins.segment.file.FileStore;
+import org.apache.jackrabbit.oak.plugins.segment.file.FileStore.ReadOnlyStore;
 import org.apache.jackrabbit.oak.plugins.segment.file.JournalReader;
 import org.apache.jackrabbit.oak.plugins.segment.standby.client.StandbyClient;
 import org.apache.jackrabbit.oak.plugins.segment.standby.server.StandbyServer;
@@ -165,6 +170,9 @@ public final class Main {
             case DEBUG:
                 debug(args);
                 break;
+            case GRAPH:
+                graph(args);
+                break;
             case CHECK:
                 check(args);
                 break;
@@ -777,6 +785,53 @@ public final class Main {
         }
     }
 
+    private static void graph(String[] args) throws Exception {
+        OptionParser parser = new OptionParser();
+        OptionSpec<File> directoryArg = parser.nonOptions(
+                "Path to segment store (required)").ofType(File.class);
+        OptionSpec<File> outFileArg = parser.accepts(
+                "output", "Output file").withRequiredArg().ofType(File.class)
+                .defaultsTo(new File("segments.gdf"));
+        OptionSpec<Long> epochArg = parser.accepts(
+                "epoch", "Epoch of the segment time stamps (derived from journal.log if not given)")
+                .withRequiredArg().ofType(Long.class);
+        OptionSet options = parser.parse(args);
+
+        File directory = directoryArg.value(options);
+        if (directory == null) {
+            System.err.println("Dump the segment graph to a file. Usage: graph [File] <options>");
+            parser.printHelpOn(System.err);
+            System.exit(-1);
+        }
+        if (!isValidFileStore(directory.getPath())) {
+            System.err.println("Invalid FileStore directory " + directory);
+            System.exit(1);
+        }
+
+        File outFile = outFileArg.value(options);
+        Date epoch;
+        if (options.has(epochArg)) {
+            epoch = new Date(epochArg.value(options));
+        } else {
+            epoch = new Date(new File(directory, "journal.log").lastModified());
+            epoch = DateUtils.setHours(epoch, 0);
+            epoch = DateUtils.setMinutes(epoch, 0);
+            epoch = DateUtils.setSeconds(epoch, 0);
+            epoch = DateUtils.setMilliseconds(epoch, 0);
+        }
+
+        System.out.println("Opening file store at " + directory);
+        ReadOnlyStore fileStore = new ReadOnlyStore(directory);
+
+        if (outFile.exists()) {
+            outFile.delete();
+        }
+
+        System.out.println("Setting epoch to " + epoch);
+        System.out.println("Writing graph to " + outFile);
+        FileStoreHelper.writeSegmentGraph(fileStore, new FileOutputStream(outFile), epoch);
+    }
+
     private static void check(String[] args) throws IOException {
         OptionParser parser = new OptionParser();
         ArgumentAcceptingOptionSpec<String> path = parser.accepts(
@@ -1191,6 +1246,7 @@ public final class Main {
         BENCHMARK("benchmark"),
         CONSOLE("console"),
         DEBUG("debug"),
+        GRAPH("graph"),
         CHECK("check"),
         COMPACT("compact"),
         SERVER("server"),