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.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.formatBytes(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.setDestination(this.destination); 322 clone.setToDisk(true); 323 // We purposely don't clone settings here, since 324 // a union a == a 325 if (!isToDisk()) { 326 // force memory contents to disk 327 clone.start(); 328 } else { 329 // using existing chunks on disk 330 for (RepositoryChunk c : chunks) { 331 clone.add(c); 332 } 333 clone.setState(RecordingState.RUNNING); 334 clone.setStartTime(getStartTime()); 335 } 336 if (pathToGcRoots == null) { 337 clone.setSettings(getSettings()); // needed for old object sample 338 clone.stop(reason); // dumps to destination path here 339 } else { 340 // Risk of violating lock order here, since 341 // clone.stop() will take recorder lock inside 342 // metadata lock, but OK if we already 343 // have recorder lock when we entered metadata lock 344 synchronized (MetadataRepository.getInstance()) { 345 clone.setSettings(OldObjectSample.createSettingsForSnapshot(this, pathToGcRoots)); 346 clone.stop(reason); 347 } 348 } 349 return clone; 350 } 351 352 public boolean isToDisk() { 353 synchronized (recorder) { 354 return toDisk; 355 } 356 } 357 358 public void setMaxSize(long maxSize) { 359 synchronized (recorder) { 360 if (getState() == RecordingState.CLOSED) { 361 throw new IllegalStateException("Can't set max age when recording is closed"); 362 } 363 this.maxSize = maxSize; 364 trimToSize(); 365 } 366 } 367 368 public void setDestination(WriteableUserPath userSuppliedPath) throws IOException { 369 synchronized (recorder) { 370 if (Utils.isState(getState(), RecordingState.STOPPED, RecordingState.CLOSED)) { 371 throw new IllegalStateException("Destination can't be set on a recording that has been stopped/closed"); 372 } 373 this.destination = userSuppliedPath; 374 } 375 } 376 377 public WriteableUserPath getDestination() { 378 synchronized (recorder) { 379 return destination; 380 } 381 } 382 383 void setState(RecordingState state) { 384 synchronized (recorder) { 385 this.state = state; 386 } 387 } 388 389 void setStartTime(Instant startTime) { 390 synchronized (recorder) { 391 this.startTime = startTime; 392 } 393 } 394 395 void setStopTime(Instant timeStamp) { 396 synchronized (recorder) { 397 stopTime = timeStamp; 398 } 399 } 400 401 public long getId() { 402 synchronized (recorder) { 403 return id; 404 } 405 } 406 407 public void setName(String name) { 408 synchronized (recorder) { 409 ensureNotClosed(); 410 this.name = name; 411 } 412 } 413 414 private void ensureNotClosed() { 415 if (getState() == RecordingState.CLOSED) { 416 throw new IllegalStateException("Can't change name on a closed recording"); 417 } 418 } 419 420 public void setDumpOnExit(boolean dumpOnExit) { 421 synchronized (recorder) { 422 this.dumpOnExit = dumpOnExit; 423 } 424 } 425 426 public boolean getDumpOnExit() { 427 synchronized (recorder) { 428 return dumpOnExit; 429 } 430 } 431 432 public void setToDisk(boolean toDisk) { 433 synchronized (recorder) { 434 if (Utils.isState(getState(), RecordingState.NEW, RecordingState.DELAYED)) { 435 this.toDisk = toDisk; 436 } else { 437 throw new IllegalStateException("Recording option disk can't be changed after recording has started"); 438 } 439 } 440 } 441 442 public void setSetting(String id, String value) { 443 synchronized (recorder) { 444 this.settings.put(id, value); 445 if (getState() == RecordingState.RUNNING) { 446 recorder.updateSettings(); 447 } 448 } 449 } 450 451 public void setSettings(Map<String, String> settings) { 452 setSettings(settings, true); 453 } 454 455 private void setSettings(Map<String, String> settings, boolean update) { 456 if (LogTag.JFR_SETTING.shouldLog(LogLevel.INFO.level) && update) { 457 TreeMap<String, String> ordered = new TreeMap<>(settings); 458 Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, "New settings for recording \"" + getName() + "\" (" + getId() + ")"); 459 for (Map.Entry<String, String> entry : ordered.entrySet()) { 460 String text = entry.getKey() + "=\"" + entry.getValue() + "\""; 461 Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, text); 462 } 463 } 464 synchronized (recorder) { 465 this.settings = new LinkedHashMap<>(settings); 466 if (getState() == RecordingState.RUNNING && update) { 467 recorder.updateSettings(); 468 } 469 } 470 } 471 472 private void notifyIfStateChanged(RecordingState newState, RecordingState oldState) { 473 if (oldState == newState) { 474 return; 475 } 476 for (FlightRecorderListener cl : PlatformRecorder.getListeners()) { 477 try { 478 cl.recordingStateChanged(getRecording()); 479 } catch (RuntimeException re) { 480 Logger.log(JFR, WARN, "Error notifying recorder listener:" + re.getMessage()); 481 } 482 } 483 } 484 485 public void setRecording(Recording recording) { 486 this.recording = recording; 487 } 488 489 public Recording getRecording() { 490 return recording; 491 } 492 493 @Override 494 public String toString() { 495 return getName() + " (id=" + getId() + ") " + getState(); 496 } 497 498 public void setConfiguration(Configuration c) { 499 setSettings(c.getSettings()); 500 } 501 502 public void setMaxAge(Duration maxAge) { 503 synchronized (recorder) { 504 if (getState() == RecordingState.CLOSED) { 505 throw new IllegalStateException("Can't set max age when recording is closed"); 506 } 507 this.maxAge = maxAge; 508 if (maxAge != null) { 509 trimToAge(Instant.now().minus(maxAge)); 510 } 511 } 512 } 513 514 void appendChunk(RepositoryChunk chunk) { 515 if (!chunk.isFinished()) { 516 throw new Error("not finished chunk " + chunk.getStartTime()); 517 } 518 synchronized (recorder) { 519 if (!toDisk) { 520 return; 521 } 522 if (maxAge != null) { 523 trimToAge(chunk.getEndTime().minus(maxAge)); 524 } 525 chunks.addLast(chunk); 526 added(chunk); 527 trimToSize(); 528 } 529 } 530 531 private void trimToSize() { 532 if (maxSize == 0) { 533 return; 534 } 535 while (size > maxSize && chunks.size() > 1) { 536 RepositoryChunk c = chunks.removeFirst(); 537 removed(c); 538 } 539 } 540 541 private void trimToAge(Instant oldest) { 542 while (!chunks.isEmpty()) { 543 RepositoryChunk oldestChunk = chunks.peek(); 544 if (oldestChunk.getEndTime().isAfter(oldest)) { 545 return; 546 } 547 chunks.removeFirst(); 548 removed(oldestChunk); 549 } 550 } 551 552 void add(RepositoryChunk c) { 553 chunks.add(c); 554 added(c); 555 } 556 557 private void added(RepositoryChunk c) { 558 c.use(); 559 size += c.getSize(); 560 Logger.log(JFR, DEBUG, () -> "Recording \"" + name + "\" (" + id + ") added chunk " + c.toString() + ", current size=" + size); 561 } 562 563 private void removed(RepositoryChunk c) { 564 size -= c.getSize(); 565 Logger.log(JFR, DEBUG, () -> "Recording \"" + name + "\" (" + id + ") removed chunk " + c.toString() + ", current size=" + size); 566 c.release(); 567 } 568 569 public List<RepositoryChunk> getChunks() { 570 return chunks; 571 } 572 573 public InputStream open(Instant start, Instant end) throws IOException { 574 synchronized (recorder) { 575 if (getState() != RecordingState.STOPPED) { 576 throw new IOException("Recording must be stopped before it can be read."); 577 } 578 List<RepositoryChunk> chunksToUse = new ArrayList<RepositoryChunk>(); 579 for (RepositoryChunk chunk : chunks) { 580 if (chunk.isFinished()) { 581 Instant chunkStart = chunk.getStartTime(); 582 Instant chunkEnd = chunk.getEndTime(); 583 if (start == null || !chunkEnd.isBefore(start)) { 584 if (end == null || !chunkStart.isAfter(end)) { 585 chunksToUse.add(chunk); 586 } 587 } 588 } 589 } 590 if (chunksToUse.isEmpty()) { 591 return null; 592 } 593 return new ChunkInputStream(chunksToUse); 594 } 595 } 596 597 public Duration getDuration() { 598 synchronized (recorder) { 599 return duration; 600 } 601 } 602 603 void setInternalDuration(Duration duration) { 604 this.duration = duration; 605 } 606 607 public void setDuration(Duration duration) { 608 synchronized (recorder) { 609 if (Utils.isState(getState(), RecordingState.STOPPED, RecordingState.CLOSED)) { 610 throw new IllegalStateException("Duration can't be set after a recording has been stopped/closed"); 611 } 612 setInternalDuration(duration); 613 if (getState() != RecordingState.NEW) { 614 updateTimer(); 615 } 616 } 617 } 618 619 void updateTimer() { 620 if (stopTask != null) { 621 stopTask.cancel(); 622 stopTask = null; 623 } 624 if (getState() == RecordingState.CLOSED) { 625 return; 626 } 627 if (duration != null) { 628 stopTask = createStopTask(); 629 recorder.getTimer().schedule(stopTask, new Date(startTime.plus(duration).toEpochMilli())); 630 } 631 } 632 633 TimerTask createStopTask() { 634 return new TimerTask() { 635 @Override 636 public void run() { 637 try { 638 stop("End of duration reached"); 639 } catch (Throwable t) { 640 // Prevent malicious user to propagate exception callback in the wrong context 641 Logger.log(LogTag.JFR, LogLevel.ERROR, "Could not stop recording."); 642 } 643 } 644 }; 645 } 646 647 public Recording newCopy(boolean stop) { 648 return recorder.newCopy(this, stop); 649 } 650 651 void setStopTask(TimerTask stopTask) { 652 synchronized (recorder) { 653 this.stopTask = stopTask; 654 } 655 } 656 657 void clearDestination() { 658 destination = null; 659 } 660 661 public AccessControlContext getNoDestinationDumpOnExitAccessControlContext() { 662 return noDestinationDumpOnExitAccessControlContext; 663 } 664 665 void setShouldWriteActiveRecordingEvent(boolean shouldWrite) { 666 this.shuoldWriteActiveRecordingEvent = shouldWrite; 667 } 668 669 boolean shouldWriteMetadataEvent() { 670 return shuoldWriteActiveRecordingEvent; 671 } 672 673 // Dump running and stopped recordings 674 public void dump(WriteableUserPath writeableUserPath) throws IOException { 675 synchronized (recorder) { 676 try(PlatformRecording p = newSnapshotClone("Dumped by user", null)) { 677 p.dumpStopped(writeableUserPath); 678 } 679 } 680 } 681 682 public void dumpStopped(WriteableUserPath userPath) throws IOException { 683 synchronized (recorder) { 684 userPath.doPriviligedIO(() -> { 685 try (ChunksChannel cc = new ChunksChannel(chunks); FileChannel fc = FileChannel.open(userPath.getReal(), StandardOpenOption.WRITE, StandardOpenOption.APPEND)) { 686 cc.transferTo(fc); 687 fc.force(true); 688 } 689 return null; 690 }); 691 } 692 } 693 694 public void filter(Instant begin, Instant end, Long maxSize) { 695 synchronized (recorder) { 696 List<RepositoryChunk> result = removeAfter(end, removeBefore(begin, new ArrayList<>(chunks))); 697 if (maxSize != null) { 698 if (begin != null && end == null) { 699 result = reduceFromBeginning(maxSize, result); 700 } else { 701 result = reduceFromEnd(maxSize, result); 702 } 703 } 704 int size = 0; 705 for (RepositoryChunk r : result) { 706 size += r.getSize(); 707 r.use(); 708 } 709 this.size = size; 710 for (RepositoryChunk r : chunks) { 711 r.release(); 712 } 713 chunks.clear(); 714 chunks.addAll(result); 715 } 716 } 717 718 private static List<RepositoryChunk> removeBefore(Instant time, List<RepositoryChunk> input) { 719 if (time == null) { 720 return input; 721 } 722 List<RepositoryChunk> result = new ArrayList<>(input.size()); 723 for (RepositoryChunk r : input) { 724 if (!r.getEndTime().isBefore(time)) { 725 result.add(r); 726 } 727 } 728 return result; 729 } 730 731 private static List<RepositoryChunk> removeAfter(Instant time, List<RepositoryChunk> input) { 732 if (time == null) { 733 return input; 734 } 735 List<RepositoryChunk> result = new ArrayList<>(input.size()); 736 for (RepositoryChunk r : input) { 737 if (!r.getStartTime().isAfter(time)) { 738 result.add(r); 739 } 740 } 741 return result; 742 } 743 744 private static List<RepositoryChunk> reduceFromBeginning(Long maxSize, List<RepositoryChunk> input) { 745 if (maxSize == null || input.isEmpty()) { 746 return input; 747 } 748 List<RepositoryChunk> result = new ArrayList<>(input.size()); 749 long total = 0; 750 for (RepositoryChunk r : input) { 751 total += r.getSize(); 752 if (total > maxSize) { 753 break; 754 } 755 result.add(r); 756 } 757 // always keep at least one chunk 758 if (result.isEmpty()) { 759 result.add(input.get(0)); 760 } 761 return result; 762 } 763 764 private static List<RepositoryChunk> reduceFromEnd(Long maxSize, List<RepositoryChunk> input) { 765 Collections.reverse(input); 766 List<RepositoryChunk> result = reduceFromBeginning(maxSize, input); 767 Collections.reverse(result); 768 return result; 769 } 770 771 public void setDumpOnExitDirectory(SafePath directory) { 772 this.dumpOnExitDirectory = directory; 773 } 774 775 public SafePath getDumpOnExitDirectory() { 776 return this.dumpOnExitDirectory; 777 } 778 }