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