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.formatBytes(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.getText());
 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.getText());
 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.setDestination(this.destination);
 322         clone.setToDisk(true);
 323         // We purposely don't clone settings here, since
 324         // a union a == a
 325         if (!isToDisk()) {
 326             // force memory contents to disk
 327             clone.start();
 328         } else {
 329             // using existing chunks on disk
 330             for (RepositoryChunk c : chunks) {
 331                 clone.add(c);
 332             }
 333             clone.setState(RecordingState.RUNNING);
 334             clone.setStartTime(getStartTime());
 335         }
 336         if (pathToGcRoots == null) {
 337             clone.setSettings(getSettings()); // needed for old object sample
 338             clone.stop(reason); // dumps to destination path here
 339         } else {
 340             // Risk of violating lock order here, since
 341             // clone.stop() will take recorder lock inside
 342             // metadata lock, but OK if we already
 343             // have recorder lock when we entered metadata lock
 344             synchronized (MetadataRepository.getInstance()) {
 345                 clone.setSettings(OldObjectSample.createSettingsForSnapshot(this, pathToGcRoots));
 346                 clone.stop(reason);
 347             }
 348         }
 349         return clone;
 350     }
 351 
 352     public boolean isToDisk() {
 353         synchronized (recorder) {
 354             return toDisk;
 355         }
 356     }
 357 
 358     public void setMaxSize(long maxSize) {
 359         synchronized (recorder) {
 360             if (getState() == RecordingState.CLOSED) {
 361                 throw new IllegalStateException("Can't set max age when recording is closed");
 362             }
 363             this.maxSize = maxSize;
 364             trimToSize();
 365         }
 366     }
 367 
 368     public void setDestination(WriteableUserPath userSuppliedPath) throws IOException {
 369         synchronized (recorder) {
 370             if (Utils.isState(getState(), RecordingState.STOPPED, RecordingState.CLOSED)) {
 371                 throw new IllegalStateException("Destination can't be set on a recording that has been stopped/closed");
 372             }
 373             this.destination = userSuppliedPath;
 374         }
 375     }
 376 
 377     public WriteableUserPath getDestination() {
 378         synchronized (recorder) {
 379             return destination;
 380         }
 381     }
 382 
 383     void setState(RecordingState state) {
 384         synchronized (recorder) {
 385             this.state = state;
 386         }
 387     }
 388 
 389     void setStartTime(Instant startTime) {
 390         synchronized (recorder) {
 391             this.startTime = startTime;
 392         }
 393     }
 394 
 395     void setStopTime(Instant timeStamp) {
 396         synchronized (recorder) {
 397             stopTime = timeStamp;
 398         }
 399     }
 400 
 401     public long getId() {
 402         synchronized (recorder) {
 403             return id;
 404         }
 405     }
 406 
 407     public void setName(String name) {
 408         synchronized (recorder) {
 409             ensureNotClosed();
 410             this.name = name;
 411         }
 412     }
 413 
 414     private void ensureNotClosed() {
 415         if (getState() == RecordingState.CLOSED) {
 416             throw new IllegalStateException("Can't change name on a closed recording");
 417         }
 418     }
 419 
 420     public void setDumpOnExit(boolean dumpOnExit) {
 421         synchronized (recorder) {
 422             this.dumpOnExit = dumpOnExit;
 423         }
 424     }
 425 
 426     public boolean getDumpOnExit() {
 427         synchronized (recorder) {
 428             return dumpOnExit;
 429         }
 430     }
 431 
 432     public void setToDisk(boolean toDisk) {
 433         synchronized (recorder) {
 434             if (Utils.isState(getState(), RecordingState.NEW, RecordingState.DELAYED)) {
 435                 this.toDisk = toDisk;
 436             } else {
 437                 throw new IllegalStateException("Recording option disk can't be changed after recording has started");
 438             }
 439         }
 440     }
 441 
 442     public void setSetting(String id, String value) {
 443         synchronized (recorder) {
 444             this.settings.put(id, value);
 445             if (getState() == RecordingState.RUNNING) {
 446                 recorder.updateSettings();
 447             }
 448         }
 449     }
 450 
 451     public void setSettings(Map<String, String> settings) {
 452         setSettings(settings, true);
 453     }
 454 
 455     private void setSettings(Map<String, String> settings, boolean update) {
 456         if (LogTag.JFR_SETTING.shouldLog(LogLevel.INFO.level) && update) {
 457             TreeMap<String, String> ordered = new TreeMap<>(settings);
 458             Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, "New settings for recording \"" + getName() + "\" (" + getId() + ")");
 459             for (Map.Entry<String, String> entry : ordered.entrySet()) {
 460                 String text = entry.getKey() + "=\"" + entry.getValue() + "\"";
 461                 Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, text);
 462             }
 463         }
 464         synchronized (recorder) {
 465             this.settings = new LinkedHashMap<>(settings);
 466             if (getState() == RecordingState.RUNNING && update) {
 467                 recorder.updateSettings();
 468             }
 469         }
 470     }
 471 
 472     private void notifyIfStateChanged(RecordingState newState, RecordingState oldState) {
 473         if (oldState == newState) {
 474             return;
 475         }
 476         for (FlightRecorderListener cl : PlatformRecorder.getListeners()) {
 477             try {
 478                 cl.recordingStateChanged(getRecording());
 479             } catch (RuntimeException re) {
 480                 Logger.log(JFR, WARN, "Error notifying recorder listener:" + re.getMessage());
 481             }
 482         }
 483     }
 484 
 485     public void setRecording(Recording recording) {
 486         this.recording = recording;
 487     }
 488 
 489     public Recording getRecording() {
 490         return recording;
 491     }
 492 
 493     @Override
 494     public String toString() {
 495         return getName() + " (id=" + getId() + ") " + getState();
 496     }
 497 
 498     public void setConfiguration(Configuration c) {
 499         setSettings(c.getSettings());
 500     }
 501 
 502     public void setMaxAge(Duration maxAge) {
 503         synchronized (recorder) {
 504             if (getState() == RecordingState.CLOSED) {
 505                 throw new IllegalStateException("Can't set max age when recording is closed");
 506             }
 507             this.maxAge = maxAge;
 508             if (maxAge != null) {
 509                 trimToAge(Instant.now().minus(maxAge));
 510             }
 511         }
 512     }
 513 
 514     void appendChunk(RepositoryChunk chunk) {
 515         if (!chunk.isFinished()) {
 516             throw new Error("not finished chunk " + chunk.getStartTime());
 517         }
 518         synchronized (recorder) {
 519             if (!toDisk) {
 520                 return;
 521             }
 522             if (maxAge != null) {
 523                 trimToAge(chunk.getEndTime().minus(maxAge));
 524             }
 525             chunks.addLast(chunk);
 526             added(chunk);
 527             trimToSize();
 528         }
 529     }
 530 
 531     private void trimToSize() {
 532         if (maxSize == 0) {
 533             return;
 534         }
 535         while (size > maxSize && chunks.size() > 1) {
 536             RepositoryChunk c = chunks.removeFirst();
 537             removed(c);
 538         }
 539     }
 540 
 541     private void trimToAge(Instant oldest) {
 542         while (!chunks.isEmpty()) {
 543             RepositoryChunk oldestChunk = chunks.peek();
 544             if (oldestChunk.getEndTime().isAfter(oldest)) {
 545                 return;
 546             }
 547             chunks.removeFirst();
 548             removed(oldestChunk);
 549         }
 550     }
 551 
 552     void add(RepositoryChunk c) {
 553         chunks.add(c);
 554         added(c);
 555     }
 556 
 557     private void added(RepositoryChunk c) {
 558         c.use();
 559         size += c.getSize();
 560         Logger.log(JFR, DEBUG, () -> "Recording \"" + name + "\" (" + id + ") added chunk " + c.toString() + ", current size=" + size);
 561     }
 562 
 563     private void removed(RepositoryChunk c) {
 564         size -= c.getSize();
 565         Logger.log(JFR, DEBUG, () -> "Recording \"" + name + "\" (" + id + ") removed chunk " + c.toString() + ", current size=" + size);
 566         c.release();
 567     }
 568 
 569     public List<RepositoryChunk> getChunks() {
 570         return chunks;
 571     }
 572 
 573     public InputStream open(Instant start, Instant end) throws IOException {
 574         synchronized (recorder) {
 575             if (getState() != RecordingState.STOPPED) {
 576                 throw new IOException("Recording must be stopped before it can be read.");
 577             }
 578             List<RepositoryChunk> chunksToUse = new ArrayList<RepositoryChunk>();
 579             for (RepositoryChunk chunk : chunks) {
 580                 if (chunk.isFinished()) {
 581                     Instant chunkStart = chunk.getStartTime();
 582                     Instant chunkEnd = chunk.getEndTime();
 583                     if (start == null || !chunkEnd.isBefore(start)) {
 584                         if (end == null || !chunkStart.isAfter(end)) {
 585                             chunksToUse.add(chunk);
 586                         }
 587                     }
 588                 }
 589             }
 590             if (chunksToUse.isEmpty()) {
 591                 return null;
 592             }
 593             return new ChunkInputStream(chunksToUse);
 594         }
 595     }
 596 
 597     public Duration getDuration() {
 598         synchronized (recorder) {
 599             return duration;
 600         }
 601     }
 602 
 603     void setInternalDuration(Duration duration) {
 604         this.duration = duration;
 605     }
 606 
 607     public void setDuration(Duration duration) {
 608         synchronized (recorder) {
 609             if (Utils.isState(getState(), RecordingState.STOPPED, RecordingState.CLOSED)) {
 610                 throw new IllegalStateException("Duration can't be set after a recording has been stopped/closed");
 611             }
 612             setInternalDuration(duration);
 613             if (getState() != RecordingState.NEW) {
 614                 updateTimer();
 615             }
 616         }
 617     }
 618 
 619     void updateTimer() {
 620         if (stopTask != null) {
 621             stopTask.cancel();
 622             stopTask = null;
 623         }
 624         if (getState() == RecordingState.CLOSED) {
 625             return;
 626         }
 627         if (duration != null) {
 628             stopTask = createStopTask();
 629             recorder.getTimer().schedule(stopTask, new Date(startTime.plus(duration).toEpochMilli()));
 630         }
 631     }
 632 
 633     TimerTask createStopTask() {
 634         return new TimerTask() {
 635             @Override
 636             public void run() {
 637                 try {
 638                     stop("End of duration reached");
 639                 } catch (Throwable t) {
 640                     // Prevent malicious user to propagate exception callback in the wrong context
 641                     Logger.log(LogTag.JFR, LogLevel.ERROR, "Could not stop recording.");
 642                 }
 643             }
 644         };
 645     }
 646 
 647     public Recording newCopy(boolean stop) {
 648         return recorder.newCopy(this, stop);
 649     }
 650 
 651     void setStopTask(TimerTask stopTask) {
 652         synchronized (recorder) {
 653             this.stopTask = stopTask;
 654         }
 655     }
 656 
 657     void clearDestination() {
 658         destination = null;
 659     }
 660 
 661     public AccessControlContext getNoDestinationDumpOnExitAccessControlContext() {
 662         return noDestinationDumpOnExitAccessControlContext;
 663     }
 664 
 665     void setShouldWriteActiveRecordingEvent(boolean shouldWrite) {
 666         this.shuoldWriteActiveRecordingEvent = shouldWrite;
 667     }
 668 
 669     boolean shouldWriteMetadataEvent() {
 670         return shuoldWriteActiveRecordingEvent;
 671     }
 672 
 673     // Dump running and stopped recordings
 674     public void dump(WriteableUserPath writeableUserPath) throws IOException {
 675         synchronized (recorder) {
 676             try(PlatformRecording p = newSnapshotClone("Dumped by user", null))  {
 677                 p.dumpStopped(writeableUserPath);
 678             }
 679         }
 680     }
 681 
 682     public void dumpStopped(WriteableUserPath userPath) throws IOException {
 683         synchronized (recorder) {
 684                 userPath.doPriviligedIO(() -> {
 685                     try (ChunksChannel cc = new ChunksChannel(chunks); FileChannel fc = FileChannel.open(userPath.getReal(), StandardOpenOption.WRITE, StandardOpenOption.APPEND)) {
 686                         cc.transferTo(fc);
 687                         fc.force(true);
 688                     }
 689                     return null;
 690                 });
 691         }
 692     }
 693 
 694     public void filter(Instant begin, Instant end, Long maxSize) {
 695         synchronized (recorder) {
 696             List<RepositoryChunk> result = removeAfter(end, removeBefore(begin, new ArrayList<>(chunks)));
 697             if (maxSize != null) {
 698                 if (begin != null && end == null) {
 699                     result = reduceFromBeginning(maxSize, result);
 700                 } else {
 701                     result = reduceFromEnd(maxSize, result);
 702                 }
 703             }
 704             int size = 0;
 705             for (RepositoryChunk r : result) {
 706                 size += r.getSize();
 707                 r.use();
 708             }
 709             this.size = size;
 710             for (RepositoryChunk r : chunks) {
 711                 r.release();
 712             }
 713             chunks.clear();
 714             chunks.addAll(result);
 715         }
 716     }
 717 
 718     private static List<RepositoryChunk> removeBefore(Instant time, List<RepositoryChunk> input) {
 719         if (time == null) {
 720             return input;
 721         }
 722         List<RepositoryChunk> result = new ArrayList<>(input.size());
 723         for (RepositoryChunk r : input) {
 724             if (!r.getEndTime().isBefore(time)) {
 725                 result.add(r);
 726             }
 727         }
 728         return result;
 729     }
 730 
 731     private static List<RepositoryChunk> removeAfter(Instant time, List<RepositoryChunk> input) {
 732         if (time == null) {
 733             return input;
 734         }
 735         List<RepositoryChunk> result = new ArrayList<>(input.size());
 736         for (RepositoryChunk r : input) {
 737             if (!r.getStartTime().isAfter(time)) {
 738                 result.add(r);
 739             }
 740         }
 741         return result;
 742     }
 743 
 744     private static List<RepositoryChunk> reduceFromBeginning(Long maxSize, List<RepositoryChunk> input) {
 745         if (maxSize == null || input.isEmpty()) {
 746             return input;
 747         }
 748         List<RepositoryChunk> result = new ArrayList<>(input.size());
 749         long total = 0;
 750         for (RepositoryChunk r : input) {
 751             total += r.getSize();
 752             if (total > maxSize) {
 753                 break;
 754             }
 755             result.add(r);
 756         }
 757         // always keep at least one chunk
 758         if (result.isEmpty()) {
 759             result.add(input.get(0));
 760         }
 761         return result;
 762     }
 763 
 764     private static List<RepositoryChunk> reduceFromEnd(Long maxSize, List<RepositoryChunk> input) {
 765         Collections.reverse(input);
 766         List<RepositoryChunk> result = reduceFromBeginning(maxSize, input);
 767         Collections.reverse(result);
 768         return result;
 769     }
 770 
 771     public void setDumpOnExitDirectory(SafePath directory) {
 772        this.dumpOnExitDirectory = directory;
 773     }
 774 
 775     public SafePath getDumpOnExitDirectory()  {
 776         return this.dumpOnExitDirectory;
 777     }
 778 }