Клиент: начальный импорт кодовой базы
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import jdk.jfr.consumer.RecordedEvent;
|
||||
import jdk.jfr.consumer.RecordedThread;
|
||||
import jdk.jfr.consumer.RecordingFile;
|
||||
|
||||
/** Streams a private JFR without expanding class loaders or exporting command-line arguments. */
|
||||
public final class JfrSummary {
|
||||
static final class Allocation {
|
||||
String name;
|
||||
Instant firstTime, lastTime;
|
||||
long firstBytes, lastBytes, observations;
|
||||
boolean invalid;
|
||||
void add(Instant time, long bytes, String label) {
|
||||
name = label;
|
||||
observations++;
|
||||
if (bytes < 0) invalid = true;
|
||||
if (firstTime == null || time.isBefore(firstTime)) { firstTime = time; firstBytes = bytes; }
|
||||
if (lastTime == null || time.isAfter(lastTime)) { lastTime = time; lastBytes = bytes; }
|
||||
if (firstTime.equals(lastTime) && firstBytes != lastBytes) invalid = true;
|
||||
}
|
||||
Map<String, Object> result(long id) {
|
||||
var row = new LinkedHashMap<String, Object>();
|
||||
row.put("thread_id", id);
|
||||
row.put("thread", name);
|
||||
row.put("counter_observations", observations);
|
||||
double span = Duration.between(firstTime, lastTime).toNanos() / 1e9;
|
||||
row.put("observed_seconds", span);
|
||||
boolean valid = !invalid && observations > 1 && span > 0 && lastBytes >= firstBytes;
|
||||
row.put("observed_allocated_bytes", valid ? lastBytes - firstBytes : null);
|
||||
row.put("observed_bytes_per_second", valid ? (lastBytes - firstBytes) / span : null);
|
||||
return row;
|
||||
}
|
||||
}
|
||||
|
||||
static final class Samples {
|
||||
String name;
|
||||
long count, missingStacks, truncatedStacks;
|
||||
final Map<String, Long> leaf = new HashMap<>(), inclusive = new HashMap<>();
|
||||
void add(RecordedEvent event, String label) {
|
||||
name = label;
|
||||
count++;
|
||||
var stack = event.getStackTrace();
|
||||
if (stack == null || stack.getFrames().isEmpty()) { missingStacks++; return; }
|
||||
if (stack.isTruncated()) truncatedStacks++;
|
||||
var methods = new java.util.HashSet<String>();
|
||||
for (var frame : stack.getFrames()) {
|
||||
var method = frame.getMethod();
|
||||
methods.add(method.getType().getName() + "." + method.getName() + method.getDescriptor());
|
||||
}
|
||||
for (String method : methods) inclusive.merge(method, 1L, Long::sum);
|
||||
var method = stack.getFrames().getFirst().getMethod();
|
||||
leaf.merge(method.getType().getName() + "." + method.getName() + method.getDescriptor(), 1L, Long::sum);
|
||||
}
|
||||
Map<String, Object> result(long id) {
|
||||
return Map.of("thread_id", id, "thread", name, "samples", count,
|
||||
"missing_stacks", missingStacks, "truncated_stacks", truncatedStacks,
|
||||
"top_leaf_methods", top(leaf), "top_inclusive_methods", top(inclusive));
|
||||
}
|
||||
}
|
||||
|
||||
static long threadId(RecordedThread thread) {
|
||||
return thread.getJavaThreadId() > 0 ? thread.getJavaThreadId() : -thread.getId();
|
||||
}
|
||||
|
||||
static String threadName(RecordedThread thread) {
|
||||
if (thread.getJavaName() != null) return thread.getJavaName();
|
||||
return thread.getOSName() == null ? "unnamed thread" : thread.getOSName();
|
||||
}
|
||||
|
||||
static List<Map<String, Object>> top(Map<String, Long> counts) {
|
||||
return counts.entrySet().stream()
|
||||
.sorted(Map.Entry.<String, Long>comparingByValue().reversed().thenComparing(Map.Entry.comparingByKey()))
|
||||
.limit(15).map(e -> Map.<String, Object>of("method", e.getKey(), "samples", e.getValue())).toList();
|
||||
}
|
||||
|
||||
static Map<String, Object> summarize(Path path) throws Exception {
|
||||
Map<Long, Allocation> allocations = new HashMap<>();
|
||||
Map<Long, Samples> execution = new HashMap<>(), allocationSamples = new HashMap<>();
|
||||
long events = 0, gcCycles = 0, pauseCount = 0, heapPeak = 0;
|
||||
double pauseMs = 0, longestPauseMs = 0;
|
||||
Instant first = null, last = null;
|
||||
try (var recording = new RecordingFile(path)) {
|
||||
while (recording.hasMoreEvents()) {
|
||||
var event = recording.readEvent();
|
||||
events++;
|
||||
Instant time = event.getStartTime();
|
||||
if (first == null || time.isBefore(first)) first = time;
|
||||
Instant end = event.getEndTime();
|
||||
if (last == null || end.isAfter(last)) last = end;
|
||||
switch (event.getEventType().getName()) {
|
||||
case "jdk.ThreadAllocationStatistics" -> {
|
||||
RecordedThread thread = event.getThread("thread");
|
||||
if (thread != null) allocations.computeIfAbsent(threadId(thread), k -> new Allocation())
|
||||
.add(time, event.getLong("allocated"), threadName(thread));
|
||||
}
|
||||
case "jdk.ExecutionSample", "jdk.NativeMethodSample", "jdk.ObjectAllocationSample" -> {
|
||||
boolean allocation = event.getEventType().getName().equals("jdk.ObjectAllocationSample");
|
||||
RecordedThread thread = event.getThread(allocation ? "eventThread" : "sampledThread");
|
||||
if (thread != null) (allocation ? allocationSamples : execution)
|
||||
.computeIfAbsent(threadId(thread), k -> new Samples()).add(event, threadName(thread));
|
||||
}
|
||||
case "jdk.GarbageCollection" -> gcCycles++;
|
||||
case "jdk.GCPhasePause" -> {
|
||||
double ms = event.getDuration().toNanos() / 1e6;
|
||||
pauseMs += ms;
|
||||
longestPauseMs = Math.max(longestPauseMs, ms);
|
||||
pauseCount++;
|
||||
}
|
||||
case "jdk.GCHeapSummary" -> heapPeak = Math.max(heapPeak, event.getLong("heapUsed"));
|
||||
default -> { }
|
||||
}
|
||||
}
|
||||
}
|
||||
List<Map<String, Object>> allocationRows = new ArrayList<>();
|
||||
allocations.forEach((id, allocation) -> allocationRows.add(allocation.result(id)));
|
||||
allocationRows.sort(Comparator.<Map<String, Object>>comparingLong(r ->
|
||||
r.get("observed_allocated_bytes") instanceof Long bytes ? bytes : -1).reversed()
|
||||
.thenComparingLong(r -> (Long) r.get("thread_id")));
|
||||
var result = new LinkedHashMap<String, Object>();
|
||||
result.put("schema", 1);
|
||||
result.put("events_read", events);
|
||||
result.put("event_span_seconds", first == null ? 0 : Duration.between(first, last).toNanos() / 1e9);
|
||||
result.put("allocation_threads", allocationRows);
|
||||
result.put("execution_threads", sampleRows(execution));
|
||||
result.put("allocation_sample_threads", sampleRows(allocationSamples));
|
||||
result.put("gc_cycles", gcCycles);
|
||||
result.put("gc_pause_events", pauseCount);
|
||||
result.put("gc_pause_total_ms", pauseMs);
|
||||
result.put("gc_pause_longest_ms", longestPauseMs);
|
||||
result.put("heap_used_peak_at_gc_bytes", heapPeak == 0 ? null : heapPeak);
|
||||
result.put("notes", List.of(
|
||||
"Allocation bytes are differences between directly observed per-thread counters, with separate observation spans. Thread startup/exit gaps are not covered; missing intervals are null.",
|
||||
"Execution/native-method samples locate sampled stacks, not exact CPU time, GPU work or elapsed-time fractions. Inclusive methods count each sample once even during recursion.",
|
||||
"Allocation stack samples are counts only. Sample weights are deliberately excluded; counts are not allocated bytes or retained RAM.",
|
||||
"GC pause fields use top-level GCPhasePause events, excluding concurrent cycle duration and nested pause phases. GC heap peaks can miss between-GC peaks.",
|
||||
"Profiling changes workload overhead. Keep raw recordings private; they may contain environment details beyond this selected summary."));
|
||||
return result;
|
||||
}
|
||||
|
||||
static List<Map<String, Object>> sampleRows(Map<Long, Samples> samples) {
|
||||
return samples.entrySet().stream().sorted(Comparator.<Map.Entry<Long, Samples>>comparingLong(e -> e.getValue().count)
|
||||
.reversed().thenComparingLong(Map.Entry::getKey)).map(e -> e.getValue().result(e.getKey())).toList();
|
||||
}
|
||||
|
||||
static String json(Object value) {
|
||||
if (value == null) return "null";
|
||||
if (value instanceof String string) {
|
||||
var out = new StringBuilder("\"");
|
||||
for (int i = 0; i < string.length(); i++) {
|
||||
char c = string.charAt(i);
|
||||
if (c == '"' || c == '\\') out.append('\\').append(c);
|
||||
else if (c < 32) out.append(String.format(java.util.Locale.ROOT, "\\u%04x", (int) c));
|
||||
else out.append(c);
|
||||
}
|
||||
return out.append('"').toString();
|
||||
}
|
||||
if (value instanceof Number || value instanceof Boolean) return value.toString();
|
||||
if (value instanceof Map<?, ?> map) return map.entrySet().stream()
|
||||
.map(e -> json(e.getKey().toString()) + ":" + json(e.getValue()))
|
||||
.collect(java.util.stream.Collectors.joining(",", "{", "}"));
|
||||
if (value instanceof List<?> list) return list.stream().map(JfrSummary::json)
|
||||
.collect(java.util.stream.Collectors.joining(",", "[", "]"));
|
||||
throw new IllegalArgumentException("Unsupported JSON value");
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
if (args.length != 1) throw new IllegalArgumentException("Usage: java tools/JfrSummary.java private-recording.jfr");
|
||||
System.out.println(json(summarize(Path.of(args[0]))));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user