1 /* 2 * Copyright (c) 2016, 2019, 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("com.oracle.jdk.OldObjectSample#" + Enabled.NAME); 310 if ("true".equals(s)) { 311 long nanos = CutoffSetting.parseValueSafe(settings.get("com.oracle.jdk.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 }