1 /*
   2  * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package jdk.jfr.internal;
  27 
  28 import static jdk.jfr.internal.LogLevel.DEBUG;
  29 import static jdk.jfr.internal.LogLevel.WARN;
  30 import static jdk.jfr.internal.LogTag.JFR;
  31 
  32 import java.io.IOException;
  33 import java.io.InputStream;
  34 import java.nio.channels.FileChannel;
  35 import java.nio.file.StandardOpenOption;
  36 import java.security.AccessControlContext;
  37 import java.security.AccessController;
  38 import java.time.Duration;
  39 import java.time.Instant;
  40 import java.time.LocalDateTime;
  41 import java.util.ArrayList;
  42 import java.util.Collections;
  43 import java.util.Date;
  44 import java.util.LinkedHashMap;
  45 import java.util.LinkedList;
  46 import java.util.List;
  47 import java.util.Map;
  48 import java.util.StringJoiner;
  49 import java.util.TimerTask;
  50 import java.util.TreeMap;
  51 
  52 import jdk.jfr.Configuration;
  53 import jdk.jfr.FlightRecorderListener;
  54 import jdk.jfr.Recording;
  55 import jdk.jfr.RecordingState;
  56 import jdk.jfr.internal.SecuritySupport.SafePath;
  57 
  58 public final class PlatformRecording implements AutoCloseable {
  59 
  60     private final PlatformRecorder recorder;
  61     private final long id;
  62     // Recording settings
  63     private Map<String, String> settings = new LinkedHashMap<>();
  64     private Duration duration;
  65     private Duration maxAge;
  66     private long maxSize;
  67 
  68     private WriteableUserPath destination;
  69 
  70     private boolean toDisk = true;
  71     private String name;
  72     private boolean dumpOnExit;
  73     private SafePath dumpOnExitDirectory = new SafePath(".");
  74     // Timestamp information
  75     private Instant stopTime;
  76     private Instant startTime;
  77 
  78     // Misc, information
  79     private RecordingState state = RecordingState.NEW;
  80     private long size;
  81     private final LinkedList<RepositoryChunk> chunks = new LinkedList<>();
  82     private volatile Recording recording;
  83     private TimerTask stopTask;
  84     private TimerTask startTask;
  85     private AccessControlContext noDestinationDumpOnExitAccessControlContext;
  86     private boolean shuoldWriteActiveRecordingEvent = true;
  87 
  88     PlatformRecording(PlatformRecorder recorder, long id) {
  89         // Typically the access control context is taken
  90         // when you call dump(Path) or setDdestination(Path),
  91         // but if no destination is set and dumponexit=true
  92         // the control context of the recording is taken when the
  93         // Recording object is constructed. This works well for
  94         // -XX:StartFlightRecording and JFR.dump
  95         this.noDestinationDumpOnExitAccessControlContext = AccessController.getContext();
  96         this.id = id;
  97         this.recorder = recorder;
  98         this.name = String.valueOf(id);
  99     }
 100 
 101     public void start() {
 102         RecordingState oldState;
 103         RecordingState newState;
 104         synchronized (recorder) {
 105             oldState = getState();
 106             if (!Utils.isBefore(state, RecordingState.RUNNING)) {
 107                 throw new IllegalStateException("Recording can only be started once.");
 108             }
 109             if (startTask != null) {
 110                 startTask.cancel();
 111                 startTask = null;
 112                 startTime = null;
 113             }
 114             recorder.start(this);
 115             Logger.log(LogTag.JFR, LogLevel.INFO, () -> {
 116                 // Only print non-default values so it easy to see
 117                 // which options were added
 118                 StringJoiner options = new StringJoiner(", ");
 119                 if (!toDisk) {
 120                     options.add("disk=false");
 121                 }
 122                 if (maxAge != null) {
 123                     options.add("maxage=" + Utils.formatTimespan(maxAge, ""));
 124                 }
 125                 if (maxSize != 0) {
 126                     options.add("maxsize=" + Utils.formatBytesCompact(maxSize));
 127                 }
 128                 if (dumpOnExit) {
 129                     options.add("dumponexit=true");
 130                 }
 131                 if (duration != null) {
 132                     options.add("duration=" + Utils.formatTimespan(duration, ""));
 133                 }
 134                 if (destination != null) {
 135                     options.add("filename=" + destination.getRealPathText());
 136                 }
 137                 String optionText = options.toString();
 138                 if (optionText.length() != 0) {
 139                     optionText = "{" + optionText + "}";
 140                 }
 141                 return "Started recording \"" + getName() + "\" (" + getId() + ") " + optionText;
 142             });
 143             newState = getState();
 144         }
 145         notifyIfStateChanged(oldState, newState);
 146     }
 147 
 148     public boolean stop(String reason) {
 149         RecordingState oldState;
 150         RecordingState newState;
 151         synchronized (recorder) {
 152             oldState = getState();
 153             if (stopTask != null) {
 154                 stopTask.cancel();
 155                 stopTask = null;
 156             }
 157             recorder.stop(this);
 158             String endText = reason == null ? "" : ". Reason \"" + reason + "\".";
 159             Logger.log(LogTag.JFR, LogLevel.INFO, "Stopped recording \"" + getName() + "\" (" + getId() + ")" + endText);
 160             this.stopTime = Instant.now();
 161             newState = getState();
 162         }
 163         WriteableUserPath dest = getDestination();
 164 
 165         if (dest != null) {
 166             try {
 167                 dumpStopped(dest);
 168                 Logger.log(LogTag.JFR, LogLevel.INFO, "Wrote recording \"" + getName() + "\" (" + getId() + ") to " + dest.getRealPathText());
 169                 notifyIfStateChanged(newState, oldState);
 170                 close(); // remove if copied out
 171             } catch(IOException e) {
 172                 // throw e; // BUG8925030
 173             }
 174         } else {
 175             notifyIfStateChanged(newState, oldState);
 176         }
 177         return true;
 178     }
 179 
 180     public void scheduleStart(Duration delay) {
 181         synchronized (recorder) {
 182             ensureOkForSchedule();
 183 
 184             startTime = Instant.now().plus(delay);
 185             LocalDateTime now = LocalDateTime.now().plus(delay);
 186             setState(RecordingState.DELAYED);
 187             startTask = createStartTask();
 188             recorder.getTimer().schedule(startTask, delay.toMillis());
 189             Logger.log(LogTag.JFR, LogLevel.INFO, "Scheduled recording \"" + getName() + "\" (" + getId() + ") to start at " + now);
 190         }
 191     }
 192 
 193     private void ensureOkForSchedule() {
 194         if (getState() != RecordingState.NEW) {
 195             throw new IllegalStateException("Only a new recoridng can be scheduled for start");
 196         }
 197     }
 198 
 199     private TimerTask createStartTask() {
 200         // Taking ref. to recording here.
 201         // Opens up for memory leaks.
 202         return new TimerTask() {
 203             @Override
 204             public void run() {
 205                 synchronized (recorder) {
 206                     if (getState() != RecordingState.DELAYED) {
 207                         return;
 208                     }
 209                     start();
 210                 }
 211             }
 212         };
 213     }
 214 
 215     void scheduleStart(Instant startTime) {
 216         synchronized (recorder) {
 217             ensureOkForSchedule();
 218             this.startTime = startTime;
 219             setState(RecordingState.DELAYED);
 220             startTask = createStartTask();
 221             recorder.getTimer().schedule(startTask, startTime.toEpochMilli());
 222         }
 223     }
 224 
 225     public Map<String, String> getSettings() {
 226         synchronized (recorder) {
 227             return settings;
 228         }
 229     }
 230 
 231     public long getSize() {
 232         return size;
 233     }
 234 
 235     public Instant getStopTime() {
 236         synchronized (recorder) {
 237             return stopTime;
 238         }
 239     }
 240 
 241     public Instant getStartTime() {
 242         synchronized (recorder) {
 243             return startTime;
 244         }
 245     }
 246 
 247     public Long getMaxSize() {
 248         synchronized (recorder) {
 249             return maxSize;
 250         }
 251     }
 252 
 253     public Duration getMaxAge() {
 254         synchronized (recorder) {
 255             return maxAge;
 256         }
 257     }
 258 
 259     public String getName() {
 260         synchronized (recorder) {
 261             return name;
 262         }
 263     }
 264 
 265     public RecordingState getState() {
 266         synchronized (recorder) {
 267             return state;
 268         }
 269     }
 270 
 271     @Override
 272     public void close() {
 273         RecordingState oldState;
 274         RecordingState newState;
 275 
 276         synchronized (recorder) {
 277             oldState = getState();
 278             if (RecordingState.CLOSED != getState()) {
 279                 if (startTask != null) {
 280                     startTask.cancel();
 281                     startTask = null;
 282                 }
 283                 recorder.finish(this);
 284                 for (RepositoryChunk c : chunks) {
 285                     removed(c);
 286                 }
 287                 chunks.clear();
 288                 setState(RecordingState.CLOSED);
 289                 Logger.log(LogTag.JFR, LogLevel.INFO, "Closed recording \"" + getName() + "\" (" + getId() + ")");
 290             }
 291             newState = getState();
 292         }
 293         notifyIfStateChanged(newState, oldState);
 294     }
 295 
 296     // To be used internally when doing dumps.
 297     // Caller must have recorder lock and close recording before releasing lock
 298     public PlatformRecording newSnapshotClone(String reason, Boolean pathToGcRoots) throws IOException {
 299         if(!Thread.holdsLock(recorder)) {
 300             throw new InternalError("Caller must have recorder lock");
 301         }
 302         RecordingState state = getState();
 303         if (state == RecordingState.CLOSED) {
 304             throw new IOException("Recording \"" + name + "\" (id=" + id + ") has been closed, no contents to write");
 305         }
 306         if (state == RecordingState.DELAYED || state == RecordingState.NEW) {
 307             throw new IOException("Recording \"" + name + "\" (id=" + id + ") has not started, no contents to write");
 308         }
 309         if (state == RecordingState.STOPPED) {
 310             PlatformRecording clone = recorder.newTemporaryRecording();
 311             for (RepositoryChunk r : chunks) {
 312                 clone.add(r);
 313             }
 314             return clone;
 315         }
 316 
 317         // Recording is RUNNING, create a clone
 318         PlatformRecording clone = recorder.newTemporaryRecording();
 319         clone.setShouldWriteActiveRecordingEvent(false);
 320         clone.setName(getName());
 321         clone.setToDisk(true);
 322         // We purposely don't clone settings here, since
 323         // a union a == a
 324         if (!isToDisk()) {
 325             // force memory contents to disk
 326             clone.start();
 327         } else {
 328             // using existing chunks on disk
 329             for (RepositoryChunk c : chunks) {
 330                 clone.add(c);
 331             }
 332             clone.setState(RecordingState.RUNNING);
 333             clone.setStartTime(getStartTime());
 334         }
 335         if (pathToGcRoots == null) {
 336             clone.setSettings(getSettings()); // needed for old object sample
 337             clone.stop(reason); // dumps to destination path here
 338         } else {
 339             // Risk of violating lock order here, since
 340             // clone.stop() will take recorder lock inside
 341             // metadata lock, but OK if we already
 342             // have recorder lock when we entered metadata lock
 343             synchronized (MetadataRepository.getInstance()) {
 344                 clone.setSettings(OldObjectSample.createSettingsForSnapshot(this, pathToGcRoots));
 345                 clone.stop(reason);
 346             }
 347         }
 348         return clone;
 349     }
 350 
 351     public boolean isToDisk() {
 352         synchronized (recorder) {
 353             return toDisk;
 354         }
 355     }
 356 
 357     public void setMaxSize(long maxSize) {
 358         synchronized (recorder) {
 359             if (getState() == RecordingState.CLOSED) {
 360                 throw new IllegalStateException("Can't set max age when recording is closed");
 361             }
 362             this.maxSize = maxSize;
 363             trimToSize();
 364         }
 365     }
 366 
 367     public void setDestination(WriteableUserPath userSuppliedPath) throws IOException {
 368         synchronized (recorder) {
 369             if (Utils.isState(getState(), RecordingState.STOPPED, RecordingState.CLOSED)) {
 370                 throw new IllegalStateException("Destination can't be set on a recording that has been stopped/closed");
 371             }
 372             this.destination = userSuppliedPath;
 373         }
 374     }
 375 
 376     public WriteableUserPath getDestination() {
 377         synchronized (recorder) {
 378             return destination;
 379         }
 380     }
 381 
 382     void setState(RecordingState state) {
 383         synchronized (recorder) {
 384             this.state = state;
 385         }
 386     }
 387 
 388     void setStartTime(Instant startTime) {
 389         synchronized (recorder) {
 390             this.startTime = startTime;
 391         }
 392     }
 393 
 394     void setStopTime(Instant timeStamp) {
 395         synchronized (recorder) {
 396             stopTime = timeStamp;
 397         }
 398     }
 399 
 400     public long getId() {
 401         synchronized (recorder) {
 402             return id;
 403         }
 404     }
 405 
 406     public void setName(String name) {
 407         synchronized (recorder) {
 408             ensureNotClosed();
 409             this.name = name;
 410         }
 411     }
 412 
 413     private void ensureNotClosed() {
 414         if (getState() == RecordingState.CLOSED) {
 415             throw new IllegalStateException("Can't change name on a closed recording");
 416         }
 417     }
 418 
 419     public void setDumpOnExit(boolean dumpOnExit) {
 420         synchronized (recorder) {
 421             this.dumpOnExit = dumpOnExit;
 422         }
 423     }
 424 
 425     public boolean getDumpOnExit() {
 426         synchronized (recorder) {
 427             return dumpOnExit;
 428         }
 429     }
 430 
 431     public void setToDisk(boolean toDisk) {
 432         synchronized (recorder) {
 433             if (Utils.isState(getState(), RecordingState.NEW, RecordingState.DELAYED)) {
 434                 this.toDisk = toDisk;
 435             } else {
 436                 throw new IllegalStateException("Recording option disk can't be changed after recording has started");
 437             }
 438         }
 439     }
 440 
 441     public void setSetting(String id, String value) {
 442         synchronized (recorder) {
 443             this.settings.put(id, value);
 444             if (getState() == RecordingState.RUNNING) {
 445                 recorder.updateSettings();
 446             }
 447         }
 448     }
 449 
 450     public void setSettings(Map<String, String> settings) {
 451         setSettings(settings, true);
 452     }
 453 
 454     private void setSettings(Map<String, String> settings, boolean update) {
 455         if (Logger.shouldLog(LogTag.JFR_SETTING, LogLevel.INFO) && update) {
 456             TreeMap<String, String> ordered = new TreeMap<>(settings);
 457             Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, "New settings for recording \"" + getName() + "\" (" + getId() + ")");
 458             for (Map.Entry<String, String> entry : ordered.entrySet()) {
 459                 String text = entry.getKey() + "=\"" + entry.getValue() + "\"";
 460                 Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, text);
 461             }
 462         }
 463         synchronized (recorder) {
 464             this.settings = new LinkedHashMap<>(settings);
 465             if (getState() == RecordingState.RUNNING && update) {
 466                 recorder.updateSettings();
 467             }
 468         }
 469     }
 470 
 471     private void notifyIfStateChanged(RecordingState newState, RecordingState oldState) {
 472         if (oldState == newState) {
 473             return;
 474         }
 475         for (FlightRecorderListener cl : PlatformRecorder.getListeners()) {
 476             try {
 477                 cl.recordingStateChanged(getRecording());
 478             } catch (RuntimeException re) {
 479                 Logger.log(JFR, WARN, "Error notifying recorder listener:" + re.getMessage());
 480             }
 481         }
 482     }
 483 
 484     public void setRecording(Recording recording) {
 485         this.recording = recording;
 486     }
 487 
 488     public Recording getRecording() {
 489         return recording;
 490     }
 491 
 492     @Override
 493     public String toString() {
 494         return getName() + " (id=" + getId() + ") " + getState();
 495     }
 496 
 497     public void setConfiguration(Configuration c) {
 498         setSettings(c.getSettings());
 499     }
 500 
 501     public void setMaxAge(Duration maxAge) {
 502         synchronized (recorder) {
 503             if (getState() == RecordingState.CLOSED) {
 504                 throw new IllegalStateException("Can't set max age when recording is closed");
 505             }
 506             this.maxAge = maxAge;
 507             if (maxAge != null) {
 508                 trimToAge(Instant.now().minus(maxAge));
 509             }
 510         }
 511     }
 512 
 513     void appendChunk(RepositoryChunk chunk) {
 514         if (!chunk.isFinished()) {
 515             throw new Error("not finished chunk " + chunk.getStartTime());
 516         }
 517         synchronized (recorder) {
 518             if (!toDisk) {
 519                 return;
 520             }
 521             if (maxAge != null) {
 522                 trimToAge(chunk.getEndTime().minus(maxAge));
 523             }
 524             chunks.addLast(chunk);
 525             added(chunk);
 526             trimToSize();
 527         }
 528     }
 529 
 530     private void trimToSize() {
 531         if (maxSize == 0) {
 532             return;
 533         }
 534         while (size > maxSize && chunks.size() > 1) {
 535             RepositoryChunk c = chunks.removeFirst();
 536             removed(c);
 537         }
 538     }
 539 
 540     private void trimToAge(Instant oldest) {
 541         while (!chunks.isEmpty()) {
 542             RepositoryChunk oldestChunk = chunks.peek();
 543             if (oldestChunk.getEndTime().isAfter(oldest)) {
 544                 return;
 545             }
 546             chunks.removeFirst();
 547             removed(oldestChunk);
 548         }
 549     }
 550 
 551     void add(RepositoryChunk c) {
 552         chunks.add(c);
 553         added(c);
 554     }
 555 
 556     private void added(RepositoryChunk c) {
 557         c.use();
 558         size += c.getSize();
 559         Logger.log(JFR, DEBUG, () -> "Recording \"" + name + "\" (" + id + ") added chunk " + c.toString() + ", current size=" + size);
 560     }
 561 
 562     private void removed(RepositoryChunk c) {
 563         size -= c.getSize();
 564         Logger.log(JFR, DEBUG, () -> "Recording \"" + name + "\" (" + id + ") removed chunk " + c.toString() + ", current size=" + size);
 565         c.release();
 566     }
 567 
 568     public List<RepositoryChunk> getChunks() {
 569         return chunks;
 570     }
 571 
 572     public InputStream open(Instant start, Instant end) throws IOException {
 573         synchronized (recorder) {
 574             if (getState() != RecordingState.STOPPED) {
 575                 throw new IOException("Recording must be stopped before it can be read.");
 576             }
 577             List<RepositoryChunk> chunksToUse = new ArrayList<RepositoryChunk>();
 578             for (RepositoryChunk chunk : chunks) {
 579                 if (chunk.isFinished()) {
 580                     Instant chunkStart = chunk.getStartTime();
 581                     Instant chunkEnd = chunk.getEndTime();
 582                     if (start == null || !chunkEnd.isBefore(start)) {
 583                         if (end == null || !chunkStart.isAfter(end)) {
 584                             chunksToUse.add(chunk);
 585                         }
 586                     }
 587                 }
 588             }
 589             if (chunksToUse.isEmpty()) {
 590                 return null;
 591             }
 592             return new ChunkInputStream(chunksToUse);
 593         }
 594     }
 595 
 596     public Duration getDuration() {
 597         synchronized (recorder) {
 598             return duration;
 599         }
 600     }
 601 
 602     void setInternalDuration(Duration duration) {
 603         this.duration = duration;
 604     }
 605 
 606     public void setDuration(Duration duration) {
 607         synchronized (recorder) {
 608             if (Utils.isState(getState(), RecordingState.STOPPED, RecordingState.CLOSED)) {
 609                 throw new IllegalStateException("Duration can't be set after a recording has been stopped/closed");
 610             }
 611             setInternalDuration(duration);
 612             if (getState() != RecordingState.NEW) {
 613                 updateTimer();
 614             }
 615         }
 616     }
 617 
 618     void updateTimer() {
 619         if (stopTask != null) {
 620             stopTask.cancel();
 621             stopTask = null;
 622         }
 623         if (getState() == RecordingState.CLOSED) {
 624             return;
 625         }
 626         if (duration != null) {
 627             stopTask = createStopTask();
 628             recorder.getTimer().schedule(stopTask, new Date(startTime.plus(duration).toEpochMilli()));
 629         }
 630     }
 631 
 632     TimerTask createStopTask() {
 633         return new TimerTask() {
 634             @Override
 635             public void run() {
 636                 try {
 637                     stop("End of duration reached");
 638                 } catch (Throwable t) {
 639                     // Prevent malicious user to propagate exception callback in the wrong context
 640                     Logger.log(LogTag.JFR, LogLevel.ERROR, "Could not stop recording.");
 641                 }
 642             }
 643         };
 644     }
 645 
 646     public Recording newCopy(boolean stop) {
 647         return recorder.newCopy(this, stop);
 648     }
 649 
 650     void setStopTask(TimerTask stopTask) {
 651         synchronized (recorder) {
 652             this.stopTask = stopTask;
 653         }
 654     }
 655 
 656     void clearDestination() {
 657         destination = null;
 658     }
 659 
 660     public AccessControlContext getNoDestinationDumpOnExitAccessControlContext() {
 661         return noDestinationDumpOnExitAccessControlContext;
 662     }
 663 
 664     void setShouldWriteActiveRecordingEvent(boolean shouldWrite) {
 665         this.shuoldWriteActiveRecordingEvent = shouldWrite;
 666     }
 667 
 668     boolean shouldWriteMetadataEvent() {
 669         return shuoldWriteActiveRecordingEvent;
 670     }
 671 
 672     // Dump running and stopped recordings
 673     public void dump(WriteableUserPath writeableUserPath) throws IOException {
 674         synchronized (recorder) {
 675             try(PlatformRecording p = newSnapshotClone("Dumped by user", null))  {
 676                 p.dumpStopped(writeableUserPath);
 677             }
 678         }
 679     }
 680 
 681     public void dumpStopped(WriteableUserPath userPath) throws IOException {
 682         synchronized (recorder) {
 683                 userPath.doPriviligedIO(() -> {
 684                     try (ChunksChannel cc = new ChunksChannel(chunks); FileChannel fc = FileChannel.open(userPath.getReal(), StandardOpenOption.WRITE, StandardOpenOption.APPEND)) {
 685                         cc.transferTo(fc);
 686                         fc.force(true);
 687                     }
 688                     return null;
 689                 });
 690         }
 691     }
 692 
 693     public void filter(Instant begin, Instant end, Long maxSize) {
 694         synchronized (recorder) {
 695             List<RepositoryChunk> result = removeAfter(end, removeBefore(begin, new ArrayList<>(chunks)));
 696             if (maxSize != null) {
 697                 if (begin != null && end == null) {
 698                     result = reduceFromBeginning(maxSize, result);
 699                 } else {
 700                     result = reduceFromEnd(maxSize, result);
 701                 }
 702             }
 703             int size = 0;
 704             for (RepositoryChunk r : result) {
 705                 size += r.getSize();
 706                 r.use();
 707             }
 708             this.size = size;
 709             for (RepositoryChunk r : chunks) {
 710                 r.release();
 711             }
 712             chunks.clear();
 713             chunks.addAll(result);
 714         }
 715     }
 716 
 717     private static List<RepositoryChunk> removeBefore(Instant time, List<RepositoryChunk> input) {
 718         if (time == null) {
 719             return input;
 720         }
 721         List<RepositoryChunk> result = new ArrayList<>(input.size());
 722         for (RepositoryChunk r : input) {
 723             if (!r.getEndTime().isBefore(time)) {
 724                 result.add(r);
 725             }
 726         }
 727         return result;
 728     }
 729 
 730     private static List<RepositoryChunk> removeAfter(Instant time, List<RepositoryChunk> input) {
 731         if (time == null) {
 732             return input;
 733         }
 734         List<RepositoryChunk> result = new ArrayList<>(input.size());
 735         for (RepositoryChunk r : input) {
 736             if (!r.getStartTime().isAfter(time)) {
 737                 result.add(r);
 738             }
 739         }
 740         return result;
 741     }
 742 
 743     private static List<RepositoryChunk> reduceFromBeginning(Long maxSize, List<RepositoryChunk> input) {
 744         if (maxSize == null || input.isEmpty()) {
 745             return input;
 746         }
 747         List<RepositoryChunk> result = new ArrayList<>(input.size());
 748         long total = 0;
 749         for (RepositoryChunk r : input) {
 750             total += r.getSize();
 751             if (total > maxSize) {
 752                 break;
 753             }
 754             result.add(r);
 755         }
 756         // always keep at least one chunk
 757         if (result.isEmpty()) {
 758             result.add(input.get(0));
 759         }
 760         return result;
 761     }
 762 
 763     private static List<RepositoryChunk> reduceFromEnd(Long maxSize, List<RepositoryChunk> input) {
 764         Collections.reverse(input);
 765         List<RepositoryChunk> result = reduceFromBeginning(maxSize, input);
 766         Collections.reverse(result);
 767         return result;
 768     }
 769 
 770     public void setDumpOnExitDirectory(SafePath directory) {
 771        this.dumpOnExitDirectory = directory;
 772     }
 773 
 774     public SafePath getDumpOnExitDirectory()  {
 775         return this.dumpOnExitDirectory;
 776     }
 777 }