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