src/jdk.jfr/share/classes/jdk/jfr/internal/PlatformRecorder.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.INFO;
       
    29 import static jdk.jfr.internal.LogLevel.TRACE;
       
    30 import static jdk.jfr.internal.LogLevel.WARN;
       
    31 import static jdk.jfr.internal.LogTag.JFR;
       
    32 import static jdk.jfr.internal.LogTag.JFR_SYSTEM;
       
    33 
       
    34 import java.io.IOException;
       
    35 import java.security.AccessControlContext;
       
    36 import java.security.AccessController;
       
    37 import java.time.Duration;
       
    38 import java.time.Instant;
       
    39 import java.util.ArrayList;
       
    40 import java.util.Collections;
       
    41 import java.util.HashSet;
       
    42 import java.util.List;
       
    43 import java.util.Map;
       
    44 import java.util.Set;
       
    45 import java.util.Timer;
       
    46 import java.util.TimerTask;
       
    47 import java.util.concurrent.CopyOnWriteArrayList;
       
    48 
       
    49 import jdk.jfr.Enabled;
       
    50 import jdk.jfr.EventType;
       
    51 import jdk.jfr.FlightRecorder;
       
    52 import jdk.jfr.FlightRecorderListener;
       
    53 import jdk.jfr.Recording;
       
    54 import jdk.jfr.RecordingState;
       
    55 import jdk.jfr.events.ActiveRecordingEvent;
       
    56 import jdk.jfr.events.ActiveSettingEvent;
       
    57 import jdk.jfr.internal.SecuritySupport.SecureRecorderListener;
       
    58 import jdk.jfr.internal.instrument.JDKEvents;
       
    59 import jdk.jfr.internal.settings.CutoffSetting;
       
    60 import jdk.jfr.internal.test.WhiteBox;
       
    61 
       
    62 public final class PlatformRecorder {
       
    63 
       
    64     private final List<PlatformRecording> recordings = new ArrayList<>();
       
    65     private final static List<SecureRecorderListener> changeListeners = new ArrayList<>();
       
    66     private final Repository repository;
       
    67     private final Timer timer;
       
    68     private final static JVM jvm = JVM.getJVM();
       
    69     private final EventType activeRecordingEvent;
       
    70     private final EventType activeSettingEvent;
       
    71     private final Thread shutdownHook;
       
    72 
       
    73     private long recordingCounter = 0;
       
    74     private RepositoryChunk currentChunk;
       
    75 
       
    76     public PlatformRecorder() throws Exception {
       
    77         repository = Repository.getRepository();
       
    78         Logger.log(JFR_SYSTEM, INFO, "Initialized disk repository");
       
    79         repository.ensureRepository();
       
    80         jvm.createNativeJFR();
       
    81         Logger.log(JFR_SYSTEM, INFO, "Created native");
       
    82         JDKEvents.initialize();
       
    83         Logger.log(JFR_SYSTEM, INFO, "Registered JDK events");
       
    84         JDKEvents.addInstrumentation();
       
    85         startDiskMonitor();
       
    86         SecuritySupport.registerEvent(ActiveRecordingEvent.class);
       
    87         activeRecordingEvent = EventType.getEventType(ActiveRecordingEvent.class);
       
    88         SecuritySupport.registerEvent(ActiveSettingEvent.class);
       
    89         activeSettingEvent = EventType.getEventType(ActiveSettingEvent.class);
       
    90         shutdownHook = SecuritySupport.createThreadWitNoPermissions("JFR: Shutdown Hook", new ShutdownHook(this));
       
    91         SecuritySupport.setUncaughtExceptionHandler(shutdownHook, new ShutdownHook.ExceptionHandler());
       
    92         SecuritySupport.registerShutdownHook(shutdownHook);
       
    93         timer = createTimer();
       
    94     }
       
    95 
       
    96     private static Timer createTimer() {
       
    97         try {
       
    98             List<Timer> result = new CopyOnWriteArrayList<>();
       
    99             Thread t = SecuritySupport.createThreadWitNoPermissions("Permissionless thread", ()-> {
       
   100                 result.add(new Timer("JFR Recording Scheduler", true));
       
   101             });
       
   102             t.start();
       
   103             t.join();
       
   104             return result.get(0);
       
   105         } catch (InterruptedException e) {
       
   106             throw new IllegalStateException("Not able to create timer task. " + e.getMessage(), e);
       
   107         }
       
   108     }
       
   109 
       
   110     public synchronized PlatformRecording newRecording(Map<String, String> settings) {
       
   111         PlatformRecording recording = new PlatformRecording(this, ++recordingCounter);
       
   112         if (!settings.isEmpty()) {
       
   113             recording.setSettings(settings);
       
   114         }
       
   115         recordings.add(recording);
       
   116         return recording;
       
   117     }
       
   118 
       
   119     synchronized void finish(PlatformRecording recording) {
       
   120         if (recording.getState() == RecordingState.RUNNING) {
       
   121             recording.stop("Recording closed");
       
   122         }
       
   123         recordings.remove(recording);
       
   124     }
       
   125 
       
   126     public synchronized List<PlatformRecording> getRecordings() {
       
   127         return Collections.unmodifiableList(new ArrayList<PlatformRecording>(recordings));
       
   128     }
       
   129 
       
   130     public synchronized static void addListener(FlightRecorderListener changeListener) {
       
   131         AccessControlContext context = AccessController.getContext();
       
   132         SecureRecorderListener sl = new SecureRecorderListener(context, changeListener);
       
   133         boolean runInitialized;
       
   134         synchronized (PlatformRecorder.class) {
       
   135             runInitialized = FlightRecorder.isInitialized();
       
   136             changeListeners.add(sl);
       
   137         }
       
   138         if (runInitialized) {
       
   139             sl.recorderInitialized(FlightRecorder.getFlightRecorder());
       
   140         }
       
   141     }
       
   142 
       
   143     public synchronized static boolean removeListener(FlightRecorderListener changeListener) {
       
   144         for (SecureRecorderListener s : new ArrayList<>(changeListeners)) {
       
   145             if (s.getChangeListener() == changeListener) {
       
   146                 changeListeners.remove(s);
       
   147                 return true;
       
   148             }
       
   149         }
       
   150         return false;
       
   151     }
       
   152 
       
   153     static synchronized List<FlightRecorderListener> getListeners() {
       
   154         return new ArrayList<>(changeListeners);
       
   155     }
       
   156 
       
   157     Timer getTimer() {
       
   158         return timer;
       
   159     }
       
   160 
       
   161     public static void notifyRecorderInitialized(FlightRecorder recorder) {
       
   162         Logger.log(JFR_SYSTEM, TRACE, "Notifying listeners that Flight Recorder is initialized");
       
   163         for (FlightRecorderListener r : getListeners()) {
       
   164             r.recorderInitialized(recorder);
       
   165         }
       
   166     }
       
   167 
       
   168     // called by shutdown hook
       
   169     synchronized void destroy() {
       
   170         try {
       
   171             timer.cancel();
       
   172         } catch (Exception ex) {
       
   173             Logger.log(JFR_SYSTEM, WARN, "Shutdown hook could not cancel timer");
       
   174         }
       
   175 
       
   176         for (PlatformRecording p : getRecordings()) {
       
   177             if (p.getState() == RecordingState.RUNNING) {
       
   178                 try {
       
   179                     p.stop("Shutdown");
       
   180                 } catch (Exception ex) {
       
   181                     Logger.log(JFR, WARN, "Recording " + p.getName() + ":" + p.getId() + " could not be stopped");
       
   182                 }
       
   183             }
       
   184         }
       
   185 
       
   186         JDKEvents.remove();
       
   187 
       
   188         if (jvm.hasNativeJFR()) {
       
   189             if (jvm.isRecording()) {
       
   190                 jvm.endRecording_();
       
   191             }
       
   192             jvm.destroyNativeJFR();
       
   193         }
       
   194         repository.clear();
       
   195     }
       
   196 
       
   197     synchronized void start(PlatformRecording recording) {
       
   198         // State can only be NEW or DELAYED because of previous checks
       
   199         Instant now = Instant.now();
       
   200         recording.setStartTime(now);
       
   201         recording.updateTimer();
       
   202         Duration duration = recording.getDuration();
       
   203         if (duration != null) {
       
   204             recording.setStopTime(now.plus(duration));
       
   205         }
       
   206         boolean toDisk = recording.isToDisk();
       
   207         boolean beginPhysical = true;
       
   208         for (PlatformRecording s : getRecordings()) {
       
   209             if (s.getState() == RecordingState.RUNNING) {
       
   210                 beginPhysical = false;
       
   211                 if (s.isToDisk()) {
       
   212                     toDisk = true;
       
   213                 }
       
   214             }
       
   215         }
       
   216         if (beginPhysical) {
       
   217             RepositoryChunk newChunk = null;
       
   218             if (toDisk) {
       
   219                 newChunk = repository.newChunk(now);
       
   220                 MetadataRepository.getInstance().setOutput(newChunk.getUnfishedFile().toString());
       
   221             } else {
       
   222                 MetadataRepository.getInstance().setOutput(null);
       
   223             }
       
   224             currentChunk = newChunk;
       
   225             jvm.beginRecording_();
       
   226             recording.setState(RecordingState.RUNNING);
       
   227             updateSettings();
       
   228             writeMetaEvents();
       
   229         } else {
       
   230             RepositoryChunk newChunk = null;
       
   231             if (toDisk) {
       
   232                 newChunk = repository.newChunk(now);
       
   233                 RequestEngine.doChunkEnd();
       
   234                 MetadataRepository.getInstance().setOutput(newChunk.getUnfishedFile().toString());
       
   235             }
       
   236             recording.setState(RecordingState.RUNNING);
       
   237             updateSettings();
       
   238             writeMetaEvents();
       
   239             if (currentChunk != null) {
       
   240                 finishChunk(currentChunk, now, recording);
       
   241             }
       
   242             currentChunk = newChunk;
       
   243         }
       
   244 
       
   245         RequestEngine.doChunkBegin();
       
   246     }
       
   247 
       
   248     synchronized void stop(PlatformRecording recording) {
       
   249         RecordingState state = recording.getState();
       
   250 
       
   251         if (Utils.isAfter(state, RecordingState.RUNNING)) {
       
   252             throw new IllegalStateException("Can't stop an already stopped recording.");
       
   253         }
       
   254         if (Utils.isBefore(state, RecordingState.RUNNING)) {
       
   255             throw new IllegalStateException("Recording must be started before it can be stopped.");
       
   256         }
       
   257         Instant now = Instant.now();
       
   258         boolean toDisk = false;
       
   259         boolean endPhysical = true;
       
   260         for (PlatformRecording s : getRecordings()) {
       
   261             RecordingState rs = s.getState();
       
   262             if (s != recording && RecordingState.RUNNING == rs) {
       
   263                 endPhysical = false;
       
   264                 if (s.isToDisk()) {
       
   265                     toDisk = true;
       
   266                 }
       
   267             }
       
   268         }
       
   269         emitOldObjectSamples(recording);
       
   270 
       
   271         if (endPhysical) {
       
   272             RequestEngine.doChunkEnd();
       
   273             if (recording.isToDisk()) {
       
   274                 if (currentChunk != null) {
       
   275                     MetadataRepository.getInstance().setOutput(null);
       
   276                     finishChunk(currentChunk, now, null);
       
   277                     currentChunk = null;
       
   278                 }
       
   279             } else {
       
   280                 // last memory
       
   281                 dumpMemoryToDestination(recording);
       
   282             }
       
   283             jvm.endRecording_();
       
   284             disableEvents();
       
   285         } else {
       
   286             RepositoryChunk newChunk = null;
       
   287             RequestEngine.doChunkEnd();
       
   288             updateSettingsButIgnoreRecording(recording);
       
   289             if (toDisk) {
       
   290                 newChunk = repository.newChunk(now);
       
   291                 MetadataRepository.getInstance().setOutput(newChunk.getUnfishedFile().toString());
       
   292             } else {
       
   293                 MetadataRepository.getInstance().setOutput(null);
       
   294             }
       
   295             writeMetaEvents();
       
   296             if (currentChunk != null) {
       
   297                 finishChunk(currentChunk, now, null);
       
   298             }
       
   299             currentChunk = newChunk;
       
   300             RequestEngine.doChunkBegin();
       
   301         }
       
   302         recording.setState(RecordingState.STOPPED);
       
   303     }
       
   304 
       
   305     // Only dump event if the recording that is being stopped
       
   306     // has OldObjectSample enabled + cutoff from recording, not global value
       
   307     private void emitOldObjectSamples(PlatformRecording recording) {
       
   308         Map<String, String> settings = recording.getSettings();
       
   309         String s = settings.get(Type.EVENT_NAME_PREFIX + "OldObjectSample#" + Enabled.NAME);
       
   310         if ("true".equals(s)) {
       
   311             long nanos = CutoffSetting.parseValueSafe(settings.get(Type.EVENT_NAME_PREFIX + "OldObjectSample#" + Cutoff.NAME));
       
   312             long ticks = Utils.nanosToTicks(nanos);
       
   313             JVM.getJVM().emitOldObjectSamples(ticks, WhiteBox.getWriteAllObjectSamples());
       
   314         }
       
   315     }
       
   316 
       
   317     private void dumpMemoryToDestination(PlatformRecording recording) {
       
   318         WriteableUserPath dest = recording.getDestination();
       
   319         if (dest != null) {
       
   320             MetadataRepository.getInstance().setOutput(dest.getText());
       
   321             recording.clearDestination();
       
   322         }
       
   323     }
       
   324 
       
   325     private void disableEvents() {
       
   326         MetadataRepository.getInstance().disableEvents();
       
   327     }
       
   328 
       
   329     void updateSettings() {
       
   330         updateSettingsButIgnoreRecording(null);
       
   331     }
       
   332 
       
   333     void updateSettingsButIgnoreRecording(PlatformRecording ignoreMe) {
       
   334         List<PlatformRecording> recordings = getRunningRecordings();
       
   335         List<Map<String, String>> list = new ArrayList<>(recordings.size());
       
   336         for (PlatformRecording r : recordings) {
       
   337             if (r != ignoreMe) {
       
   338                 list.add(r.getSettings());
       
   339             }
       
   340         }
       
   341         MetadataRepository.getInstance().setSettings(list);
       
   342     }
       
   343 
       
   344     synchronized void rotateDisk() {
       
   345         Instant now = Instant.now();
       
   346         RepositoryChunk newChunk = repository.newChunk(now);
       
   347         RequestEngine.doChunkEnd();
       
   348         MetadataRepository.getInstance().setOutput(newChunk.getUnfishedFile().toString());
       
   349         writeMetaEvents();
       
   350         if (currentChunk != null) {
       
   351             finishChunk(currentChunk, now, null);
       
   352         }
       
   353         currentChunk = newChunk;
       
   354         RequestEngine.doChunkBegin();
       
   355     }
       
   356 
       
   357     private List<PlatformRecording> getRunningRecordings() {
       
   358         List<PlatformRecording> runningRecordings = new ArrayList<>();
       
   359         for (PlatformRecording recording : getRecordings()) {
       
   360             if (recording.getState() == RecordingState.RUNNING) {
       
   361                 runningRecordings.add(recording);
       
   362             }
       
   363         }
       
   364         return runningRecordings;
       
   365     }
       
   366 
       
   367     private List<RepositoryChunk> makeChunkList(Instant startTime, Instant endTime) {
       
   368         Set<RepositoryChunk> chunkSet = new HashSet<>();
       
   369         for (PlatformRecording r : getRecordings()) {
       
   370             chunkSet.addAll(r.getChunks());
       
   371         }
       
   372         if (chunkSet.size() > 0) {
       
   373             List<RepositoryChunk> chunks = new ArrayList<>(chunkSet.size());
       
   374             for (RepositoryChunk rc : chunkSet) {
       
   375                 if (rc.inInterval(startTime, endTime)) {
       
   376                     chunks.add(rc);
       
   377                 }
       
   378             }
       
   379             // n*log(n), should be able to do n*log(k) with a priority queue,
       
   380             // where k = number of recordings, n = number of chunks
       
   381             Collections.sort(chunks, RepositoryChunk.END_TIME_COMPARATOR);
       
   382             return chunks;
       
   383         }
       
   384 
       
   385         return Collections.emptyList();
       
   386     }
       
   387 
       
   388     private void startDiskMonitor() {
       
   389         Thread t = SecuritySupport.createThreadWitNoPermissions("JFR Periodic Tasks", () -> periodicTask());
       
   390         SecuritySupport.setDaemonThread(t, true);
       
   391         t.start();
       
   392     }
       
   393 
       
   394     private void finishChunk(RepositoryChunk chunk, Instant time, PlatformRecording ignoreMe) {
       
   395         chunk.finish(time);
       
   396         for (PlatformRecording r : getRecordings()) {
       
   397             if (r != ignoreMe && r.getState() == RecordingState.RUNNING) {
       
   398                 r.appendChunk(chunk);
       
   399             }
       
   400         }
       
   401     }
       
   402 
       
   403     private void writeMetaEvents() {
       
   404 
       
   405         if (activeRecordingEvent.isEnabled()) {
       
   406             for (PlatformRecording r : getRecordings()) {
       
   407                 if (r.getState() == RecordingState.RUNNING && r.shouldWriteMetadataEvent()) {
       
   408                     ActiveRecordingEvent event = new ActiveRecordingEvent();
       
   409                     event.id = r.getId();
       
   410                     event.name = r.getName();
       
   411                     WriteableUserPath p = r.getDestination();
       
   412                     event.destination = p == null ? null : p.getText();
       
   413                     Duration d = r.getDuration();
       
   414                     event.recordingDuration = d == null ? Long.MAX_VALUE : d.toMillis();
       
   415                     Duration age = r.getMaxAge();
       
   416                     event.maxAge = age == null ? Long.MAX_VALUE : age.toMillis();
       
   417                     Long size = r.getMaxSize();
       
   418                     event.maxSize = size == null ? Long.MAX_VALUE : size;
       
   419                     Instant start = r.getStartTime();
       
   420                     event.recordingStart = start == null ? Long.MAX_VALUE : start.toEpochMilli();
       
   421                     event.commit();
       
   422                 }
       
   423             }
       
   424         }
       
   425         if (activeSettingEvent.isEnabled()) {
       
   426             for (EventControl ec : MetadataRepository.getInstance().getEventControls()) {
       
   427                 ec.writeActiveSettingEvent();
       
   428             }
       
   429         }
       
   430     }
       
   431 
       
   432     private void periodicTask() {
       
   433         while (true) {
       
   434             synchronized (this) {
       
   435                 if (!jvm.hasNativeJFR()) {
       
   436                     return;
       
   437                 }
       
   438                 if (currentChunk != null) {
       
   439                     try {
       
   440                         if (SecuritySupport.getFileSize(currentChunk.getUnfishedFile()) > Options.getMaxChunkSize()) {
       
   441                             rotateDisk();
       
   442                         }
       
   443                     } catch (IOException e) {
       
   444                         Logger.log(JFR_SYSTEM, WARN, "Could not check file size to determine chunk rotation");
       
   445                     }
       
   446                 }
       
   447             }
       
   448             long minDelta = RequestEngine.doPeriodic();
       
   449             long wait = Math.min(minDelta, Options.getWaitInterval());
       
   450             takeNap(wait);
       
   451         }
       
   452     }
       
   453 
       
   454     private void takeNap(long duration) {
       
   455         try {
       
   456             synchronized (JVM.FILE_DELTA_CHANGE) {
       
   457                 JVM.FILE_DELTA_CHANGE.wait(duration < 10 ? 10 : duration);
       
   458             }
       
   459         } catch (InterruptedException e) {
       
   460             e.printStackTrace();
       
   461         }
       
   462     }
       
   463 
       
   464     synchronized Recording newCopy(PlatformRecording r, boolean stop) {
       
   465         Recording newRec = new Recording();
       
   466         PlatformRecording copy = PrivateAccess.getInstance().getPlatformRecording(newRec);
       
   467         copy.setSettings(r.getSettings());
       
   468         copy.setMaxAge(r.getMaxAge());
       
   469         copy.setMaxSize(r.getMaxSize());
       
   470         copy.setDumpOnExit(r.getDumpOnExit());
       
   471         copy.setName("Clone of " + r.getName());
       
   472         copy.setToDisk(r.isToDisk());
       
   473         copy.setInternalDuration(r.getDuration());
       
   474         copy.setStartTime(r.getStartTime());
       
   475         copy.setStopTime(r.getStopTime());
       
   476 
       
   477         if (r.getState() == RecordingState.NEW) {
       
   478             return newRec;
       
   479         }
       
   480         if (r.getState() == RecordingState.DELAYED) {
       
   481             copy.scheduleStart(r.getStartTime());
       
   482             return newRec;
       
   483         }
       
   484         copy.setState(r.getState());
       
   485         // recording has started, copy chunks
       
   486         for (RepositoryChunk c : r.getChunks()) {
       
   487             copy.add(c);
       
   488         }
       
   489         if (r.getState() == RecordingState.RUNNING) {
       
   490             if (stop) {
       
   491                 copy.stop("Stopped when cloning recording '" + r.getName() + "'");
       
   492             } else {
       
   493                 if (r.getStopTime() != null) {
       
   494                     TimerTask stopTask = copy.createStopTask();
       
   495                     copy.setStopTask(copy.createStopTask());
       
   496                     getTimer().schedule(stopTask, r.getStopTime().toEpochMilli());
       
   497                 }
       
   498             }
       
   499         }
       
   500         return newRec;
       
   501     }
       
   502 
       
   503     public synchronized Recording newSnapshot() {
       
   504         boolean running = false;
       
   505         boolean toDisk = false;
       
   506         for (PlatformRecording r : recordings) {
       
   507             if (r.getState() == RecordingState.RUNNING) {
       
   508                 running = true;
       
   509             }
       
   510             if (r.isToDisk()) {
       
   511                 toDisk = true;
       
   512             }
       
   513 
       
   514         }
       
   515         // If needed, flush data from memory
       
   516         if (running) {
       
   517             if (toDisk) {
       
   518                 rotateDisk();
       
   519             } else {
       
   520                 try (Recording snapshot = new Recording()) {
       
   521                     PlatformRecording internal = PrivateAccess.getInstance().getPlatformRecording(snapshot);
       
   522                     internal.setShouldWriteActiveRecordingEvent(false);
       
   523                     snapshot.start();
       
   524                     snapshot.stop();
       
   525                     return makeRecordingWithDiskChunks();
       
   526                 }
       
   527             }
       
   528         }
       
   529         return makeRecordingWithDiskChunks();
       
   530     }
       
   531 
       
   532     private Recording makeRecordingWithDiskChunks() {
       
   533         Recording snapshot = new Recording();
       
   534         snapshot.setName("Snapshot");
       
   535         PlatformRecording internal = PrivateAccess.getInstance().getPlatformRecording(snapshot);
       
   536         for (RepositoryChunk c : makeChunkList(null, null)) {
       
   537             internal.add(c);
       
   538         }
       
   539         internal.setState(RecordingState.STOPPED);
       
   540         Instant startTime = null;
       
   541         Instant endTime = null;
       
   542 
       
   543         for (RepositoryChunk c : makeChunkList(null, null)) {
       
   544             if (startTime == null || c.getStartTime().isBefore(startTime)) {
       
   545                 startTime = c.getStartTime();
       
   546             }
       
   547             if (endTime == null || c.getEndTime().isAfter(endTime)) {
       
   548                 endTime = c.getEndTime();
       
   549             }
       
   550         }
       
   551         Instant now = Instant.now();
       
   552         if (startTime == null) {
       
   553             startTime = now;
       
   554         }
       
   555         if (endTime == null) {
       
   556             endTime = now;
       
   557         }
       
   558         internal.setStartTime(startTime);
       
   559         internal.setStopTime(endTime);
       
   560         internal.setInternalDuration(Duration.between(startTime, endTime));
       
   561         return snapshot;
       
   562     }
       
   563 }