From 23cd33361736f7be16be88d97eee49bf781309c9 Mon Sep 17 00:00:00 2001 From: Ben Manes Date: Sat, 21 Nov 2020 16:48:33 -0800 Subject: [PATCH] Add support for LRB traces to the simulator LRB offers a 17gb cdn trace (2.8B events) that it was analyzed against. The archive support was improved so that a tar.gz can be supplied and the entries will be concatenated. When hit/miss penalty support was added, the Optimal policy degraded by having to hold the entire event in-memory. This is now conditioned so that for non-latency traces only the key (primitive long) is held. This reduces GC pressure and improves performance on large traces. --- checksum.xml | 3 + gradle/dependencies.gradle | 4 +- gradle/wrapper/gradle-wrapper.properties | 2 +- .../cache/simulator/TraceBenchmark.java | 8 +- .../cache/simulator/BasicSettings.java | 41 +++++---- .../caffeine/cache/simulator/Simulator.java | 12 +-- .../caffeine/cache/simulator/Synthetic.java | 3 +- .../simulator/parser/AbstractTraceReader.java | 88 +++++++++++++++---- .../simulator/parser/BinaryTraceReader.java | 3 +- .../cache/simulator/parser/TraceFormat.java | 2 + .../simulator/parser/lrb/LrbTraceReader.java | 54 ++++++++++++ .../cache/simulator/policy/AccessEvent.java | 24 ++--- .../policy/opt/ClairvoyantPolicy.java | 66 +++++++++++--- simulator/src/main/resources/reference.conf | 14 ++- 14 files changed, 249 insertions(+), 75 deletions(-) create mode 100644 simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/lrb/LrbTraceReader.java diff --git a/checksum.xml b/checksum.xml index 12f7338ffc..ad8036a634 100644 --- a/checksum.xml +++ b/checksum.xml @@ -85,6 +85,7 @@ + @@ -110,6 +111,7 @@ + @@ -137,6 +139,7 @@ + diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index cd43e9c898..859dbb19a0 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -45,7 +45,7 @@ ext { flipTables: '1.1.0', googleJavaFormat: '1.7', guava: '30.0-jre', - jackrabbit: '1.34.0', + jackrabbit: '1.36', jandex: '2.2.2.Final', jamm: '0.3.3', javaObjectLayout: '0.14', @@ -97,7 +97,7 @@ ext { semanticVersioning: '1.1.0', shadow: '6.1.0', sonarqube: '3.0', - spotbugs: '4.1.3', + spotbugs: '4.1.4', spotbugsPlugin: '4.6.0', stats: '0.2.2', versions: '0.36.0', diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f982ee918a..4a80df96f0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,4 @@ -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/simulator/src/jmh/java/com/github/benmanes/caffeine/cache/simulator/TraceBenchmark.java b/simulator/src/jmh/java/com/github/benmanes/caffeine/cache/simulator/TraceBenchmark.java index d22b246156..8a5d25b77c 100644 --- a/simulator/src/jmh/java/com/github/benmanes/caffeine/cache/simulator/TraceBenchmark.java +++ b/simulator/src/jmh/java/com/github/benmanes/caffeine/cache/simulator/TraceBenchmark.java @@ -84,11 +84,11 @@ public Policy makePolicy() { } private Stream readEventStream(BasicSettings settings) throws IOException { - if (settings.isSynthetic()) { - return Synthetic.generate(settings).events(); + if (settings.trace().isSynthetic()) { + return Synthetic.generate(settings.trace()).events(); } - List filePaths = settings.traceFiles().paths(); - TraceFormat format = settings.traceFiles().format(); + List filePaths = settings.trace().traceFiles().paths(); + TraceFormat format = settings.trace().traceFiles().format(); return format.readFiles(filePaths).events(); } } diff --git a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/BasicSettings.java b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/BasicSettings.java index 58e415b049..46526ca264 100644 --- a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/BasicSettings.java +++ b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/BasicSettings.java @@ -80,22 +80,8 @@ public long maximumSize() { return config().getLong("maximum-size"); } - public boolean isFiles() { - return config().getString("source").equals("files"); - } - - public boolean isSynthetic() { - return config().getString("source").equals("synthetic"); - } - - public TraceFilesSettings traceFiles() { - checkState(isFiles()); - return new TraceFilesSettings(); - } - - public SyntheticSettings synthetic() { - checkState(isSynthetic()); - return new SyntheticSettings(); + public TraceSettings trace() { + return new TraceSettings(); } /** Returns the config resolved at the simulator's path. */ @@ -185,6 +171,29 @@ public boolean enabled() { } } + public final class TraceSettings { + public long skip() { + return config().getLong("trace.skip"); + } + public long limit() { + return config().getIsNull("trace.limit") ? Long.MAX_VALUE : config().getLong("trace.limit"); + } + public boolean isFiles() { + return config().getString("trace.source").equals("files"); + } + public boolean isSynthetic() { + return config().getString("trace.source").equals("synthetic"); + } + public TraceFilesSettings traceFiles() { + checkState(isFiles()); + return new TraceFilesSettings(); + } + public SyntheticSettings synthetic() { + checkState(isSynthetic()); + return new SyntheticSettings(); + } + } + public final class TraceFilesSettings { public List paths() { return config().getStringList("files.paths"); diff --git a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/Simulator.java b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/Simulator.java index 28369a13a7..931042fe01 100644 --- a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/Simulator.java +++ b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/Simulator.java @@ -119,7 +119,9 @@ private void broadcast() { return; } - try (Stream events = traceReader.events()) { + long skip = settings.trace().skip(); + long limit = settings.trace().limit(); + try (Stream events = traceReader.events().skip(skip).limit(limit)) { Iterators.partition(events.iterator(), batchSize) .forEachRemaining(batch -> router.route(batch, self())); router.route(FINISH, self()); @@ -128,11 +130,11 @@ private void broadcast() { /** Returns a trace reader for the access events. */ private TraceReader makeTraceReader() { - if (settings.isSynthetic()) { - return Synthetic.generate(settings); + if (settings.trace().isSynthetic()) { + return Synthetic.generate(settings.trace()); } - List filePaths = settings.traceFiles().paths(); - TraceFormat format = settings.traceFiles().format(); + List filePaths = settings.trace().traceFiles().paths(); + TraceFormat format = settings.trace().traceFiles().format(); return format.readFiles(filePaths); } diff --git a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/Synthetic.java b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/Synthetic.java index cba7a8f963..93b8e86ae8 100644 --- a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/Synthetic.java +++ b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/Synthetic.java @@ -21,6 +21,7 @@ import com.github.benmanes.caffeine.cache.simulator.BasicSettings.SyntheticSettings.HotspotSettings; import com.github.benmanes.caffeine.cache.simulator.BasicSettings.SyntheticSettings.UniformSettings; +import com.github.benmanes.caffeine.cache.simulator.BasicSettings.TraceSettings; import com.github.benmanes.caffeine.cache.simulator.parser.TraceReader.KeyOnlyTraceReader; import site.ycsb.generator.CounterGenerator; @@ -42,7 +43,7 @@ public final class Synthetic { private Synthetic() {} /** Returns a sequence of events based on the setting's distribution. */ - public static KeyOnlyTraceReader generate(BasicSettings settings) { + public static KeyOnlyTraceReader generate(TraceSettings settings) { int events = settings.synthetic().events(); switch (settings.synthetic().distribution().toLowerCase(US)) { case "counter": diff --git a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/AbstractTraceReader.java b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/AbstractTraceReader.java index 3c839d1324..c497fa18a8 100644 --- a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/AbstractTraceReader.java +++ b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/AbstractTraceReader.java @@ -20,17 +20,27 @@ import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; -import java.io.UncheckedIOException; +import java.io.SequenceInputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; import org.apache.commons.compress.archivers.ArchiveException; +import org.apache.commons.compress.archivers.ArchiveInputStream; import org.apache.commons.compress.archivers.ArchiveStreamFactory; import org.apache.commons.compress.compressors.CompressorException; import org.apache.commons.compress.compressors.CompressorStreamFactory; +import org.checkerframework.checker.nullness.qual.Nullable; import org.tukaani.xz.XZInputStream; +import com.google.common.base.Throwables; +import com.google.common.collect.AbstractIterator; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterators; + /** * A skeletal implementation that reads the trace files into a data stream. * @@ -46,28 +56,70 @@ protected AbstractTraceReader(String filePath) { } /** Returns the input stream of the trace data. */ - protected InputStream readFile() { + @SuppressWarnings("PMD.CloseResource") + protected BufferedInputStream readFile() { + BufferedInputStream input = null; try { - BufferedInputStream input = new BufferedInputStream(openFile(), BUFFER_SIZE); - input.mark(100); - try { - return new XZInputStream(input); - } catch (IOException e) { - input.reset(); - } - try { - return new CompressorStreamFactory().createCompressorInputStream(input); - } catch (CompressorException e) { - input.reset(); + input = new BufferedInputStream(openFile(), BUFFER_SIZE); + List> extractors = ImmutableList.of( + this::tryXZ, this::tryCompressed, this::tryArchived); + for (Function extractor : extractors) { + input.mark(100); + InputStream next = extractor.apply(input); + if (next == null) { + input.reset(); + } else { + input = new BufferedInputStream(next, BUFFER_SIZE); + } } + return input; + } catch (Throwable t) { try { - return new ArchiveStreamFactory().createArchiveInputStream(input); - } catch (ArchiveException e) { - input.reset(); + if (input != null) { + input.close(); + } + } catch (IOException e) { + t.addSuppressed(e); } - return input; + Throwables.throwIfUnchecked(t); + throw new RuntimeException(t); + } + } + + /** Returns a uncompressed stream if XZ encoded, else {@code null}. */ + private @Nullable InputStream tryXZ(InputStream input) { + try { + return new XZInputStream(input); } catch (IOException e) { - throw new UncheckedIOException(e); + return null; + } + } + + /** Returns a uncompressed stream, else {@code null}. */ + private @Nullable InputStream tryCompressed(InputStream input) { + try { + return new CompressorStreamFactory().createCompressorInputStream(input); + } catch (CompressorException e) { + return null; + } + } + + /** Returns a unarchived stream, else {@code null}. */ + private @Nullable InputStream tryArchived(InputStream input) { + try { + ArchiveInputStream archive = new ArchiveStreamFactory().createArchiveInputStream(input); + Iterator entries = new AbstractIterator() { + @Override protected InputStream computeNext() { + try { + return (archive.getNextEntry() == null) ? endOfData() : archive; + } catch (IOException e) { + return endOfData(); + } + } + }; + return new SequenceInputStream(Iterators.asEnumeration(entries)); + } catch (ArchiveException e) { + return null; } } diff --git a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/BinaryTraceReader.java b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/BinaryTraceReader.java index 4062fb6d02..354ce2a2a4 100644 --- a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/BinaryTraceReader.java +++ b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/BinaryTraceReader.java @@ -17,7 +17,6 @@ import static java.util.Objects.requireNonNull; -import java.io.BufferedInputStream; import java.io.DataInputStream; import java.io.EOFException; import java.io.IOException; @@ -46,7 +45,7 @@ protected BinaryTraceReader(String filePath) { @Override @SuppressWarnings("PMD.CloseResource") public Stream events() { - DataInputStream input = new DataInputStream(new BufferedInputStream(readFile())); + DataInputStream input = new DataInputStream(readFile()); Stream stream = StreamSupport.stream(Spliterators.spliteratorUnknownSize( new TraceIterator(input), Spliterator.ORDERED), /* parallel */ false); return stream.onClose(() -> Closeables.closeQuietly(input)); diff --git a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/TraceFormat.java b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/TraceFormat.java index a00e01dd69..be9f94ebc4 100644 --- a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/TraceFormat.java +++ b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/TraceFormat.java @@ -34,6 +34,7 @@ import com.github.benmanes.caffeine.cache.simulator.parser.gradle.GradleTraceReader; import com.github.benmanes.caffeine.cache.simulator.parser.kaggle.OutbrainTraceReader; import com.github.benmanes.caffeine.cache.simulator.parser.lirs.LirsTraceReader; +import com.github.benmanes.caffeine.cache.simulator.parser.lrb.LrbTraceReader; import com.github.benmanes.caffeine.cache.simulator.parser.scarab.ScarabTraceReader; import com.github.benmanes.caffeine.cache.simulator.parser.snia.cambridge.CambridgeTraceReader; import com.github.benmanes.caffeine.cache.simulator.parser.snia.parallel.K5cloudTraceReader; @@ -66,6 +67,7 @@ public enum TraceFormat { CORDA(CordaTraceReader::new), GRADLE(GradleTraceReader::new), LIRS(LirsTraceReader::new), + LRB(LrbTraceReader::new), OUTBRAIN(OutbrainTraceReader::new), SCARAB(ScarabTraceReader::new), SNIA_CAMBRIDGE(CambridgeTraceReader::new), diff --git a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/lrb/LrbTraceReader.java b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/lrb/LrbTraceReader.java new file mode 100644 index 0000000000..748677ab78 --- /dev/null +++ b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/parser/lrb/LrbTraceReader.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020 Ben Manes. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.github.benmanes.caffeine.cache.simulator.parser.lrb; + +import static com.github.benmanes.caffeine.cache.simulator.policy.Policy.Characteristic.WEIGHTED; + +import java.util.Set; +import java.util.stream.Stream; + +import com.github.benmanes.caffeine.cache.simulator.parser.TextTraceReader; +import com.github.benmanes.caffeine.cache.simulator.policy.AccessEvent; +import com.github.benmanes.caffeine.cache.simulator.policy.Policy.Characteristic; +import com.google.common.collect.Sets; + +/** + * A reader for the trace files provided by the authors of the LRB algorithm. See + * traces. + * + * @author ben.manes@gmail.com (Ben Manes) + */ +public final class LrbTraceReader extends TextTraceReader { + + public LrbTraceReader(String filePath) { + super(filePath); + } + + @Override + public Set characteristics() { + return Sets.immutableEnumSet(WEIGHTED); + } + + @Override + public Stream events() { + return lines() + .map(line -> line.split(" ")) + .map(array -> { + return AccessEvent.forKeyAndWeight( + Long.parseLong(array[1]), Integer.parseInt(array[2])); + }); + } +} diff --git a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/AccessEvent.java b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/AccessEvent.java index 0015125832..83e70d2987 100644 --- a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/AccessEvent.java +++ b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/AccessEvent.java @@ -43,16 +43,21 @@ public int weight() { return 1; } - /** Returns the hit penalty of the entry */ + /** Returns the hit penalty of the entry. */ public double hitPenalty() { return 0; } - /** Returns the miss penalty of the entry */ + /** Returns the miss penalty of the entry. */ public double missPenalty() { return 0; } + /** Returns if the trace supplies the hit/miss penalty for this entry. */ + public boolean isPenaltyAware() { + return false; + } + @Override public boolean equals(Object o) { if (o == this) { @@ -105,9 +110,7 @@ private static final class WeightedAccessEvent extends AccessEvent { this.weight = weight; checkArgument(weight >= 0); } - - @Override - public int weight() { + @Override public int weight() { return weight; } } @@ -123,15 +126,14 @@ private static final class PenaltiesAccessEvent extends AccessEvent { checkArgument(hitPenalty >= 0); checkArgument(missPenalty >= hitPenalty); } - - @Override - public double missPenalty() { + @Override public double missPenalty() { return missPenalty; } - - @Override - public double hitPenalty() { + @Override public double hitPenalty() { return hitPenalty; } + @Override public boolean isPenaltyAware() { + return true; + } } } diff --git a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/opt/ClairvoyantPolicy.java b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/opt/ClairvoyantPolicy.java index aafb18e745..7dbb259ca7 100644 --- a/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/opt/ClairvoyantPolicy.java +++ b/simulator/src/main/java/com/github/benmanes/caffeine/cache/simulator/policy/opt/ClairvoyantPolicy.java @@ -33,20 +33,22 @@ import it.unimi.dsi.fastutil.ints.IntSortedSet; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; /** - *
Bélády's
optimal page replacement policy. The upper bound of the hit rate is estimated + * Bélády's optimal page replacement policy. The upper bound of the hit rate is estimated * by evicting from the cache the item that will next be used farthest into the future. * * @author ben.manes@gmail.com (Ben Manes) */ public final class ClairvoyantPolicy implements Policy { private final Long2ObjectMap accessTimes; - private final Queue future; private final PolicyStats policyStats; private final IntSortedSet data; private final int maximumSize; + private Recorder recorder; + private int infiniteTimestamp; private int tick; @@ -56,7 +58,6 @@ public ClairvoyantPolicy(Config config) { policyStats = new PolicyStats("opt.Clairvoyant"); accessTimes = new Long2ObjectOpenHashMap<>(); infiniteTimestamp = Integer.MAX_VALUE; - future = new ArrayDeque<>(maximumSize); data = new IntRBTreeSet(); } @@ -72,8 +73,12 @@ public Set characteristics() { @Override public void record(AccessEvent event) { + if (recorder == null) { + recorder = event.isPenaltyAware() ? new EventRecorder() : new KeyOnlyRecorder(); + } + tick++; - future.add(event); + recorder.add(event); IntPriorityQueue times = accessTimes.get(event.key().longValue()); if (times == null) { times = new IntArrayFIFOQueue(); @@ -90,31 +95,29 @@ public PolicyStats stats() { @Override public void finished() { policyStats.stopwatch().start(); - while (!future.isEmpty()) { - process(future.poll()); - } + recorder.process(); policyStats.stopwatch().stop(); } /** Performs the cache operations for the given key. */ - private void process(AccessEvent event) { - IntPriorityQueue times = accessTimes.get(event.key().longValue()); + private void process(long key, double hitPenalty, double missPenalty) { + IntPriorityQueue times = accessTimes.get(key); int lastAccess = times.dequeueInt(); boolean found = data.remove(lastAccess); if (times.isEmpty()) { data.add(infiniteTimestamp--); - accessTimes.remove(event.key().longValue()); + accessTimes.remove(key); } else { data.add(times.firstInt()); } if (found) { policyStats.recordHit(); - policyStats.recordHitPenalty(event.hitPenalty()); + policyStats.recordHitPenalty(hitPenalty); } else { policyStats.recordMiss(); - policyStats.recordMissPenalty(event.missPenalty()); + policyStats.recordMissPenalty(missPenalty); if (data.size() > maximumSize) { evict(); } @@ -126,4 +129,43 @@ private void evict() { data.remove(data.lastInt()); policyStats.recordEviction(); } + + /** An optimized strategy for storing the event history. */ + private interface Recorder { + void add(AccessEvent event); + void process(); + } + + private final class KeyOnlyRecorder implements Recorder { + private final LongArrayFIFOQueue future; + + KeyOnlyRecorder() { + future = new LongArrayFIFOQueue(maximumSize); + } + @Override public void add(AccessEvent event) { + future.enqueue(event.key().longValue()); + } + @Override public void process() { + while (!future.isEmpty()) { + ClairvoyantPolicy.this.process(future.dequeueLong(), 0.0, 0.0); + } + } + } + + private final class EventRecorder implements Recorder { + private final Queue future; + + EventRecorder() { + future = new ArrayDeque<>(maximumSize); + } + @Override public void add(AccessEvent event) { + future.add(event); + } + @Override public void process() { + while (!future.isEmpty()) { + AccessEvent event = future.poll(); + ClairvoyantPolicy.this.process(event.key(), event.hitPenalty(), event.missPenalty()); + } + } + } } diff --git a/simulator/src/main/resources/reference.conf b/simulator/src/main/resources/reference.conf index a4a458303c..1dc095932e 100644 --- a/simulator/src/main/resources/reference.conf +++ b/simulator/src/main/resources/reference.conf @@ -454,9 +454,16 @@ caffeine.simulator { policy = lfu } - # files: reads from the trace file(s) - # synthetic: reads from a synthetic generator - source = files + trace { + # files: reads from the trace file(s) + # synthetic: reads from a synthetic generator + source = files + + # The number of events to skip + skip = 0 + # The number of events to process or null if unbounded + limit = null + } files { # The paths to the trace files or the file names if in the format's package. To use a mix of @@ -473,6 +480,7 @@ caffeine.simulator { # corda: format of Corda traces # gradle: format from the authors of the Gradle build tool # lirs: format from the authors of the LIRS algorithm + # lrb: format from the authors of the LRB algorithm # outbrain: format from Outbrain trace provided on Kaggle # scarab: format of Scarab Research traces # snia-cambridge: format from the SNIA MSR Cambridge traces