87 PlatformRecording(PlatformRecorder recorder, long id) { |
88 PlatformRecording(PlatformRecorder recorder, long id) { |
88 // Typically the access control context is taken |
89 // Typically the access control context is taken |
89 // when you call dump(Path) or setDdestination(Path), |
90 // when you call dump(Path) or setDdestination(Path), |
90 // but if no destination is set and dumponexit=true |
91 // but if no destination is set and dumponexit=true |
91 // the control context of the recording is taken when the |
92 // the control context of the recording is taken when the |
92 // Recording object is constructed. This works well for |
93 // Recording object is constructed. This works well for |
93 // -XX:StartFlightRecording and JFR.dump |
94 // -XX:StartFlightRecording and JFR.dump |
94 this.noDestinationDumpOnExitAccessControlContext = AccessController.getContext(); |
95 this.noDestinationDumpOnExitAccessControlContext = AccessController.getContext(); |
95 this.id = id; |
96 this.id = id; |
96 this.recorder = recorder; |
97 this.recorder = recorder; |
97 this.name = String.valueOf(id); |
98 this.name = String.valueOf(id); |
112 } |
113 } |
113 recorder.start(this); |
114 recorder.start(this); |
114 Logger.log(LogTag.JFR, LogLevel.INFO, () -> { |
115 Logger.log(LogTag.JFR, LogLevel.INFO, () -> { |
115 // Only print non-default values so it easy to see |
116 // Only print non-default values so it easy to see |
116 // which options were added |
117 // which options were added |
117 StringJoiner options = new StringJoiner(", "); |
118 StringJoiner options = new StringJoiner(", "); |
118 if (!toDisk) { |
119 if (!toDisk) { |
119 options.add("disk=false"); |
120 options.add("disk=false"); |
120 } |
121 } |
121 if (maxAge != null) { |
122 if (maxAge != null) { |
122 options.add("maxage=" + Utils.formatTimespan(maxAge, "")); |
123 options.add("maxage=" + Utils.formatTimespan(maxAge, "")); |
123 } |
124 } |
124 if (maxSize != 0) { |
125 if (maxSize != 0) { |
125 options.add("maxsize=" + Utils.formatBytes(maxSize, "")); |
126 options.add("maxsize=" + Utils.formatBytes(maxSize, "")); |
126 } |
127 } |
127 if (dumpOnExit) { |
128 if (dumpOnExit) { |
128 options.add("dumponexit=true"); |
129 options.add("dumponexit=true"); |
129 } |
130 } |
130 if (duration != null) { |
131 if (duration != null) { |
131 options.add("duration=" + Utils.formatTimespan(duration, "")); |
132 options.add("duration=" + Utils.formatTimespan(duration, "")); |
132 } |
133 } |
133 if (destination != null) { |
134 if (destination != null) { |
134 options.add("filename=" + destination.getText()); |
135 options.add("filename=" + destination.getText()); |
135 } |
136 } |
136 String optionText = options.toString(); |
137 String optionText = options.toString(); |
137 if (optionText.length() != 0) { |
138 if (optionText.length() != 0) { |
138 optionText = "{" + optionText + "}"; |
139 optionText = "{" + optionText + "}"; |
139 } |
140 } |
140 return "Started recording \"" + getName() + "\" (" + getId() + ") " + optionText; |
141 return "Started recording \"" + getName() + "\" (" + getId() + ") " + optionText; |
141 }); |
142 }); |
142 newState = getState(); |
143 newState = getState(); |
143 } |
144 } |
144 notifyIfStateChanged(oldState, newState); |
145 notifyIfStateChanged(oldState, newState); |
145 } |
146 } |
146 |
147 |
147 public boolean stop(String reason) { |
148 public boolean stop(String reason) { |
148 return stop(reason, null); |
|
149 } |
|
150 |
|
151 public boolean stop(String reason, WriteableUserPath alternativePath) { |
|
152 return stop(reason, alternativePath, Collections.emptyMap()); |
|
153 } |
|
154 |
|
155 boolean stop(String reason, WriteableUserPath alternativePath, Map<String, String> overlaySettings) { |
|
156 RecordingState oldState; |
149 RecordingState oldState; |
157 RecordingState newState; |
150 RecordingState newState; |
158 synchronized (recorder) { |
151 synchronized (recorder) { |
159 oldState = getState(); |
152 oldState = getState(); |
160 if (stopTask != null) { |
153 if (stopTask != null) { |
161 stopTask.cancel(); |
154 stopTask.cancel(); |
162 stopTask = null; |
155 stopTask = null; |
163 } |
156 } |
164 recorder.stop(this, alternativePath); |
157 recorder.stop(this); |
165 String endText = reason == null ? "" : ". Reason \"" + reason + "\"."; |
158 String endText = reason == null ? "" : ". Reason \"" + reason + "\"."; |
166 Logger.log(LogTag.JFR, LogLevel.INFO, "Stopped recording \"" + getName() + "\" (" + getId()+ ")" + endText); |
159 Logger.log(LogTag.JFR, LogLevel.INFO, "Stopped recording \"" + getName() + "\" (" + getId() + ")" + endText); |
167 this.stopTime = Instant.now(); |
160 this.stopTime = Instant.now(); |
168 newState = getState(); |
161 newState = getState(); |
169 } |
162 } |
170 WriteableUserPath dest = getDestination(); |
163 WriteableUserPath dest = getDestination(); |
171 if (dest == null && alternativePath != null) { |
164 |
172 dest = alternativePath; |
|
173 } |
|
174 if (dest != null) { |
165 if (dest != null) { |
175 try { |
166 try { |
176 copyTo(dest, reason, overlaySettings); |
167 dumpStopped(dest); |
177 Logger.log(LogTag.JFR, LogLevel.INFO, "Wrote recording \"" + getName() + "\" (" + getId()+ ") to " + dest.getText()); |
168 Logger.log(LogTag.JFR, LogLevel.INFO, "Wrote recording \"" + getName() + "\" (" + getId() + ") to " + dest.getText()); |
178 notifyIfStateChanged(newState, oldState); |
169 notifyIfStateChanged(newState, oldState); |
179 close(); // remove if copied out |
170 close(); // remove if copied out |
180 } catch (IOException e) { |
171 } catch(IOException e) { |
181 // throw e; // BUG8925030 |
172 // throw e; // BUG8925030 |
182 } |
173 } |
183 } else { |
174 } else { |
184 notifyIfStateChanged(newState, oldState); |
175 notifyIfStateChanged(newState, oldState); |
185 } |
176 } |
294 for (RepositoryChunk c : chunks) { |
284 for (RepositoryChunk c : chunks) { |
295 removed(c); |
285 removed(c); |
296 } |
286 } |
297 chunks.clear(); |
287 chunks.clear(); |
298 setState(RecordingState.CLOSED); |
288 setState(RecordingState.CLOSED); |
299 Logger.log(LogTag.JFR, LogLevel.INFO, "Closed recording \"" + getName() + "\" (" + getId()+ ")"); |
289 Logger.log(LogTag.JFR, LogLevel.INFO, "Closed recording \"" + getName() + "\" (" + getId() + ")"); |
300 } |
290 } |
301 newState = getState(); |
291 newState = getState(); |
302 } |
292 } |
303 notifyIfStateChanged(newState, oldState); |
293 notifyIfStateChanged(newState, oldState); |
304 } |
294 } |
305 |
295 |
306 public void copyTo(WriteableUserPath path, String reason, Map<String, String> dumpSettings) throws IOException { |
296 // To be used internally when doing dumps. |
307 synchronized (recorder) { |
297 // Caller must have recorder lock and close recording before releasing lock |
308 RecordingState state = getState(); |
298 public PlatformRecording newSnapshotClone(String reason, Boolean pathToGcRoots) throws IOException { |
309 if (state == RecordingState.CLOSED) { |
299 if(!Thread.holdsLock(recorder)) { |
310 throw new IOException("Recording \"" + name + "\" (id=" + id + ") has been closed, no contents to write"); |
300 throw new InternalError("Caller must have recorder lock"); |
311 } |
301 } |
312 if (state == RecordingState.DELAYED || state == RecordingState.NEW) { |
302 RecordingState state = getState(); |
313 throw new IOException("Recording \"" + name + "\" (id=" + id + ") has not started, no contents to write"); |
303 if (state == RecordingState.CLOSED) { |
314 } |
304 throw new IOException("Recording \"" + name + "\" (id=" + id + ") has been closed, no contents to write"); |
315 if (state == RecordingState.STOPPED) { |
305 } |
316 // have all we need, just write it |
306 if (state == RecordingState.DELAYED || state == RecordingState.NEW) { |
317 dumpToFile(path, reason, getId()); |
307 throw new IOException("Recording \"" + name + "\" (id=" + id + ") has not started, no contents to write"); |
318 return; |
308 } |
319 } |
309 if (state == RecordingState.STOPPED) { |
320 |
310 PlatformRecording clone = recorder.newTemporaryRecording(); |
321 // Recording is RUNNING, create a clone |
311 for (RepositoryChunk r : chunks) { |
322 try(PlatformRecording clone = recorder.newRecording(Collections.emptyMap(), 0)) { |
312 clone.add(r); |
323 clone.setShouldWriteActiveRecordingEvent(false); |
313 } |
324 clone.setName(getName()); |
314 return clone; |
325 clone.setDestination(path); |
315 } |
326 clone.setToDisk(true); |
316 |
327 // We purposely don't clone settings, since |
317 // Recording is RUNNING, create a clone |
328 // a union a == a |
318 PlatformRecording clone = recorder.newTemporaryRecording(); |
329 if (!isToDisk()) { |
319 clone.setShouldWriteActiveRecordingEvent(false); |
330 // force memory contents to disk |
320 clone.setName(getName()); |
331 clone.start(); |
321 clone.setDestination(this.destination); |
332 } else { |
322 clone.setToDisk(true); |
333 // using existing chunks on disk |
323 // We purposely don't clone settings here, since |
334 for (RepositoryChunk c : chunks) { |
324 // a union a == a |
335 clone.add(c); |
325 if (!isToDisk()) { |
336 } |
326 // force memory contents to disk |
337 clone.setState(RecordingState.RUNNING); |
327 clone.start(); |
338 clone.setStartTime(getStartTime()); |
328 } else { |
339 } |
329 // using existing chunks on disk |
340 if (dumpSettings.isEmpty()) { |
330 for (RepositoryChunk c : chunks) { |
341 clone.setSettings(getSettings()); |
331 clone.add(c); |
342 clone.stop(reason); // dumps to destination path here |
332 } |
343 } else { |
333 clone.setState(RecordingState.RUNNING); |
344 // Risk of violating lock order here, since |
334 clone.setStartTime(getStartTime()); |
345 // clone.stop() will take recorder lock inside |
335 } |
346 // metadata lock, but OK if we already |
336 if (pathToGcRoots == null) { |
347 // have recorder lock when we entered metadata lock |
337 clone.setSettings(getSettings()); // needed for old object sample |
348 Thread.holdsLock(recorder); |
338 clone.stop(reason); // dumps to destination path here |
349 synchronized(MetadataRepository.getInstance()) { |
339 } else { |
350 Thread.holdsLock(recorder); |
340 // Risk of violating lock order here, since |
351 Map<String, String> oldSettings = getSettings(); |
341 // clone.stop() will take recorder lock inside |
352 Map<String, String> newSettings = new HashMap<>(oldSettings); |
342 // metadata lock, but OK if we already |
353 // replace with dump settings |
343 // have recorder lock when we entered metadata lock |
354 newSettings.putAll(dumpSettings); |
344 synchronized (MetadataRepository.getInstance()) { |
355 clone.setSettings(newSettings); |
345 clone.setSettings(OldObjectSample.createSettingsForSnapshot(this, pathToGcRoots)); |
356 clone.stop(reason); |
346 clone.stop(reason); |
357 } |
347 } |
358 } |
348 } |
359 } |
349 return clone; |
360 return; |
|
361 } |
|
362 } |
|
363 |
|
364 private void dumpToFile(WriteableUserPath userPath, String reason, long id) throws IOException { |
|
365 userPath.doPriviligedIO(() -> { |
|
366 try (ChunksChannel cc = new ChunksChannel(chunks); FileChannel fc = FileChannel.open(userPath.getReal(), StandardOpenOption.WRITE, StandardOpenOption.APPEND)) { |
|
367 cc.transferTo(fc); |
|
368 fc.force(true); |
|
369 } |
|
370 return null; |
|
371 }); |
|
372 } |
350 } |
373 |
351 |
374 public boolean isToDisk() { |
352 public boolean isToDisk() { |
375 synchronized (recorder) { |
353 synchronized (recorder) { |
376 return toDisk; |
354 return toDisk; |
477 private void setSettings(Map<String, String> settings, boolean update) { |
455 private void setSettings(Map<String, String> settings, boolean update) { |
478 if (LogTag.JFR_SETTING.shouldLog(LogLevel.INFO.level) && update) { |
456 if (LogTag.JFR_SETTING.shouldLog(LogLevel.INFO.level) && update) { |
479 TreeMap<String, String> ordered = new TreeMap<>(settings); |
457 TreeMap<String, String> ordered = new TreeMap<>(settings); |
480 Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, "New settings for recording \"" + getName() + "\" (" + getId() + ")"); |
458 Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, "New settings for recording \"" + getName() + "\" (" + getId() + ")"); |
481 for (Map.Entry<String, String> entry : ordered.entrySet()) { |
459 for (Map.Entry<String, String> entry : ordered.entrySet()) { |
482 String text = entry.getKey() + "=\"" + entry.getValue() + "\""; |
460 String text = entry.getKey() + "=\"" + entry.getValue() + "\""; |
483 Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, text); |
461 Logger.log(LogTag.JFR_SETTING, LogLevel.INFO, text); |
484 } |
462 } |
485 } |
463 } |
486 synchronized (recorder) { |
464 synchronized (recorder) { |
487 this.settings = new LinkedHashMap<>(settings); |
465 this.settings = new LinkedHashMap<>(settings); |
488 if (getState() == RecordingState.RUNNING && update) { |
466 if (getState() == RecordingState.RUNNING && update) { |
489 recorder.updateSettings(); |
467 recorder.updateSettings(); |
490 } |
468 } |
491 } |
469 } |
492 } |
470 } |
493 |
|
494 |
471 |
495 private void notifyIfStateChanged(RecordingState newState, RecordingState oldState) { |
472 private void notifyIfStateChanged(RecordingState newState, RecordingState oldState) { |
496 if (oldState == newState) { |
473 if (oldState == newState) { |
497 return; |
474 return; |
498 } |
475 } |
676 this.stopTask = stopTask; |
653 this.stopTask = stopTask; |
677 } |
654 } |
678 } |
655 } |
679 |
656 |
680 void clearDestination() { |
657 void clearDestination() { |
681 destination = null; |
658 destination = null; |
682 } |
659 } |
683 |
660 |
684 public AccessControlContext getNoDestinationDumpOnExitAccessControlContext() { |
661 public AccessControlContext getNoDestinationDumpOnExitAccessControlContext() { |
685 return noDestinationDumpOnExitAccessControlContext; |
662 return noDestinationDumpOnExitAccessControlContext; |
686 } |
663 } |
687 |
664 |
688 void setShouldWriteActiveRecordingEvent(boolean shouldWrite) { |
665 void setShouldWriteActiveRecordingEvent(boolean shouldWrite) { |
689 this.shuoldWriteActiveRecordingEvent = shouldWrite; |
666 this.shuoldWriteActiveRecordingEvent = shouldWrite; |
690 } |
667 } |
691 |
668 |
692 boolean shouldWriteMetadataEvent() { |
669 boolean shouldWriteMetadataEvent() { |
693 return shuoldWriteActiveRecordingEvent; |
670 return shuoldWriteActiveRecordingEvent; |
694 } |
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 } |
695 } |
778 } |