src/jdk.jfr/share/classes/jdk/jfr/internal/PlatformRecording.java
changeset 50745 a390cbb82d47
parent 50197 f4735ff8d17d
child 52413 6372f5af9612
equal deleted inserted replaced
50744:6c306d54366d 50745:a390cbb82d47
    39 import java.time.Instant;
    39 import java.time.Instant;
    40 import java.time.LocalDateTime;
    40 import java.time.LocalDateTime;
    41 import java.util.ArrayList;
    41 import java.util.ArrayList;
    42 import java.util.Collections;
    42 import java.util.Collections;
    43 import java.util.Date;
    43 import java.util.Date;
    44 import java.util.HashMap;
       
    45 import java.util.LinkedHashMap;
    44 import java.util.LinkedHashMap;
    46 import java.util.LinkedList;
    45 import java.util.LinkedList;
    47 import java.util.List;
    46 import java.util.List;
    48 import java.util.Map;
    47 import java.util.Map;
    49 import java.util.StringJoiner;
    48 import java.util.StringJoiner;
    52 
    51 
    53 import jdk.jfr.Configuration;
    52 import jdk.jfr.Configuration;
    54 import jdk.jfr.FlightRecorderListener;
    53 import jdk.jfr.FlightRecorderListener;
    55 import jdk.jfr.Recording;
    54 import jdk.jfr.Recording;
    56 import jdk.jfr.RecordingState;
    55 import jdk.jfr.RecordingState;
       
    56 import jdk.jfr.internal.SecuritySupport.SafePath;
    57 
    57 
    58 public final class PlatformRecording implements AutoCloseable {
    58 public final class PlatformRecording implements AutoCloseable {
    59 
    59 
    60     private final PlatformRecorder recorder;
    60     private final PlatformRecorder recorder;
    61     private final long id;
    61     private final long id;
    68     private WriteableUserPath destination;
    68     private WriteableUserPath destination;
    69 
    69 
    70     private boolean toDisk = true;
    70     private boolean toDisk = true;
    71     private String name;
    71     private String name;
    72     private boolean dumpOnExit;
    72     private boolean dumpOnExit;
       
    73     private SafePath dumpOnExitDirectory = new SafePath(".");
    73     // Timestamp information
    74     // Timestamp information
    74     private Instant stopTime;
    75     private Instant stopTime;
    75     private Instant startTime;
    76     private Instant startTime;
    76 
    77 
    77     // Misc, information
    78     // Misc, information
    87     PlatformRecording(PlatformRecorder recorder, long id) {
    88     PlatformRecording(PlatformRecorder recorder, long id) {
    88         // Typically the access control context is taken
    89         // Typically the access control context is taken
    89         // when you call dump(Path) or setDdestination(Path),
    90         // when you call dump(Path) or setDdestination(Path),
    90         // but if no destination is set and dumponexit=true
    91         // but if no destination is set and dumponexit=true
    91         // the control context of the recording is taken when the
    92         // the control context of the recording is taken when the
    92         // Recording object is constructed.  This works well for
    93         // Recording object is constructed. This works well for
    93         // -XX:StartFlightRecording and JFR.dump
    94         // -XX:StartFlightRecording and JFR.dump
    94         this.noDestinationDumpOnExitAccessControlContext = AccessController.getContext();
    95         this.noDestinationDumpOnExitAccessControlContext = AccessController.getContext();
    95         this.id = id;
    96         this.id = id;
    96         this.recorder = recorder;
    97         this.recorder = recorder;
    97         this.name = String.valueOf(id);
    98         this.name = String.valueOf(id);
   112             }
   113             }
   113             recorder.start(this);
   114             recorder.start(this);
   114             Logger.log(LogTag.JFR, LogLevel.INFO, () -> {
   115             Logger.log(LogTag.JFR, LogLevel.INFO, () -> {
   115                 // Only print non-default values so it easy to see
   116                 // Only print non-default values so it easy to see
   116                 // which options were added
   117                 // which options were added
   117                     StringJoiner options = new StringJoiner(", ");
   118                 StringJoiner options = new StringJoiner(", ");
   118                     if (!toDisk) {
   119                 if (!toDisk) {
   119                         options.add("disk=false");
   120                     options.add("disk=false");
   120                     }
   121                 }
   121                     if (maxAge != null) {
   122                 if (maxAge != null) {
   122                         options.add("maxage=" + Utils.formatTimespan(maxAge, ""));
   123                     options.add("maxage=" + Utils.formatTimespan(maxAge, ""));
   123                     }
   124                 }
   124                     if (maxSize != 0) {
   125                 if (maxSize != 0) {
   125                         options.add("maxsize=" + Utils.formatBytes(maxSize, ""));
   126                     options.add("maxsize=" + Utils.formatBytes(maxSize, ""));
   126                     }
   127                 }
   127                     if (dumpOnExit) {
   128                 if (dumpOnExit) {
   128                         options.add("dumponexit=true");
   129                     options.add("dumponexit=true");
   129                     }
   130                 }
   130                     if (duration != null) {
   131                 if (duration != null) {
   131                         options.add("duration=" + Utils.formatTimespan(duration, ""));
   132                     options.add("duration=" + Utils.formatTimespan(duration, ""));
   132                     }
   133                 }
   133                     if (destination != null) {
   134                 if (destination != null) {
   134                         options.add("filename=" + destination.getText());
   135                     options.add("filename=" + destination.getText());
   135                     }
   136                 }
   136                     String optionText = options.toString();
   137                 String optionText = options.toString();
   137                     if (optionText.length() != 0) {
   138                 if (optionText.length() != 0) {
   138                         optionText = "{" + optionText + "}";
   139                     optionText = "{" + optionText + "}";
   139                     }
   140                 }
   140                     return "Started recording \"" + getName() + "\" (" + getId() + ") " + optionText;
   141                 return "Started recording \"" + getName() + "\" (" + getId() + ") " + optionText;
   141                 });
   142             });
   142             newState = getState();
   143             newState = getState();
   143         }
   144         }
   144         notifyIfStateChanged(oldState, newState);
   145         notifyIfStateChanged(oldState, newState);
   145     }
   146     }
   146 
   147 
   147     public boolean stop(String reason) {
   148     public boolean stop(String reason) {
   148         return stop(reason, null);
       
   149     }
       
   150 
       
   151     public boolean stop(String reason, WriteableUserPath alternativePath) {
       
   152         return stop(reason, alternativePath, Collections.emptyMap());
       
   153     }
       
   154 
       
   155     boolean stop(String reason, WriteableUserPath alternativePath, Map<String, String> overlaySettings) {
       
   156         RecordingState oldState;
   149         RecordingState oldState;
   157         RecordingState newState;
   150         RecordingState newState;
   158         synchronized (recorder) {
   151         synchronized (recorder) {
   159             oldState = getState();
   152             oldState = getState();
   160             if (stopTask != null) {
   153             if (stopTask != null) {
   161                 stopTask.cancel();
   154                 stopTask.cancel();
   162                 stopTask = null;
   155                 stopTask = null;
   163             }
   156             }
   164             recorder.stop(this, alternativePath);
   157             recorder.stop(this);
   165             String endText = reason == null ? "" : ". Reason \"" + reason + "\".";
   158             String endText = reason == null ? "" : ". Reason \"" + reason + "\".";
   166             Logger.log(LogTag.JFR, LogLevel.INFO, "Stopped recording \"" + getName() + "\" (" + getId()+ ")" + endText);
   159             Logger.log(LogTag.JFR, LogLevel.INFO, "Stopped recording \"" + getName() + "\" (" + getId() + ")" + endText);
   167             this.stopTime = Instant.now();
   160             this.stopTime = Instant.now();
   168             newState = getState();
   161             newState = getState();
   169         }
   162         }
   170         WriteableUserPath dest = getDestination();
   163         WriteableUserPath dest = getDestination();
   171         if (dest == null && alternativePath != null) {
   164 
   172             dest = alternativePath;
       
   173         }
       
   174         if (dest != null) {
   165         if (dest != null) {
   175             try {
   166             try {
   176                 copyTo(dest, reason, overlaySettings);
   167                 dumpStopped(dest);
   177                 Logger.log(LogTag.JFR, LogLevel.INFO, "Wrote recording \"" + getName() + "\" (" + getId()+ ") to " + dest.getText());
   168                 Logger.log(LogTag.JFR, LogLevel.INFO, "Wrote recording \"" + getName() + "\" (" + getId() + ") to " + dest.getText());
   178                 notifyIfStateChanged(newState, oldState);
   169                 notifyIfStateChanged(newState, oldState);
   179                 close(); // remove if copied out
   170                 close(); // remove if copied out
   180             } catch (IOException e) {
   171             } catch(IOException e) {
   181                 // throw e; // BUG8925030
   172                 // throw e; // BUG8925030
   182             }
   173             }
   183         } else {
   174         } else {
   184             notifyIfStateChanged(newState, oldState);
   175             notifyIfStateChanged(newState, oldState);
   185         }
   176         }
   193             startTime = Instant.now().plus(delay);
   184             startTime = Instant.now().plus(delay);
   194             LocalDateTime now = LocalDateTime.now().plus(delay);
   185             LocalDateTime now = LocalDateTime.now().plus(delay);
   195             setState(RecordingState.DELAYED);
   186             setState(RecordingState.DELAYED);
   196             startTask = createStartTask();
   187             startTask = createStartTask();
   197             recorder.getTimer().schedule(startTask, delay.toMillis());
   188             recorder.getTimer().schedule(startTask, delay.toMillis());
   198             Logger.log(LogTag.JFR, LogLevel.INFO, "Scheduled recording \"" + getName() + "\" (" + getId()+ ") to start at " + now);
   189             Logger.log(LogTag.JFR, LogLevel.INFO, "Scheduled recording \"" + getName() + "\" (" + getId() + ") to start at " + now);
   199         }
   190         }
   200     }
   191     }
   201 
   192 
   202     private void ensureOkForSchedule() {
   193     private void ensureOkForSchedule() {
   203         if (getState() != RecordingState.NEW) {
   194         if (getState() != RecordingState.NEW) {
   269         synchronized (recorder) {
   260         synchronized (recorder) {
   270             return name;
   261             return name;
   271         }
   262         }
   272     }
   263     }
   273 
   264 
   274 
       
   275     public RecordingState getState() {
   265     public RecordingState getState() {
   276         synchronized (recorder) {
   266         synchronized (recorder) {
   277             return state;
   267             return state;
   278         }
   268         }
   279     }
   269     }
   294                 for (RepositoryChunk c : chunks) {
   284                 for (RepositoryChunk c : chunks) {
   295                     removed(c);
   285                     removed(c);
   296                 }
   286                 }
   297                 chunks.clear();
   287                 chunks.clear();
   298                 setState(RecordingState.CLOSED);
   288                 setState(RecordingState.CLOSED);
   299                 Logger.log(LogTag.JFR, LogLevel.INFO, "Closed recording \"" + getName() + "\" (" + getId()+ ")");
   289                 Logger.log(LogTag.JFR, LogLevel.INFO, "Closed recording \"" + getName() + "\" (" + getId() + ")");
   300             }
   290             }
   301             newState = getState();
   291             newState = getState();
   302         }
   292         }
   303         notifyIfStateChanged(newState, oldState);
   293         notifyIfStateChanged(newState, oldState);
   304     }
   294     }
   305 
   295 
   306     public void copyTo(WriteableUserPath path, String reason, Map<String, String> dumpSettings) throws IOException {
   296     // To be used internally when doing dumps.
   307         synchronized (recorder) {
   297     // Caller must have recorder lock and close recording before releasing lock
   308             RecordingState state = getState();
   298     public PlatformRecording newSnapshotClone(String reason, Boolean pathToGcRoots) throws IOException {
   309             if (state == RecordingState.CLOSED) {
   299         if(!Thread.holdsLock(recorder)) {
   310                 throw new IOException("Recording \"" + name + "\" (id=" + id + ") has been closed, no contents to write");
   300             throw new InternalError("Caller must have recorder lock");
   311             }
   301         }
   312             if (state == RecordingState.DELAYED || state == RecordingState.NEW) {
   302         RecordingState state = getState();
   313                 throw new IOException("Recording \"" + name + "\" (id=" + id + ") has not started, no contents to write");
   303         if (state == RecordingState.CLOSED) {
   314             }
   304             throw new IOException("Recording \"" + name + "\" (id=" + id + ") has been closed, no contents to write");
   315             if (state == RecordingState.STOPPED) {
   305         }
   316                 // have all we need, just write it
   306         if (state == RecordingState.DELAYED || state == RecordingState.NEW) {
   317                 dumpToFile(path, reason, getId());
   307             throw new IOException("Recording \"" + name + "\" (id=" + id + ") has not started, no contents to write");
   318                 return;
   308         }
   319             }
   309         if (state == RecordingState.STOPPED) {
   320 
   310             PlatformRecording clone = recorder.newTemporaryRecording();
   321             // Recording is RUNNING, create a clone
   311             for (RepositoryChunk r : chunks) {
   322             try(PlatformRecording clone = recorder.newRecording(Collections.emptyMap(), 0)) {
   312                 clone.add(r);
   323                 clone.setShouldWriteActiveRecordingEvent(false);
   313             }
   324                 clone.setName(getName());
   314             return clone;
   325                 clone.setDestination(path);
   315         }
   326                 clone.setToDisk(true);
   316 
   327                 // We purposely don't clone settings, since
   317         // Recording is RUNNING, create a clone
   328                 // a union a == a
   318         PlatformRecording clone = recorder.newTemporaryRecording();
   329                 if (!isToDisk()) {
   319         clone.setShouldWriteActiveRecordingEvent(false);
   330                     // force memory contents to disk
   320         clone.setName(getName());
   331                     clone.start();
   321         clone.setDestination(this.destination);
   332                 } else {
   322         clone.setToDisk(true);
   333                     // using existing chunks on disk
   323         // We purposely don't clone settings here, since
   334                     for (RepositoryChunk c : chunks) {
   324         // a union a == a
   335                         clone.add(c);
   325         if (!isToDisk()) {
   336                     }
   326             // force memory contents to disk
   337                     clone.setState(RecordingState.RUNNING);
   327             clone.start();
   338                     clone.setStartTime(getStartTime());
   328         } else {
   339                 }
   329             // using existing chunks on disk
   340                 if (dumpSettings.isEmpty()) {
   330             for (RepositoryChunk c : chunks) {
   341                     clone.setSettings(getSettings());
   331                 clone.add(c);
   342                     clone.stop(reason); // dumps to destination path here
   332             }
   343                 } else {
   333             clone.setState(RecordingState.RUNNING);
   344                     // Risk of violating lock order here, since
   334             clone.setStartTime(getStartTime());
   345                     // clone.stop() will take recorder lock inside
   335         }
   346                     // metadata lock, but OK if we already
   336         if (pathToGcRoots == null) {
   347                     // have recorder lock when we entered metadata lock
   337             clone.setSettings(getSettings()); // needed for old object sample
   348                     Thread.holdsLock(recorder);
   338             clone.stop(reason); // dumps to destination path here
   349                     synchronized(MetadataRepository.getInstance()) {
   339         } else {
   350                         Thread.holdsLock(recorder);
   340             // Risk of violating lock order here, since
   351                         Map<String, String> oldSettings = getSettings();
   341             // clone.stop() will take recorder lock inside
   352                         Map<String, String> newSettings = new HashMap<>(oldSettings);
   342             // metadata lock, but OK if we already
   353                         // replace with dump settings
   343             // have recorder lock when we entered metadata lock
   354                         newSettings.putAll(dumpSettings);
   344             synchronized (MetadataRepository.getInstance()) {
   355                         clone.setSettings(newSettings);
   345                 clone.setSettings(OldObjectSample.createSettingsForSnapshot(this, pathToGcRoots));
   356                         clone.stop(reason);
   346                 clone.stop(reason);
   357                     }
   347             }
   358                 }
   348         }
   359             }
   349         return clone;
   360             return;
       
   361         }
       
   362     }
       
   363 
       
   364     private void dumpToFile(WriteableUserPath userPath, String reason, long id) throws IOException {
       
   365         userPath.doPriviligedIO(() -> {
       
   366             try (ChunksChannel cc = new ChunksChannel(chunks); FileChannel fc = FileChannel.open(userPath.getReal(), StandardOpenOption.WRITE, StandardOpenOption.APPEND)) {
       
   367                 cc.transferTo(fc);
       
   368                 fc.force(true);
       
   369             }
       
   370             return null;
       
   371         });
       
   372     }
   350     }
   373 
   351 
   374     public boolean isToDisk() {
   352     public boolean isToDisk() {
   375         synchronized (recorder) {
   353         synchronized (recorder) {
   376             return toDisk;
   354             return toDisk;
   385             this.maxSize = maxSize;
   363             this.maxSize = maxSize;
   386             trimToSize();
   364             trimToSize();
   387         }
   365         }
   388     }
   366     }
   389 
   367 
   390    public void setDestination(WriteableUserPath userSuppliedPath) throws IOException {
   368     public void setDestination(WriteableUserPath userSuppliedPath) throws IOException {
   391         synchronized (recorder) {
   369         synchronized (recorder) {
   392             if (Utils.isState(getState(), RecordingState.STOPPED, RecordingState.CLOSED)) {
   370             if (Utils.isState(getState(), RecordingState.STOPPED, RecordingState.CLOSED)) {
   393                 throw new IllegalStateException("Destination can't be set on a recording that has been stopped/closed");
   371                 throw new IllegalStateException("Destination can't be set on a recording that has been stopped/closed");
   394             }
   372             }
   395             this.destination = userSuppliedPath;
   373             this.destination = userSuppliedPath;
   477     private void setSettings(Map<String, String> settings, boolean update) {
   455     private void setSettings(Map<String, String> settings, boolean update) {
   478         if (LogTag.JFR_SETTING.shouldLog(LogLevel.INFO.level) && update) {
   456         if (LogTag.JFR_SETTING.shouldLog(LogLevel.INFO.level) && update) {
   479             TreeMap<String, String> ordered = new TreeMap<>(settings);
   457             TreeMap<String, String> ordered = new TreeMap<>(settings);
   480             Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, "New settings for recording \"" + getName() + "\" (" + getId() + ")");
   458             Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, "New settings for recording \"" + getName() + "\" (" + getId() + ")");
   481             for (Map.Entry<String, String> entry : ordered.entrySet()) {
   459             for (Map.Entry<String, String> entry : ordered.entrySet()) {
   482                 String text =  entry.getKey() + "=\"" + entry.getValue() + "\"";
   460                 String text = entry.getKey() + "=\"" + entry.getValue() + "\"";
   483                 Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, text);
   461                 Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, text);
   484             }
   462             }
   485         }
   463         }
   486         synchronized (recorder) {
   464         synchronized (recorder) {
   487             this.settings = new LinkedHashMap<>(settings);
   465             this.settings = new LinkedHashMap<>(settings);
   488             if (getState() == RecordingState.RUNNING && update) {
   466             if (getState() == RecordingState.RUNNING && update) {
   489                 recorder.updateSettings();
   467                 recorder.updateSettings();
   490             }
   468             }
   491         }
   469         }
   492     }
   470     }
   493 
       
   494 
   471 
   495     private void notifyIfStateChanged(RecordingState newState, RecordingState oldState) {
   472     private void notifyIfStateChanged(RecordingState newState, RecordingState oldState) {
   496         if (oldState == newState) {
   473         if (oldState == newState) {
   497             return;
   474             return;
   498         }
   475         }
   578     }
   555     }
   579 
   556 
   580     private void added(RepositoryChunk c) {
   557     private void added(RepositoryChunk c) {
   581         c.use();
   558         c.use();
   582         size += c.getSize();
   559         size += c.getSize();
   583         Logger.log(JFR, DEBUG,  ()-> "Recording \"" + name + "\" (" + id + ") added chunk " + c.toString() + ", current size=" + size);
   560         Logger.log(JFR, DEBUG, () -> "Recording \"" + name + "\" (" + id + ") added chunk " + c.toString() + ", current size=" + size);
   584     }
   561     }
   585 
   562 
   586     private void removed(RepositoryChunk c) {
   563     private void removed(RepositoryChunk c) {
   587         size -= c.getSize();
   564         size -= c.getSize();
   588         Logger.log(JFR, DEBUG,  ()->  "Recording \"" + name + "\" (" + id + ") removed chunk " + c.toString() + ", current size=" + size);
   565         Logger.log(JFR, DEBUG, () -> "Recording \"" + name + "\" (" + id + ") removed chunk " + c.toString() + ", current size=" + size);
   589         c.release();
   566         c.release();
   590     }
   567     }
   591 
   568 
   592     public List<RepositoryChunk> getChunks() {
   569     public List<RepositoryChunk> getChunks() {
   593         return chunks;
   570         return chunks;
   659             public void run() {
   636             public void run() {
   660                 try {
   637                 try {
   661                     stop("End of duration reached");
   638                     stop("End of duration reached");
   662                 } catch (Throwable t) {
   639                 } catch (Throwable t) {
   663                     // Prevent malicious user to propagate exception callback in the wrong context
   640                     // Prevent malicious user to propagate exception callback in the wrong context
   664                     Logger.log(LogTag.JFR, LogLevel.ERROR, "Could not stop recording. " + t.getMessage());
   641                     Logger.log(LogTag.JFR, LogLevel.ERROR, "Could not stop recording.");
   665                 }
   642                 }
   666             }
   643             }
   667         };
   644         };
   668     }
   645     }
   669 
   646 
   676             this.stopTask = stopTask;
   653             this.stopTask = stopTask;
   677         }
   654         }
   678     }
   655     }
   679 
   656 
   680     void clearDestination() {
   657     void clearDestination() {
   681        destination = null;
   658         destination = null;
   682     }
   659     }
   683 
   660 
   684     public AccessControlContext getNoDestinationDumpOnExitAccessControlContext() {
   661     public AccessControlContext getNoDestinationDumpOnExitAccessControlContext() {
   685         return noDestinationDumpOnExitAccessControlContext;
   662         return noDestinationDumpOnExitAccessControlContext;
   686     }
   663     }
   687 
   664 
   688     void setShouldWriteActiveRecordingEvent(boolean shouldWrite) {
   665     void setShouldWriteActiveRecordingEvent(boolean shouldWrite) {
   689        this.shuoldWriteActiveRecordingEvent = shouldWrite;
   666         this.shuoldWriteActiveRecordingEvent = shouldWrite;
   690     }
   667     }
   691 
   668 
   692     boolean shouldWriteMetadataEvent() {
   669     boolean shouldWriteMetadataEvent() {
   693         return shuoldWriteActiveRecordingEvent;
   670         return shuoldWriteActiveRecordingEvent;
   694     }
   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     }
   695 }
   778 }