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