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.DEBUG; 29 import static jdk.jfr.internal.LogLevel.WARN; 30 import static jdk.jfr.internal.LogTag.JFR; 31 32 import java.io.IOException; 33 import java.io.InputStream; 34 import java.nio.channels.FileChannel; 35 import java.nio.file.StandardOpenOption; 36 import java.security.AccessControlContext; 37 import java.security.AccessController; 38 import java.time.Duration; 39 import java.time.Instant; 40 import java.time.LocalDateTime; 41 import java.util.ArrayList; 42 import java.util.Collections; 43 import java.util.Date; 44 import java.util.LinkedHashMap; 45 import java.util.LinkedList; 46 import java.util.List; 47 import java.util.Map; 48 import java.util.StringJoiner; 49 import java.util.TimerTask; 50 import java.util.TreeMap; 51 52 import jdk.jfr.Configuration; 53 import jdk.jfr.FlightRecorderListener; 54 import jdk.jfr.Recording; 55 import jdk.jfr.RecordingState; 56 import jdk.jfr.internal.SecuritySupport.SafePath; 57 58 public final class PlatformRecording implements AutoCloseable { 59 60 private final PlatformRecorder recorder; 61 private final long id; 62 // Recording settings 63 private Map<String, String> settings = new LinkedHashMap<>(); 64 private Duration duration; 65 private Duration maxAge; 66 private long maxSize; 67 68 private WriteableUserPath destination; 69 70 private boolean toDisk = true; 71 private String name; 72 private boolean dumpOnExit; 73 private SafePath dumpOnExitDirectory = new SafePath("."); 74 // Timestamp information 75 private Instant stopTime; 76 private Instant startTime; 77 78 // Misc, information 79 private RecordingState state = RecordingState.NEW; 80 private long size; 81 private final LinkedList<RepositoryChunk> chunks = new LinkedList<>(); 82 private volatile Recording recording; 83 private TimerTask stopTask; 84 private TimerTask startTask; 85 private AccessControlContext noDestinationDumpOnExitAccessControlContext; 86 private boolean shuoldWriteActiveRecordingEvent = true; 87 88 PlatformRecording(PlatformRecorder recorder, long id) { 89 // Typically the access control context is taken 90 // when you call dump(Path) or setDdestination(Path), 91 // but if no destination is set and dumponexit=true 92 // the control context of the recording is taken when the 93 // Recording object is constructed. This works well for 94 // -XX:StartFlightRecording and JFR.dump 95 this.noDestinationDumpOnExitAccessControlContext = AccessController.getContext(); 96 this.id = id; 97 this.recorder = recorder; 98 this.name = String.valueOf(id); 99 } 100 101 public void start() { 102 RecordingState oldState; 103 RecordingState newState; 104 synchronized (recorder) { 105 oldState = getState(); 106 if (!Utils.isBefore(state, RecordingState.RUNNING)) { 107 throw new IllegalStateException("Recording can only be started once."); 108 } 109 if (startTask != null) { 110 startTask.cancel(); 111 startTask = null; 112 startTime = null; 113 } 114 recorder.start(this); 115 Logger.log(LogTag.JFR, LogLevel.INFO, () -> { 116 // Only print non-default values so it easy to see 117 // which options were added 118 StringJoiner options = new StringJoiner(", "); 119 if (!toDisk) { 120 options.add("disk=false"); 121 } 122 if (maxAge != null) { 123 options.add("maxage=" + Utils.formatTimespan(maxAge, "")); 124 } 125 if (maxSize != 0) { 126 options.add("maxsize=" + Utils.formatBytesCompact(maxSize)); 127 } 128 if (dumpOnExit) { 129 options.add("dumponexit=true"); 130 } 131 if (duration != null) { 132 options.add("duration=" + Utils.formatTimespan(duration, "")); 133 } 134 if (destination != null) { 135 options.add("filename=" + destination.getText()); 136 } 137 String optionText = options.toString(); 138 if (optionText.length() != 0) { 139 optionText = "{" + optionText + "}"; 140 } 141 return "Started recording \"" + getName() + "\" (" + getId() + ") " + optionText; 142 }); 143 newState = getState(); 144 } 145 notifyIfStateChanged(oldState, newState); 146 } 147 148 public boolean stop(String reason) { 149 RecordingState oldState; 150 RecordingState newState; 151 synchronized (recorder) { 152 oldState = getState(); 153 if (stopTask != null) { 154 stopTask.cancel(); 155 stopTask = null; 156 } 157 recorder.stop(this); 158 String endText = reason == null ? "" : ". Reason \"" + reason + "\"."; 159 Logger.log(LogTag.JFR, LogLevel.INFO, "Stopped recording \"" + getName() + "\" (" + getId() + ")" + endText); 160 this.stopTime = Instant.now(); 161 newState = getState(); 162 } 163 WriteableUserPath dest = getDestination(); 164 165 if (dest != null) { 166 try { 167 dumpStopped(dest); 168 Logger.log(LogTag.JFR, LogLevel.INFO, "Wrote recording \"" + getName() + "\" (" + getId() + ") to " + dest.getText()); 169 notifyIfStateChanged(newState, oldState); 170 close(); // remove if copied out 171 } catch(IOException e) { 172 // throw e; // BUG8925030 173 } 174 } else { 175 notifyIfStateChanged(newState, oldState); 176 } 177 return true; 178 } 179 180 public void scheduleStart(Duration delay) { 181 synchronized (recorder) { 182 ensureOkForSchedule(); 183 184 startTime = Instant.now().plus(delay); 185 LocalDateTime now = LocalDateTime.now().plus(delay); 186 setState(RecordingState.DELAYED); 187 startTask = createStartTask(); 188 recorder.getTimer().schedule(startTask, delay.toMillis()); 189 Logger.log(LogTag.JFR, LogLevel.INFO, "Scheduled recording \"" + getName() + "\" (" + getId() + ") to start at " + now); 190 } 191 } 192 193 private void ensureOkForSchedule() { 194 if (getState() != RecordingState.NEW) { 195 throw new IllegalStateException("Only a new recoridng can be scheduled for start"); 196 } 197 } 198 199 private TimerTask createStartTask() { 200 // Taking ref. to recording here. 201 // Opens up for memory leaks. 202 return new TimerTask() { 203 @Override 204 public void run() { 205 synchronized (recorder) { 206 if (getState() != RecordingState.DELAYED) { 207 return; 208 } 209 start(); 210 } 211 } 212 }; 213 } 214 215 void scheduleStart(Instant startTime) { 216 synchronized (recorder) { 217 ensureOkForSchedule(); 218 this.startTime = startTime; 219 setState(RecordingState.DELAYED); 220 startTask = createStartTask(); 221 recorder.getTimer().schedule(startTask, startTime.toEpochMilli()); 222 } 223 } 224 225 public Map<String, String> getSettings() { 226 synchronized (recorder) { 227 return settings; 228 } 229 } 230 231 public long getSize() { 232 return size; 233 } 234 235 public Instant getStopTime() { 236 synchronized (recorder) { 237 return stopTime; 238 } 239 } 240 241 public Instant getStartTime() { 242 synchronized (recorder) { 243 return startTime; 244 } 245 } 246 247 public Long getMaxSize() { 248 synchronized (recorder) { 249 return maxSize; 250 } 251 } 252 253 public Duration getMaxAge() { 254 synchronized (recorder) { 255 return maxAge; 256 } 257 } 258 259 public String getName() { 260 synchronized (recorder) { 261 return name; 262 } 263 } 264 265 public RecordingState getState() { 266 synchronized (recorder) { 267 return state; 268 } 269 } 270 271 @Override 272 public void close() { 273 RecordingState oldState; 274 RecordingState newState; 275 276 synchronized (recorder) { 277 oldState = getState(); 278 if (RecordingState.CLOSED != getState()) { 279 if (startTask != null) { 280 startTask.cancel(); 281 startTask = null; 282 } 283 recorder.finish(this); 284 for (RepositoryChunk c : chunks) { 285 removed(c); 286 } 287 chunks.clear(); 288 setState(RecordingState.CLOSED); 289 Logger.log(LogTag.JFR, LogLevel.INFO, "Closed recording \"" + getName() + "\" (" + getId() + ")"); 290 } 291 newState = getState(); 292 } 293 notifyIfStateChanged(newState, oldState); 294 } 295 296 // To be used internally when doing dumps. 297 // Caller must have recorder lock and close recording before releasing lock 298 public PlatformRecording newSnapshotClone(String reason, Boolean pathToGcRoots) throws IOException { 299 if(!Thread.holdsLock(recorder)) { 300 throw new InternalError("Caller must have recorder lock"); 301 } 302 RecordingState state = getState(); 303 if (state == RecordingState.CLOSED) { 304 throw new IOException("Recording \"" + name + "\" (id=" + id + ") has been closed, no contents to write"); 305 } 306 if (state == RecordingState.DELAYED || state == RecordingState.NEW) { 307 throw new IOException("Recording \"" + name + "\" (id=" + id + ") has not started, no contents to write"); 308 } 309 if (state == RecordingState.STOPPED) { 310 PlatformRecording clone = recorder.newTemporaryRecording(); 311 for (RepositoryChunk r : chunks) { 312 clone.add(r); 313 } 314 return clone; 315 } 316 317 // Recording is RUNNING, create a clone 318 PlatformRecording clone = recorder.newTemporaryRecording(); 319 clone.setShouldWriteActiveRecordingEvent(false); 320 clone.setName(getName()); 321 clone.setToDisk(true); 322 // We purposely don't clone settings here, since 323 // a union a == a 324 if (!isToDisk()) { 325 // force memory contents to disk 326 clone.start(); 327 } else { 328 // using existing chunks on disk 329 for (RepositoryChunk c : chunks) { 330 clone.add(c); 331 } 332 clone.setState(RecordingState.RUNNING); 333 clone.setStartTime(getStartTime()); 334 } 335 if (pathToGcRoots == null) { 336 clone.setSettings(getSettings()); // needed for old object sample 337 clone.stop(reason); // dumps to destination path here 338 } else { 339 // Risk of violating lock order here, since 340 // clone.stop() will take recorder lock inside 341 // metadata lock, but OK if we already 342 // have recorder lock when we entered metadata lock 343 synchronized (MetadataRepository.getInstance()) { 344 clone.setSettings(OldObjectSample.createSettingsForSnapshot(this, pathToGcRoots)); 345 clone.stop(reason); 346 } 347 } 348 return clone; 349 } 350 351 public boolean isToDisk() { 352 synchronized (recorder) { 353 return toDisk; 354 } 355 } 356 357 public void setMaxSize(long maxSize) { 358 synchronized (recorder) { 359 if (getState() == RecordingState.CLOSED) { 360 throw new IllegalStateException("Can't set max age when recording is closed"); 361 } 362 this.maxSize = maxSize; 363 trimToSize(); 364 } 365 } 366 367 public void setDestination(WriteableUserPath userSuppliedPath) throws IOException { 368 synchronized (recorder) { 369 if (Utils.isState(getState(), RecordingState.STOPPED, RecordingState.CLOSED)) { 370 throw new IllegalStateException("Destination can't be set on a recording that has been stopped/closed"); 371 } 372 this.destination = userSuppliedPath; 373 } 374 } 375 376 public WriteableUserPath getDestination() { 377 synchronized (recorder) { 378 return destination; 379 } 380 } 381 382 void setState(RecordingState state) { 383 synchronized (recorder) { 384 this.state = state; 385 } 386 } 387 388 void setStartTime(Instant startTime) { 389 synchronized (recorder) { 390 this.startTime = startTime; 391 } 392 } 393 394 void setStopTime(Instant timeStamp) { 395 synchronized (recorder) { 396 stopTime = timeStamp; 397 } 398 } 399 400 public long getId() { 401 synchronized (recorder) { 402 return id; 403 } 404 } 405 406 public void setName(String name) { 407 synchronized (recorder) { 408 ensureNotClosed(); 409 this.name = name; 410 } 411 } 412 413 private void ensureNotClosed() { 414 if (getState() == RecordingState.CLOSED) { 415 throw new IllegalStateException("Can't change name on a closed recording"); 416 } 417 } 418 419 public void setDumpOnExit(boolean dumpOnExit) { 420 synchronized (recorder) { 421 this.dumpOnExit = dumpOnExit; 422 } 423 } 424 425 public boolean getDumpOnExit() { 426 synchronized (recorder) { 427 return dumpOnExit; 428 } 429 } 430 431 public void setToDisk(boolean toDisk) { 432 synchronized (recorder) { 433 if (Utils.isState(getState(), RecordingState.NEW, RecordingState.DELAYED)) { 434 this.toDisk = toDisk; 435 } else { 436 throw new IllegalStateException("Recording option disk can't be changed after recording has started"); 437 } 438 } 439 } 440 441 public void setSetting(String id, String value) { 442 synchronized (recorder) { 443 this.settings.put(id, value); 444 if (getState() == RecordingState.RUNNING) { 445 recorder.updateSettings(); 446 } 447 } 448 } 449 450 public void setSettings(Map<String, String> settings) { 451 setSettings(settings, true); 452 } 453 454 private void setSettings(Map<String, String> settings, boolean update) { 455 if (Logger.shouldLog(LogTag.JFR_SETTING, LogLevel.INFO) && update) { 456 TreeMap<String, String> ordered = new TreeMap<>(settings); 457 Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, "New settings for recording \"" + getName() + "\" (" + getId() + ")"); 458 for (Map.Entry<String, String> entry : ordered.entrySet()) { 459 String text = entry.getKey() + "=\"" + entry.getValue() + "\""; 460 Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, text); 461 } 462 } 463 synchronized (recorder) { 464 this.settings = new LinkedHashMap<>(settings); 465 if (getState() == RecordingState.RUNNING && update) { 466 recorder.updateSettings(); 467 } 468 } 469 } 470 471 private void notifyIfStateChanged(RecordingState newState, RecordingState oldState) { 472 if (oldState == newState) { 473 return; 474 } 475 for (FlightRecorderListener cl : PlatformRecorder.getListeners()) { 476 try { 477 cl.recordingStateChanged(getRecording()); 478 } catch (RuntimeException re) { 479 Logger.log(JFR, WARN, "Error notifying recorder listener:" + re.getMessage()); 480 } 481 } 482 } 483 484 public void setRecording(Recording recording) { 485 this.recording = recording; 486 } 487 488 public Recording getRecording() { 489 return recording; 490 } 491 492 @Override 493 public String toString() { 494 return getName() + " (id=" + getId() + ") " + getState(); 495 } 496 497 public void setConfiguration(Configuration c) { 498 setSettings(c.getSettings()); 499 } 500 501 public void setMaxAge(Duration maxAge) { 502 synchronized (recorder) { 503 if (getState() == RecordingState.CLOSED) { 504 throw new IllegalStateException("Can't set max age when recording is closed"); 505 } 506 this.maxAge = maxAge; 507 if (maxAge != null) { 508 trimToAge(Instant.now().minus(maxAge)); 509 } 510 } 511 } 512 513 void appendChunk(RepositoryChunk chunk) { 514 if (!chunk.isFinished()) { 515 throw new Error("not finished chunk " + chunk.getStartTime()); 516 } 517 synchronized (recorder) { 518 if (!toDisk) { 519 return; 520 } 521 if (maxAge != null) { 522 trimToAge(chunk.getEndTime().minus(maxAge)); 523 } 524 chunks.addLast(chunk); 525 added(chunk); 526 trimToSize(); 527 } 528 } 529 530 private void trimToSize() { 531 if (maxSize == 0) { 532 return; 533 } 534 while (size > maxSize && chunks.size() > 1) { 535 RepositoryChunk c = chunks.removeFirst(); 536 removed(c); 537 } 538 } 539 540 private void trimToAge(Instant oldest) { 541 while (!chunks.isEmpty()) { 542 RepositoryChunk oldestChunk = chunks.peek(); 543 if (oldestChunk.getEndTime().isAfter(oldest)) { 544 return; 545 } 546 chunks.removeFirst(); 547 removed(oldestChunk); 548 } 549 } 550 551 void add(RepositoryChunk c) { 552 chunks.add(c); 553 added(c); 554 } 555 556 private void added(RepositoryChunk c) { 557 c.use(); 558 size += c.getSize(); 559 Logger.log(JFR, DEBUG, () -> "Recording \"" + name + "\" (" + id + ") added chunk " + c.toString() + ", current size=" + size); 560 } 561 562 private void removed(RepositoryChunk c) { 563 size -= c.getSize(); 564 Logger.log(JFR, DEBUG, () -> "Recording \"" + name + "\" (" + id + ") removed chunk " + c.toString() + ", current size=" + size); 565 c.release(); 566 } 567 568 public List<RepositoryChunk> getChunks() { 569 return chunks; 570 } 571 572 public InputStream open(Instant start, Instant end) throws IOException { 573 synchronized (recorder) { 574 if (getState() != RecordingState.STOPPED) { 575 throw new IOException("Recording must be stopped before it can be read."); 576 } 577 List<RepositoryChunk> chunksToUse = new ArrayList<RepositoryChunk>(); 578 for (RepositoryChunk chunk : chunks) { 579 if (chunk.isFinished()) { 580 Instant chunkStart = chunk.getStartTime(); 581 Instant chunkEnd = chunk.getEndTime(); 582 if (start == null || !chunkEnd.isBefore(start)) { 583 if (end == null || !chunkStart.isAfter(end)) { 584 chunksToUse.add(chunk); 585 } 586 } 587 } 588 } 589 if (chunksToUse.isEmpty()) { 590 return null; 591 } 592 return new ChunkInputStream(chunksToUse); 593 } 594 } 595 596 public Duration getDuration() { 597 synchronized (recorder) { 598 return duration; 599 } 600 } 601 602 void setInternalDuration(Duration duration) { 603 this.duration = duration; 604 } 605 606 public void setDuration(Duration duration) { 607 synchronized (recorder) { 608 if (Utils.isState(getState(), RecordingState.STOPPED, RecordingState.CLOSED)) { 609 throw new IllegalStateException("Duration can't be set after a recording has been stopped/closed"); 610 } 611 setInternalDuration(duration); 612 if (getState() != RecordingState.NEW) { 613 updateTimer(); 614 } 615 } 616 } 617 618 void updateTimer() { 619 if (stopTask != null) { 620 stopTask.cancel(); 621 stopTask = null; 622 } 623 if (getState() == RecordingState.CLOSED) { 624 return; 625 } 626 if (duration != null) { 627 stopTask = createStopTask(); 628 recorder.getTimer().schedule(stopTask, new Date(startTime.plus(duration).toEpochMilli())); 629 } 630 } 631 632 TimerTask createStopTask() { 633 return new TimerTask() { 634 @Override 635 public void run() { 636 try { 637 stop("End of duration reached"); 638 } catch (Throwable t) { 639 // Prevent malicious user to propagate exception callback in the wrong context 640 Logger.log(LogTag.JFR, LogLevel.ERROR, "Could not stop recording."); 641 } 642 } 643 }; 644 } 645 646 public Recording newCopy(boolean stop) { 647 return recorder.newCopy(this, stop); 648 } 649 650 void setStopTask(TimerTask stopTask) { 651 synchronized (recorder) { 652 this.stopTask = stopTask; 653 } 654 } 655 656 void clearDestination() { 657 destination = null; 658 } 659 660 public AccessControlContext getNoDestinationDumpOnExitAccessControlContext() { 661 return noDestinationDumpOnExitAccessControlContext; 662 } 663 664 void setShouldWriteActiveRecordingEvent(boolean shouldWrite) { 665 this.shuoldWriteActiveRecordingEvent = shouldWrite; 666 } 667 668 boolean shouldWriteMetadataEvent() { 669 return shuoldWriteActiveRecordingEvent; 670 } 671 672 // Dump running and stopped recordings 673 public void dump(WriteableUserPath writeableUserPath) throws IOException { 674 synchronized (recorder) { 675 try(PlatformRecording p = newSnapshotClone("Dumped by user", null)) { 676 p.dumpStopped(writeableUserPath); 677 } 678 } 679 } 680 681 public void dumpStopped(WriteableUserPath userPath) throws IOException { 682 synchronized (recorder) { 683 userPath.doPriviligedIO(() -> { 684 try (ChunksChannel cc = new ChunksChannel(chunks); FileChannel fc = FileChannel.open(userPath.getReal(), StandardOpenOption.WRITE, StandardOpenOption.APPEND)) { 685 cc.transferTo(fc); 686 fc.force(true); 687 } 688 return null; 689 }); 690 } 691 } 692 693 public void filter(Instant begin, Instant end, Long maxSize) { 694 synchronized (recorder) { 695 List<RepositoryChunk> result = removeAfter(end, removeBefore(begin, new ArrayList<>(chunks))); 696 if (maxSize != null) { 697 if (begin != null && end == null) { 698 result = reduceFromBeginning(maxSize, result); 699 } else { 700 result = reduceFromEnd(maxSize, result); 701 } 702 } 703 int size = 0; 704 for (RepositoryChunk r : result) { 705 size += r.getSize(); 706 r.use(); 707 } 708 this.size = size; 709 for (RepositoryChunk r : chunks) { 710 r.release(); 711 } 712 chunks.clear(); 713 chunks.addAll(result); 714 } 715 } 716 717 private static List<RepositoryChunk> removeBefore(Instant time, List<RepositoryChunk> input) { 718 if (time == null) { 719 return input; 720 } 721 List<RepositoryChunk> result = new ArrayList<>(input.size()); 722 for (RepositoryChunk r : input) { 723 if (!r.getEndTime().isBefore(time)) { 724 result.add(r); 725 } 726 } 727 return result; 728 } 729 730 private static List<RepositoryChunk> removeAfter(Instant time, List<RepositoryChunk> input) { 731 if (time == null) { 732 return input; 733 } 734 List<RepositoryChunk> result = new ArrayList<>(input.size()); 735 for (RepositoryChunk r : input) { 736 if (!r.getStartTime().isAfter(time)) { 737 result.add(r); 738 } 739 } 740 return result; 741 } 742 743 private static List<RepositoryChunk> reduceFromBeginning(Long maxSize, List<RepositoryChunk> input) { 744 if (maxSize == null || input.isEmpty()) { 745 return input; 746 } 747 List<RepositoryChunk> result = new ArrayList<>(input.size()); 748 long total = 0; 749 for (RepositoryChunk r : input) { 750 total += r.getSize(); 751 if (total > maxSize) { 752 break; 753 } 754 result.add(r); 755 } 756 // always keep at least one chunk 757 if (result.isEmpty()) { 758 result.add(input.get(0)); 759 } 760 return result; 761 } 762 763 private static List<RepositoryChunk> reduceFromEnd(Long maxSize, List<RepositoryChunk> input) { 764 Collections.reverse(input); 765 List<RepositoryChunk> result = reduceFromBeginning(maxSize, input); 766 Collections.reverse(result); 767 return result; 768 } 769 770 public void setDumpOnExitDirectory(SafePath directory) { 771 this.dumpOnExitDirectory = directory; 772 } 773 774 public SafePath getDumpOnExitDirectory() { 775 return this.dumpOnExitDirectory; 776 } 777 }