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