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