Refine performance detail attribution

This commit is contained in:
MoePus committed 2026-09-03 12:18:49 +09:00
1 parent 85820e1d00
commit ddb5a94c4d
26 files changed
+590 -283

No files matched your search

@@ -199,7 +199,9 @@ final class PerfReportRows {
}
if (entry.type() == TickType.TASK) {
if (entry.eventLoop().isEmpty()) {
return PerfDisplayText.translated("screen.serverwarashi.profiler.tickable.unknown");
return isUnknownFallback(entry)
? PerfDisplayText.translated("screen.serverwarashi.profiler.tickable.unknown")
: name;
}
return name + " @ " + entry.eventLoop();
}
@@ -286,7 +286,7 @@ public final class PerfReportScreen extends Screen {
PerfReportRow.LabelTooltip tooltip = row.capabilities().labelTooltip().orElse(null);
PerfReportRow.LabelAction labelAction = row.capabilities().labelAction().orElse(null);
boolean tooltipHovered = (tooltip != null || labelAction != null)
&& PerfReportTableRenderer.labelBounds(font, context).contains(mouseX, mouseY);
&& PerfReportTableRenderer.labelCellBounds(context).contains(mouseX, mouseY);
boolean hovered = tooltipHovered || rowHits.stream().anyMatch(hit -> hit.contains(mouseX, mouseY));
if (hovered || index % 2 == 0) {
graphics.fill(geometry.contentX(), y, geometry.contentRight(), y + ROW_HEIGHT,
@@ -328,7 +328,7 @@ public final class PerfReportScreen extends Screen {
if (row.capabilities().labelAction().isPresent()) {
var context = new PerfReportTableRenderer.RowContext(tableContext(geometry), row, y);
result.add(new Hit(Action.UNKNOWN_DETAILS, null,
PerfReportTableRenderer.labelBounds(font, context)));
PerfReportTableRenderer.labelCellBounds(context)));
}
return result;
}
@@ -91,14 +91,10 @@ final class PerfReportTableRenderer {
return new PerfReportBounds(x, context.y, font.width(coordinate), ROW_HEIGHT);
}
static PerfReportBounds labelBounds(Font font, RowContext context) {
static PerfReportBounds labelCellBounds(RowContext context) {
int labelWidth = labelWidth(context.table);
int depth = context.row.presentation().depth();
int x = context.table.geometry.contentX() + depth * INDENT;
int width = Math.max(1, labelWidth - depth * INDENT);
String label = fit(font, expansionMarker(context.row)
+ context.row.content().labelPrefix(), width);
return new PerfReportBounds(x, context.y, font.width(label), ROW_HEIGHT);
return new PerfReportBounds(
context.table.geometry.contentX(), context.y, labelWidth, ROW_HEIGHT);
}
private static void renderLabel(GuiGraphics graphics, Font font, RowContext context, int labelWidth) {
@@ -3,8 +3,6 @@ package com.moepus.serverwarashi.client.screen;
import com.moepus.serverwarashi.common.modules.performance.report.PerfReportData;
import net.minecraft.network.chat.Component;
import java.util.Locale;
final class PerfTimeText {
private static final long MINUTE_MILLIS = 60_000L;
private static final long HOUR_MILLIS = 60L * MINUTE_MILLIS;
@@ -18,15 +16,8 @@ final class PerfTimeText {
? Component.translatable("screen.serverwarashi.profiler.status.running")
: relative(report.generatedAtEpochMillis(), nowMillis,
"screen.serverwarashi.profiler.status.no_sample");
String samples = String.format(Locale.ROOT, "%,d", report.totalSamples());
String confidence = report.sampling().confidence().name().toLowerCase(Locale.ROOT);
if (!report.sampling().degradationReason().isEmpty()) {
return Component.translatable("screen.serverwarashi.profiler.status.session_degraded",
time, samples, report.sampling().backend(), confidence,
report.sampling().degradationReason());
}
return Component.translatable("screen.serverwarashi.profiler.status.session",
time, samples, report.sampling().backend(), confidence);
time, String.format(java.util.Locale.ROOT, "%,d", report.totalSamples()));
}
static Component player(boolean online, long lastLoginMillis, long nowMillis) {
@@ -162,8 +162,7 @@ public final class UnknownStackCopyController {
private void copy(UnknownStackDetailsPayload payload) {
dependencies.output.copy(format(payload, unknownSummary));
int copied = payload.average().size() + payload.burst().size();
dependencies.output.notifyCopied(copied);
dependencies.output.notifyCopied(payload.average().size());
tooltipState = TooltipState.COPIED;
if (!profiling) {
recentStopped = payload;
@@ -200,10 +199,7 @@ public final class UnknownStackCopyController {
appendCoverage(text, payload, summary);
text.append("Reservoir samples: ").append(payload.retainedCount()).append('\n');
double estimateScale = summary == null ? 1.0D : summary.estimateScale();
appendSection(text, new Section("Average-time Top 3", payload.average(), true),
payload.retainedCount(), estimateScale);
appendSection(text, new Section("Burst-time Top 3", payload.burst(), false),
payload.retainedCount(), estimateScale);
appendSection(text, payload.average(), payload.retainedCount(), estimateScale);
return text.toString();
}
@@ -223,29 +219,26 @@ public final class UnknownStackCopyController {
}
private static void appendSection(StringBuilder text,
Section section,
int retainedCount,
double estimateScale) {
text.append('\n').append(section.heading).append('\n');
if (section.clusters.isEmpty()) {
List<UnknownStackDetailsPayload.Cluster> clusters,
int retainedCount,
double estimateScale) {
text.append("\nAverage-time Top 3\n");
if (clusters.isEmpty()) {
text.append("(none)\n");
return;
}
for (int index = 0; index < section.clusters.size(); index++) {
UnknownStackDetailsPayload.Cluster cluster = section.clusters.get(index);
for (int index = 0; index < clusters.size(); index++) {
UnknownStackDetailsPayload.Cluster cluster = clusters.get(index);
double share = retainedCount == 0 ? 0.0D
: cluster.sampleCount() * 100.0D / retainedCount;
text.append(index + 1).append(". ")
.append(String.format(Locale.ROOT, "%.2f%%", share)).append(" | ")
.append(section.average
? String.format(Locale.ROOT, "estimated %.3f MSPT",
cluster.estimatedAverageMillis() * estimateScale)
: String.format(Locale.ROOT, "estimated %.3f ms peak",
cluster.estimatedBurstMillis() * estimateScale))
.append(String.format(Locale.ROOT, "estimated %.3f MSPT",
cluster.estimatedAverageMillis() * estimateScale))
.append("\n Anchor: ").append(cluster.anchor())
.append("\n Samples: ").append(cluster.sampleCount())
.append("; representative path: ").append(cluster.representativeCount())
.append("; retained tick peak: ").append(cluster.peakSampleCount()).append('\n');
.append("; representative path: ")
.append(cluster.representativeCount()).append('\n');
for (String frame : cluster.frames()) {
text.append("\tat ").append(frame).append('\n');
}
@@ -281,11 +274,6 @@ public final class UnknownStackCopyController {
}
}
private record Section(String heading,
List<UnknownStackDetailsPayload.Cluster> clusters,
boolean average) {
}
interface Transport {
boolean available();
@@ -9,7 +9,6 @@ import java.util.Map;
final class DetailSampleAllocator {
private static final int MIN_EVIDENCE_SAMPLES = 2;
private static final String GENERIC_HANDLER = "Handler";
private static final String GENERIC_UNKNOWN = "Unknown";
private final Map<PerfWorkTracker.Snapshot, Integer> handlers = new LinkedHashMap<>();
@@ -30,35 +29,20 @@ final class DetailSampleAllocator {
if (detail.kind() == PerfStackAttributor.Kind.ENTITY_DISPATCH
|| detail.kind() == PerfStackAttributor.Kind.BLOCK_ENTITY_DISPATCH) {
dispatches.merge(detail.snapshot(), 1, Integer::sum);
} else if (detail.kind() != PerfStackAttributor.Kind.IDLE) {
} else if (detail.kind() == PerfStackAttributor.Kind.UNKNOWN) {
unknownStacks.add(sample.stack());
}
}
void apply(TickSamples tick, UnknownStackReservoir unknownReservoir) {
splitHandlers(tick);
splitUnknown(tick);
if (tick.unknownSamples > 0L) {
recordUnknownEvidence(unknownReservoir, tick.tickId);
}
}
private void splitHandlers(TickSamples tick) {
int evidence = evidenceCount(handlers);
if (evidence < MIN_EVIDENCE_SAMPLES) {
return;
}
int coarseSamples = tick.removeGeneric(TickType.HANDLER, GENERIC_HANDLER);
if (coarseSamples == 0) {
return;
}
List<Bucket> buckets = buckets(handlers);
allocate(coarseSamples, buckets);
for (Bucket bucket : buckets) {
tick.addDetail(bucket.snapshot, bucket.allocated,
PerfReportData.AttributionSource.ASYNC_STACK,
PerfReportData.Confidence.MEDIUM);
}
Map<PerfWorkTracker.Snapshot, Integer> handlerEvidence() {
return handlers;
}
private void splitUnknown(TickSamples tick) {
@@ -0,0 +1,162 @@
package com.moepus.serverwarashi.common.modules.performance.sampling;
import com.moepus.serverwarashi.common.modules.performance.report.PerfReportData;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
final class HandlerDetailAccumulator {
private static final int MIN_EVIDENCE_SAMPLES = 2;
private static final int MAX_DETAIL_KEYS = 4_096;
private static final int INITIAL_TICK_CAPACITY = 256;
private static final String GENERIC_HANDLER = "Handler";
private final Map<PerfWorkTracker.Snapshot, Integer> evidence = new LinkedHashMap<>();
private long[] ticks = new long[INITIAL_TICK_CAPACITY];
private int[] samples = new int[INITIAL_TICK_CAPACITY];
private TickSamples.TickableKey genericKey;
private int tickSize;
private int genericEvidence;
void accept(TickSamples tick, long tickIndex) {
removeGenericHandler(tick, tickIndex);
tick.details.handlerEvidence().forEach(this::recordEvidence);
}
List<PerfReportData.TickableEntry> build(SampleMetricContext context) {
if (tickSize == 0 || genericKey == null) {
return List.of();
}
int totalEvidence = genericEvidence + evidenceCount();
if (totalEvidence < MIN_EVIDENCE_SAMPLES) {
return List.of(genericEntry(context));
}
List<Bucket> buckets = detailBuckets();
Map<TickSamples.TickableKey, PerfSampleAggregator.TickableAggregate> aggregates =
new LinkedHashMap<>();
for (int index = 0; index < tickSize; index++) {
allocate(samples[index], totalEvidence, buckets);
recordAllocated(aggregates, buckets, ticks[index]);
}
return aggregates.entrySet().stream()
.map(entry -> entry.getValue().toReport(entry.getKey(), context))
.toList();
}
private void removeGenericHandler(TickSamples tick, long tickIndex) {
var iterator = tick.tickables.entrySet().iterator();
while (iterator.hasNext()) {
var entry = iterator.next();
TickSamples.TickableKey key = entry.getKey();
if (!isGenericHandler(key)) {
continue;
}
iterator.remove();
genericKey = genericKey == null ? key : genericKey;
appendTick(tickIndex, entry.getValue().count);
return;
}
}
private static boolean isGenericHandler(TickSamples.TickableKey key) {
return key.type() == TickType.HANDLER
&& GENERIC_HANDLER.equals(key.displayName())
&& key.attributionSource() == PerfReportData.AttributionSource.TASK_CONTEXT;
}
private void appendTick(long tick, int count) {
if (count <= 0) {
return;
}
ensureTickCapacity();
ticks[tickSize] = tick;
samples[tickSize] = count;
tickSize++;
}
private void ensureTickCapacity() {
if (tickSize < ticks.length) {
return;
}
int capacity = ticks.length << 1;
ticks = Arrays.copyOf(ticks, capacity);
samples = Arrays.copyOf(samples, capacity);
}
private void recordEvidence(PerfWorkTracker.Snapshot snapshot, int count) {
Integer previous = evidence.get(snapshot);
if (previous != null) {
evidence.put(snapshot, previous + count);
} else if (evidence.size() < MAX_DETAIL_KEYS) {
evidence.put(snapshot, count);
} else {
genericEvidence += count;
}
}
private int evidenceCount() {
return evidence.values().stream().mapToInt(Integer::intValue).sum();
}
private PerfReportData.TickableEntry genericEntry(SampleMetricContext context) {
PerfSampleAggregator.TickableAggregate aggregate =
new PerfSampleAggregator.TickableAggregate();
for (int index = 0; index < tickSize; index++) {
aggregate.recordSamples(samples[index], ticks[index]);
}
return aggregate.toReport(genericKey, context);
}
private List<Bucket> detailBuckets() {
List<Bucket> result = new ArrayList<>(evidence.size() + 1);
evidence.forEach((snapshot, count) -> result.add(new Bucket(
TickSamples.TickableKey.from(snapshot, SampleMetricMode.STATISTICAL,
PerfReportData.AttributionSource.ASYNC_STACK,
PerfReportData.Confidence.MEDIUM), count)));
if (genericEvidence > 0) {
result.add(new Bucket(genericKey, genericEvidence));
}
return result;
}
private static void allocate(int samples, int evidence, List<Bucket> buckets) {
int allocated = 0;
for (Bucket bucket : buckets) {
long weighted = (long) samples * bucket.evidence;
bucket.allocated = (int) (weighted / evidence);
bucket.remainder = weighted % evidence;
allocated += bucket.allocated;
}
buckets.stream().sorted((left, right) -> Long.compare(right.remainder, left.remainder))
.limit(samples - allocated).forEach(bucket -> bucket.allocated++);
}
private static void recordAllocated(
Map<TickSamples.TickableKey, PerfSampleAggregator.TickableAggregate> aggregates,
List<Bucket> buckets,
long tick) {
for (Bucket bucket : buckets) {
if (bucket.allocated <= 0) {
continue;
}
aggregates.computeIfAbsent(bucket.key,
ignored -> new PerfSampleAggregator.TickableAggregate())
.recordSamples(bucket.allocated, tick);
}
}
private static final class Bucket {
final TickSamples.TickableKey key;
final int evidence;
int allocated;
long remainder;
Bucket(TickSamples.TickableKey key, int evidence) {
this.key = key;
this.evidence = evidence;
}
}
}
@@ -41,6 +41,7 @@ final class PerfSampleAggregator {
.thenComparingInt(entry -> entry.attributionSource().ordinal());
private final long reportId;
private final PerfOwnerAggregator ownerAggregator = new PerfOwnerAggregator();
private final HandlerDetailAccumulator handlerDetails = new HandlerDetailAccumulator();
private final Map<TickSamples.TickableKey, TickableAggregate> tickables = new HashMap<>();
private final Map<TickSamples.ChunkKey, SampleStatistics> chunks = new HashMap<>();
private final ArrayDeque<GcTick> recentGcTicks = new ArrayDeque<>();
@@ -68,6 +69,7 @@ final class PerfSampleAggregator {
tickCount++;
totalTickNanos += tick.durationNanos;
recordGc(tick);
handlerDetails.accept(tick, tickCount);
if (tick.totalSamples == 0) {
ownerAggregator.updateMetadata(tick.attribution);
} else {
@@ -92,8 +94,10 @@ final class PerfSampleAggregator {
chunkEntries(objectBuild.chunkCounts(), nanosPerSample), CHUNK_VIEW_LIMIT_PER_METRIC,
PerfReportData.ChunkEntry::metric, STABLE_CHUNK_ORDER);
var sampledTickables = objectBuild.tickables();
List<PerfReportData.TickableEntry> candidates = new ArrayList<>(sampledTickables.entries());
candidates.addAll(handlerDetails.build(metricContext));
var limitedTickables = PerfReportLimits.topMetricUnion(
sampledTickables.entries(), TICKABLE_VIEW_LIMIT_PER_METRIC,
candidates, TICKABLE_VIEW_LIMIT_PER_METRIC,
PerfReportData.TickableEntry::metric, STABLE_TICKABLE_ORDER);
PerfOwnerAggregator.Build ownerBuild = ownerAggregator.build(tickCount, nanosPerSample);
GcMetric gc = gcMetric();
@@ -271,6 +275,11 @@ final class PerfSampleAggregator {
stats.record(count.count, tick);
}
void recordSamples(int samples, long tick) {
lastSeenTick = tick;
stats.record(samples, tick);
}
double totalWeight() {
return stats.totalWeight();
}
@@ -66,7 +66,7 @@ final class PerfSamplerReportSink {
boolean stopped) {
var parameters = new UnknownStackReservoir.SnapshotParameters(
reportId, completedTickId, intervalMicros, completedTickId,
0L, 0L, 0L, stopped);
0L, stopped);
return unknownStacks.snapshot(parameters);
}
@@ -10,8 +10,12 @@ final class PerfStackAttributor {
private static final String MINECRAFT_SERVER_CLASS = "net.minecraft.server.MinecraftServer";
private static final String SERVER_LEVEL_CLASS = "net.minecraft.server.level.ServerLevel";
private static final String ENTITY_CLASS = "net.minecraft.world.entity.Entity";
private static final String ENTITY_TICK_LIST_CLASS =
"net.minecraft.world.level.entity.EntityTickList";
private static final String BOUND_BLOCK_ENTITY_CLASS =
"net.minecraft.world.level.chunk.LevelChunk$BoundTickingBlockEntity";
private static final String REBINDABLE_BLOCK_ENTITY_CLASS =
"net.minecraft.world.level.chunk.LevelChunk$RebindableTickingBlockEntityWrapper";
private static final String LEVEL_CHUNK_CLASS = "net.minecraft.world.level.chunk.LevelChunk";
private static final String BLOCK_ENTITY_CLASS =
"net.minecraft.world.level.block.entity.BlockEntity";
@@ -75,13 +79,15 @@ final class PerfStackAttributor {
private static Classification findDispatch(StackTraceElement[] stack) {
for (int index = 0; index < stack.length; index++) {
StackTraceElement frame = stack[index];
if (SERVER_LEVEL_CLASS.equals(frame.getClassName())
&& "tickNonPassenger".equals(frame.getMethodName())) {
if (isEntityTickWrapper(frame)) {
return onlyWrapperInfrastructureAbove(stack, index, Wrapper.ENTITY)
? ENTITY_DISPATCH : TRACKED;
}
if (BOUND_BLOCK_ENTITY_CLASS.equals(frame.getClassName())
&& "tick".equals(frame.getMethodName())) {
if (ENTITY_TICK_LIST_CLASS.equals(frame.getClassName())
&& "forEach".equals(frame.getMethodName())) {
return ENTITY_DISPATCH;
}
if (isBlockEntityWrapper(frame)) {
return onlyWrapperInfrastructureAbove(stack, index, Wrapper.BLOCK_ENTITY)
? BLOCK_ENTITY_DISPATCH : TRACKED;
}
@@ -89,6 +95,20 @@ final class PerfStackAttributor {
return null;
}
private static boolean isBlockEntityWrapper(StackTraceElement frame) {
String className = frame.getClassName();
return BOUND_BLOCK_ENTITY_CLASS.equals(className)
|| REBINDABLE_BLOCK_ENTITY_CLASS.equals(className);
}
private static boolean isEntityTickWrapper(StackTraceElement frame) {
if (!SERVER_LEVEL_CLASS.equals(frame.getClassName())) {
return false;
}
return "tickNonPassenger".equals(frame.getMethodName())
|| "tickPassenger".equals(frame.getMethodName());
}
private static boolean onlyWrapperInfrastructureAbove(
StackTraceElement[] stack, int wrapperIndex, Wrapper wrapper) {
for (int index = 0; index < wrapperIndex; index++) {
@@ -107,6 +127,7 @@ final class PerfStackAttributor {
}
if (wrapper == Wrapper.ENTITY) {
return SERVER_LEVEL_CLASS.equals(className)
|| ENTITY_TICK_LIST_CLASS.equals(className)
|| ENTITY_CLASS.equals(className)
&& ENTITY_WRAPPER_METHODS.contains(frame.getMethodName());
}
@@ -15,10 +15,6 @@ public final class UnknownStackClusterer {
Comparator.comparingInt(AnchorStats::sampleCount).reversed()
.thenComparingInt(AnchorStats::representativeDepth)
.thenComparing(AnchorStats::anchor);
private static final Comparator<AnchorStats> BURST_ORDER =
Comparator.comparingDouble(AnchorStats::estimatedBurstSamples).reversed()
.thenComparing(AVERAGE_ORDER);
private UnknownStackClusterer() {
}
@@ -26,18 +22,16 @@ public final class UnknownStackClusterer {
if (snapshot.totalObserved() < MIN_RELIABLE_OBSERVATIONS
|| snapshot.retainedCount() < MIN_RELIABLE_OBSERVATIONS) {
return new Result(snapshot.reportId(), snapshot.revision(), snapshot.totalObserved(),
snapshot.retainedCount(), List.of(), List.of());
snapshot.retainedCount(), List.of());
}
Map<String, AnchorStats> groups = collect(snapshot);
Map<Long, TickRetention> retainedTicks = retainedTicks(snapshot);
List<AnchorStats> values = new ArrayList<>(groups.values());
for (AnchorStats group : values) {
group.prepare(snapshot, retainedTicks);
group.prepare(snapshot);
}
List<Cluster> average = top(values, AVERAGE_ORDER);
List<Cluster> burst = top(values, BURST_ORDER);
return new Result(snapshot.reportId(), snapshot.revision(), snapshot.totalObserved(),
snapshot.retainedCount(), average, burst);
snapshot.retainedCount(), average);
}
private static Map<String, AnchorStats> collect(UnknownStackSnapshot snapshot) {
@@ -53,25 +47,11 @@ public final class UnknownStackClusterer {
group = new AnchorStats(anchor);
groups.put(anchor, group);
}
group.accept(stack, snapshot.tickIdAt(index));
group.accept(stack);
}
return groups;
}
private static Map<Long, TickRetention> retainedTicks(UnknownStackSnapshot snapshot) {
Map<Long, TickRetention> ticks = new HashMap<>();
for (int index = 0; index < snapshot.retainedCount(); index++) {
long tickId = snapshot.tickIdAt(index);
TickRetention retention = ticks.get(tickId);
if (retention == null) {
retention = new TickRetention();
ticks.put(tickId, retention);
}
retention.retain(snapshot.tickTotalAt(index));
}
return ticks;
}
private static List<Cluster> top(List<AnchorStats> groups,
Comparator<AnchorStats> order) {
return groups.stream().sorted(order).limit(TOP_LIMIT)
@@ -139,11 +119,18 @@ public final class UnknownStackClusterer {
private static List<String> rawFrames(StackTraceElement[] stack) {
List<String> frames = new ArrayList<>(stack.length);
for (StackTraceElement frame : stack) {
frames.add(limit(frame.toString(), MAX_FRAME_TEXT_LENGTH));
frames.add(limit(displayFrame(frame), MAX_FRAME_TEXT_LENGTH));
}
return List.copyOf(frames);
}
private static String displayFrame(StackTraceElement frame) {
if (frame.getFileName() != null || frame.isNativeMethod()) {
return frame.toString();
}
return frame.getClassName() + "." + frame.getMethodName();
}
private static String rawKey(StackTraceElement[] stack) {
StringBuilder result = new StringBuilder(stack.length * 64);
for (StackTraceElement frame : stack) {
@@ -160,20 +147,16 @@ public final class UnknownStackClusterer {
long revision,
long totalObserved,
int retainedCount,
List<Cluster> average,
List<Cluster> burst) {
List<Cluster> average) {
public Result {
average = List.copyOf(average);
burst = List.copyOf(burst);
}
}
public record Cluster(String anchor,
int sampleCount,
int representativeCount,
int peakSampleCount,
double estimatedAverageMillis,
double estimatedBurstMillis,
List<String> frames) {
public Cluster {
frames = List.copyOf(frames);
@@ -183,10 +166,7 @@ public final class UnknownStackClusterer {
private static final class AnchorStats {
private final String anchor;
private final Map<String, PathStats> paths = new HashMap<>();
private final Map<Long, Integer> tickCounts = new HashMap<>();
private int sampleCount;
private int peakSampleCount;
private double estimatedBurstSamples;
private PathStats representative;
private Cluster cluster;
@@ -194,11 +174,8 @@ public final class UnknownStackClusterer {
this.anchor = anchor;
}
void accept(StackTraceElement[] stack, long tickId) {
void accept(StackTraceElement[] stack) {
sampleCount++;
int tickCount = tickCounts.getOrDefault(tickId, 0) + 1;
tickCounts.put(tickId, tickCount);
peakSampleCount = Math.max(peakSampleCount, tickCount);
String path = normalizedPath(stack);
PathStats stats = paths.get(path);
if (stats == null) {
@@ -208,18 +185,15 @@ public final class UnknownStackClusterer {
}
}
void prepare(UnknownStackSnapshot snapshot, Map<Long, TickRetention> retainedTicks) {
void prepare(UnknownStackSnapshot snapshot) {
representative = selectRepresentative();
estimatedBurstSamples = estimateBurstSamples(retainedTicks);
double scale = snapshot.retainedCount() == 0 ? 0.0D
: snapshot.totalPrimarySamples() / (double) snapshot.retainedCount();
double intervalMillis = snapshot.intervalMicros() / 1_000.0D;
double average = snapshot.tickCount() == 0L ? 0.0D
: sampleCount * scale * intervalMillis / snapshot.tickCount();
double burst = estimatedBurstSamples * intervalMillis;
cluster = new Cluster(limit(anchor, MAX_FRAME_TEXT_LENGTH), sampleCount,
representative.count, peakSampleCount, average, burst,
rawFrames(representative.stack));
representative.count, average, rawFrames(representative.stack));
}
Cluster toCluster() {
@@ -230,10 +204,6 @@ public final class UnknownStackClusterer {
return sampleCount;
}
double estimatedBurstSamples() {
return estimatedBurstSamples;
}
int representativeDepth() {
return representative.stack.length;
}
@@ -246,33 +216,6 @@ public final class UnknownStackClusterer {
return paths.values().stream().sorted(PathStats.ORDER).findFirst().orElseThrow();
}
private double estimateBurstSamples(Map<Long, TickRetention> retainedTicks) {
double peak = 0.0D;
for (Map.Entry<Long, Integer> tick : tickCounts.entrySet()) {
TickRetention retention = retainedTicks.get(tick.getKey());
if (retention == null) {
continue;
}
double estimate = tick.getValue() * retention.totalSamples()
/ (double) retention.retainedSamples;
peak = Math.max(peak, estimate);
}
return peak;
}
}
private static final class TickRetention {
private long reportedTotal;
private int retainedSamples;
void retain(long totalSamples) {
retainedSamples++;
reportedTotal = Math.max(reportedTotal, totalSamples);
}
long totalSamples() {
return Math.max(reportedTotal, retainedSamples);
}
}
private static final class PathStats {
@@ -9,7 +9,6 @@ final class UnknownStackReservoir {
private final AtomicReferenceArray<StackTraceElement[]> stacks =
new AtomicReferenceArray<>(CAPACITY);
private final long[] tickIds = new long[CAPACITY];
private final long[] tickTotals = new long[CAPACITY];
private final SplittableRandom random;
private long revision;
private long totalObserved;
@@ -26,7 +25,6 @@ final class UnknownStackReservoir {
long observed = ++totalObserved;
int slot = replacementSlot(observed);
if (slot >= 0) {
tickTotals[slot] = 0L;
tickIds[slot] = tickId;
stacks.lazySet(slot, stack);
}
@@ -36,7 +34,7 @@ final class UnknownStackReservoir {
UnknownStackSnapshot snapshot(long reportId, long tickCount, int intervalMicros) {
return snapshot(new SnapshotParameters(
reportId, tickCount, intervalMicros, Long.MAX_VALUE,
0L, 0L, 0L, false));
0L, false));
}
void completeTick(long tickId, long totalSamples) {
@@ -44,12 +42,6 @@ final class UnknownStackReservoir {
return;
}
totalPrimarySamples += totalSamples;
int populated = (int) Math.min(totalObserved, CAPACITY);
for (int index = 0; index < populated; index++) {
if (tickIds[index] == tickId && stacks.get(index) != null) {
tickTotals[index] = totalSamples;
}
}
if (totalSamples > 0L) {
revision++;
}
@@ -61,15 +53,13 @@ final class UnknownStackReservoir {
int retained = retainedCount(populated, parameters.maximumTickId);
StackTraceElement[][] copiedStacks = new StackTraceElement[retained][];
long[] copiedTickIds = new long[retained];
long[] copiedTickTotals = new long[retained];
var copy = new RetainedCopy(copiedStacks, copiedTickIds, copiedTickTotals);
var copy = new RetainedCopy(copiedStacks, copiedTickIds);
copyRetained(populated, copy, parameters);
long includedObserved = Math.max(0L, observed - parameters.excludedObserved);
var metadata = new UnknownStackSnapshot.Metadata(
parameters.reportId, revision, includedObserved, totalPrimarySamples,
parameters.tickCount, parameters.intervalMicros, parameters.finalized);
var samples = new UnknownStackSnapshot.Samples(
copiedStacks, copiedTickIds, copiedTickTotals);
var samples = new UnknownStackSnapshot.Samples(copiedStacks, copiedTickIds);
return new UnknownStackSnapshot(metadata, samples);
}
@@ -95,9 +85,6 @@ final class UnknownStackReservoir {
}
copy.tickIds[target] = tickId;
copy.stacks[target] = stack;
long tickTotal = tickTotals[index];
copy.tickTotals[target] = tickId == parameters.activeTickId
? parameters.activeTickTotal : tickTotal;
target++;
}
}
@@ -115,13 +102,10 @@ final class UnknownStackReservoir {
int intervalMicros,
long maximumTickId,
long excludedObserved,
long activeTickId,
long activeTickTotal,
boolean finalized) {
}
private record RetainedCopy(StackTraceElement[][] stacks,
long[] tickIds,
long[] tickTotals) {
long[] tickIds) {
}
}
@@ -10,7 +10,7 @@ public final class UnknownStackSnapshot {
Objects.requireNonNull(metadata, "metadata");
Objects.requireNonNull(samples, "samples");
int retained = samples.stacks.length;
if (retained != samples.tickIds.length || retained != samples.tickTotals.length
if (retained != samples.tickIds.length
|| retained > UnknownStackReservoir.CAPACITY
|| metadata.totalObserved < retained) {
throw new IllegalArgumentException("Invalid Unknown stack snapshot size");
@@ -20,9 +20,6 @@ public final class UnknownStackSnapshot {
if (samples.tickIds[index] <= 0L) {
throw new IllegalArgumentException("Unknown stack tick IDs must be positive");
}
if (samples.tickTotals[index] < 0L) {
throw new IllegalArgumentException("Unknown stack tick totals cannot be negative");
}
}
this.metadata = metadata;
this.samples = samples;
@@ -68,10 +65,6 @@ public final class UnknownStackSnapshot {
return samples.tickIds[index];
}
long tickTotalAt(int index) {
return samples.tickTotals[index];
}
record Metadata(long reportId,
long revision,
long totalObserved,
@@ -87,11 +80,10 @@ public final class UnknownStackSnapshot {
}
}
record Samples(StackTraceElement[][] stacks, long[] tickIds, long[] tickTotals) {
record Samples(StackTraceElement[][] stacks, long[] tickIds) {
Samples {
Objects.requireNonNull(stacks, "stacks");
Objects.requireNonNull(tickIds, "tickIds");
Objects.requireNonNull(tickTotals, "tickTotals");
}
}
}
@@ -163,7 +163,7 @@ public final class UnknownStackDetailDelivery {
UnknownStackDetailsRequestPayload request, UnknownStackSnapshot snapshot) {
return new UnknownStackDetailsPayload(request.requestId(), request.reportId(),
snapshot.revision(), UnknownStackDetailsPayload.Status.EMPTY,
snapshot.totalObserved(), snapshot.retainedCount(), List.of(), List.of());
snapshot.totalObserved(), snapshot.retainedCount(), List.of());
}
static UnknownStackDetailsPayload response(
@@ -20,8 +20,7 @@ public record UnknownStackDetailsPayload(long requestId,
Status status,
long totalObserved,
int retainedCount,
List<Cluster> average,
List<Cluster> burst)
List<Cluster> average)
implements CustomPacketPayload {
static final int MAX_TOTAL_TEXT_BYTES = 64 * 1_024;
static final int MAX_STACK_FRAMES = 96;
@@ -34,14 +33,12 @@ public record UnknownStackDetailsPayload(long requestId,
public UnknownStackDetailsPayload {
Objects.requireNonNull(status, "status");
PreparedClusterList preparedAverage = prepared(average);
PreparedClusterList preparedBurst = prepared(burst);
boolean prevalidated = preparedAverage != null && preparedBurst != null;
boolean prevalidated = preparedAverage != null;
average = prevalidated ? preparedAverage.values : List.copyOf(average);
burst = prevalidated ? preparedBurst.values : List.copyOf(burst);
var metadata = new Metadata(requestId, reportId, reservoirRevision,
status, totalObserved, retainedCount);
validate(metadata, average, burst, prevalidated,
prevalidated ? preparedAverage.textBytes + preparedBurst.textBytes : 0);
validate(metadata, average, prevalidated,
prevalidated ? preparedAverage.textBytes : 0);
}
public static UnknownStackDetailsPayload failed(
@@ -50,7 +47,7 @@ public record UnknownStackDetailsPayload(long requestId,
throw new IllegalArgumentException("OK is not a failure status");
}
return new UnknownStackDetailsPayload(requestId, reportId, 0L, status,
0L, 0, List.of(), List.of());
0L, 0, List.of());
}
@Override
@@ -66,7 +63,6 @@ public record UnknownStackDetailsPayload(long requestId,
buffer.writeVarLong(payload.totalObserved);
buffer.writeVarInt(payload.retainedCount);
writeClusters(buffer, payload.average);
writeClusters(buffer, payload.burst);
}
private static UnknownStackDetailsPayload read(RegistryFriendlyByteBuf buffer) {
@@ -77,9 +73,8 @@ public record UnknownStackDetailsPayload(long requestId,
long totalObserved = buffer.readVarLong();
int retainedCount = buffer.readVarInt();
List<Cluster> average = readClusters(buffer);
List<Cluster> burst = readClusters(buffer);
return new UnknownStackDetailsPayload(requestId, reportId, revision,
status, totalObserved, retainedCount, average, burst);
status, totalObserved, retainedCount, average);
}
private static void writeClusters(RegistryFriendlyByteBuf buffer, List<Cluster> clusters) {
@@ -88,9 +83,7 @@ public record UnknownStackDetailsPayload(long requestId,
buffer.writeUtf(cluster.anchor, UnknownStackClusterer.MAX_FRAME_TEXT_LENGTH);
buffer.writeVarInt(cluster.sampleCount);
buffer.writeVarInt(cluster.representativeCount);
buffer.writeVarInt(cluster.peakSampleCount);
buffer.writeDouble(cluster.estimatedAverageMillis);
buffer.writeDouble(cluster.estimatedBurstMillis);
buffer.writeVarInt(cluster.frames.size());
for (String frame : cluster.frames) {
buffer.writeUtf(frame, UnknownStackClusterer.MAX_FRAME_TEXT_LENGTH);
@@ -105,16 +98,14 @@ public record UnknownStackDetailsPayload(long requestId,
String anchor = buffer.readUtf(UnknownStackClusterer.MAX_FRAME_TEXT_LENGTH);
int samples = buffer.readVarInt();
int representative = buffer.readVarInt();
int peak = buffer.readVarInt();
double averageMillis = buffer.readDouble();
double burstMillis = buffer.readDouble();
int frameCount = readSize(buffer, MAX_STACK_FRAMES, "stack frame list");
List<String> frames = new ArrayList<>(frameCount);
for (int frame = 0; frame < frameCount; frame++) {
frames.add(buffer.readUtf(UnknownStackClusterer.MAX_FRAME_TEXT_LENGTH));
}
clusters.add(new Cluster(anchor, samples, representative, peak,
averageMillis, burstMillis, frames));
clusters.add(new Cluster(anchor, samples, representative,
averageMillis, frames));
}
return List.copyOf(clusters);
}
@@ -149,7 +140,6 @@ public record UnknownStackDetailsPayload(long requestId,
private static void validate(Metadata metadata,
List<Cluster> average,
List<Cluster> burst,
boolean prevalidated,
int preparedTextBytes) {
if (metadata.requestId <= 0L || metadata.reportId <= 0L || metadata.revision < 0L
@@ -158,15 +148,14 @@ public record UnknownStackDetailsPayload(long requestId,
|| metadata.totalObserved < metadata.retainedCount) {
throw new IllegalArgumentException("Invalid Unknown stack response metadata");
}
if (average.size() > UnknownStackClusterer.TOP_LIMIT
|| burst.size() > UnknownStackClusterer.TOP_LIMIT) {
if (average.size() > UnknownStackClusterer.TOP_LIMIT) {
throw new IllegalArgumentException("Too many Unknown stack clusters");
}
if (metadata.status != Status.OK && (!average.isEmpty() || !burst.isEmpty())) {
if (metadata.status != Status.OK && !average.isEmpty()) {
throw new IllegalArgumentException("Failure response cannot contain clusters");
}
int textBytes = prevalidated ? preparedTextBytes
: validateClusters(average) + validateClusters(burst);
: validateClusters(average);
if (textBytes > MAX_TOTAL_TEXT_BYTES) {
throw new IllegalArgumentException("Unknown stack response text exceeds 64 KiB");
}
@@ -259,18 +248,14 @@ public record UnknownStackDetailsPayload(long requestId,
public record Cluster(String anchor,
int sampleCount,
int representativeCount,
int peakSampleCount,
double estimatedAverageMillis,
double estimatedBurstMillis,
List<String> frames) {
public Cluster {
Objects.requireNonNull(anchor, "anchor");
frames = List.copyOf(frames);
if (sampleCount <= 0 || representativeCount <= 0
|| representativeCount > sampleCount || peakSampleCount <= 0
|| peakSampleCount > sampleCount
|| !Double.isFinite(estimatedAverageMillis) || estimatedAverageMillis < 0.0D
|| !Double.isFinite(estimatedBurstMillis) || estimatedBurstMillis < 0.0D) {
|| representativeCount > sampleCount
|| !Double.isFinite(estimatedAverageMillis) || estimatedAverageMillis < 0.0D) {
throw new IllegalArgumentException("Invalid Unknown stack cluster");
}
}
@@ -10,10 +10,9 @@ record UnknownStackPreparedDetails(long reportId,
long revision,
long totalObserved,
int retainedCount,
List<UnknownStackDetailsPayload.Cluster> average,
List<UnknownStackDetailsPayload.Cluster> burst) {
List<UnknownStackDetailsPayload.Cluster> average) {
private static final int MAX_CLUSTER_TEXT_BYTES =
UnknownStackDetailsPayload.MAX_TOTAL_TEXT_BYTES / (UnknownStackClusterer.TOP_LIMIT * 2);
UnknownStackDetailsPayload.MAX_TOTAL_TEXT_BYTES / UnknownStackClusterer.TOP_LIMIT;
static UnknownStackPreparedDetails prepare(UnknownStackSnapshot snapshot) {
return from(UnknownStackClusterer.cluster(snapshot));
@@ -21,14 +20,13 @@ record UnknownStackPreparedDetails(long reportId,
static UnknownStackPreparedDetails from(UnknownStackClusterer.Result result) {
return new UnknownStackPreparedDetails(result.reportId(), result.revision(),
result.totalObserved(), result.retainedCount(), clusters(result.average()),
clusters(result.burst()));
result.totalObserved(), result.retainedCount(), clusters(result.average()));
}
UnknownStackDetailsPayload payload(long requestId) {
return new UnknownStackDetailsPayload(requestId, reportId, revision,
UnknownStackDetailsPayload.Status.OK, totalObserved, retainedCount,
average, burst);
average);
}
private static List<UnknownStackDetailsPayload.Cluster> clusters(
@@ -55,7 +53,6 @@ record UnknownStackPreparedDetails(long reportId,
}
return new UnknownStackDetailsPayload.Cluster(
cluster.anchor(), cluster.sampleCount(), cluster.representativeCount(),
cluster.peakSampleCount(), cluster.estimatedAverageMillis(),
cluster.estimatedBurstMillis(), frames);
cluster.estimatedAverageMillis(), frames);
}
}
@@ -45,7 +45,7 @@
"screen.serverwarashi.profiler.status.no_sample": "No sample",
"screen.serverwarashi.profiler.status.online": "Online",
"screen.serverwarashi.profiler.status.unknown": "Unknown",
"screen.serverwarashi.profiler.status.session": "%1$s | %2$s samples | %3$s | %4$s confidence",
"screen.serverwarashi.profiler.status.session": "%1$s | %2$s samples",
"screen.serverwarashi.profiler.status.session_degraded": "%1$s | %2$s samples | %3$s | %4$s confidence | %5$s",
"screen.serverwarashi.profiler.confidence.high": "high",
"screen.serverwarashi.profiler.confidence.medium": "medium",
@@ -45,7 +45,7 @@
"screen.serverwarashi.profiler.status.no_sample": "无采样",
"screen.serverwarashi.profiler.status.online": "在线",
"screen.serverwarashi.profiler.status.unknown": "未知",
"screen.serverwarashi.profiler.status.session": "%1$s | %2$s 个样本 | %3$s | %4$s 置信度",
"screen.serverwarashi.profiler.status.session": "%1$s | %2$s 个样本",
"screen.serverwarashi.profiler.status.session_degraded": "%1$s | %2$s 个样本 | %3$s | %4$s 置信度 | %5$s",
"screen.serverwarashi.profiler.confidence.high": "高",
"screen.serverwarashi.profiler.confidence.medium": "中",
@@ -95,6 +95,23 @@ class PerfReportRowsTest {
assertTrue(row.capabilities().expansion().isEmpty());
}
@Test
void dispatchTaskWithoutEventLoopKeepsItsSyntheticName() {
var unknown = new PerfReportData.TickableEntry(
TickType.TASK, "", "Unknown", "", TickDetailKind.NONE,
-1, false, 0, 0, 0, false, "", metric(4.0, 5.0));
var dispatch = new PerfReportData.TickableEntry(
TickType.TASK, "", "Block Entity Dispatch", "", TickDetailKind.NONE,
-1, false, 0, 0, 0, false, "", metric(2.0, 3.0));
List<PerfReportRow> rows = PerfReportRows.tickables(
report(List.of(), List.of(unknown, dispatch), List.of()),
PerfReportRows.SortKey.MSPT, Map.of());
assertEquals(List.of("screen.serverwarashi.profiler.tickable.unknown",
"Block Entity Dispatch"), labels(rows));
}
@Test
void negotiatedUnknownFallbackHasOnlyLabelTooltipAndAction() {
var unknown = new PerfReportData.TickableEntry(
@@ -203,6 +220,25 @@ class PerfReportRowsTest {
headerSorts(geometry, PerfReportTableRenderer.Layout.PLAYERS));
}
@Test
void labelTooltipHitAreaFillsTheTickObjectColumn() {
PerfReportGeometry geometry = new PerfReportGeometry(
0, 0, 760, 400, 12, 730, 100, 380);
PerfReportRow row = PerfReportRows.tickables(
report(List.of(), List.of(tickable("Handler", metric(1.0D, 2.0D))), List.of()),
PerfReportRows.SortKey.MSPT, Map.of()).getFirst();
var table = new PerfReportTableRenderer.TableContext(
geometry, PerfReportTableRenderer.Layout.TICKABLES);
var context = new PerfReportTableRenderer.RowContext(table, row, 120);
PerfReportBounds bounds = PerfReportTableRenderer.labelCellBounds(context);
assertEquals(geometry.contentX(), bounds.x());
assertEquals(PerfReportTableRenderer.labelWidth(table), bounds.width());
assertEquals(120, bounds.y());
assertEquals(PerfReportStyle.ROW_HEIGHT, bounds.height());
}
@Test
void selectedHeaderIsBoldAndOnlyTurnsItalicWhileOrderIsStale() {
var fresh = new PerfReportTableRenderer.HeaderState(PerfReportRows.SortKey.MSPT, false, 0, 0);
@@ -16,10 +16,11 @@ class PerfTimeTextTest {
0, 0, 0, java.util.List.of(), java.util.List.of(), java.util.List.of())
.withSampling(new PerfReportData.SamplingMetadata("JFR",
PerfReportData.DurationSemantics.PRIMARY_SAMPLE_DENSITY,
PerfReportData.Confidence.HIGH, 0L, ""));
PerfReportData.Confidence.HIGH, 3L, "detail degraded"));
Component session = PerfTimeText.session(report, true, now);
assertEquals("screen.serverwarashi.profiler.status.session", key(session));
assertEquals(2, contents(session).getArgs().length);
assertEquals("screen.serverwarashi.profiler.status.running",
key((Component) contents(session).getArgs()[0]));
assertEquals("screen.serverwarashi.profiler.status.online",
@@ -39,8 +39,8 @@ class UnknownStackCopyControllerTest {
UnknownStackCopyController.handlePayload(response(requestId, 9L));
assertTrue(fixture.output.clipboard.contains("Average-time Top 3"));
assertTrue(fixture.output.clipboard.contains("Burst-time Top 3"));
assertEquals(2, fixture.output.notifiedCount);
assertFalse(fixture.output.clipboard.contains("Burst-time Top 3"));
assertEquals(1, fixture.output.notifiedCount);
assertEquals(0L, fixture.controller.pendingRequestId());
}
@@ -149,11 +149,11 @@ class UnknownStackCopyControllerTest {
@Test
void copiedDetailsScaleReliableJfrSharesToPrimaryUnknownTime() {
UnknownStackDetailsPayload.Cluster cluster = new UnknownStackDetailsPayload.Cluster(
"example.Work#run", 98, 98, 25, 4.6087890625D, 176.5D,
"example.Work#run", 98, 98, 4.6087890625D,
List.of("example.Work.run(Work.java:10)"));
UnknownStackDetailsPayload payload = new UnknownStackDetailsPayload(
1L, 3L, 2L, UnknownStackDetailsPayload.Status.OK,
1_822L, 256, List.of(cluster), List.of(cluster));
1_822L, 256, List.of(cluster));
var summary = new UnknownStackCopyController.UnknownSummary(
47_194L, 13.703D, 3_920L, 1_000);
@@ -162,16 +162,18 @@ class UnknownStackCopyControllerTest {
assertTrue(copied.contains("Primary Unknown: 47,194 samples | 13.703 MSPT"));
assertTrue(copied.contains("Reliable JFR observations: 1,822"));
assertFalse(copied.contains("Unresolved Unknown"));
assertFalse(copied.contains("Burst-time"));
assertFalse(copied.contains("retained tick peak"));
assertTrue(copied.contains("estimated 5.246 MSPT"));
}
private static UnknownStackDetailsPayload response(long requestId, long reportId) {
UnknownStackDetailsPayload.Cluster cluster = new UnknownStackDetailsPayload.Cluster(
"example.Work#run", 4, 3, 2, 1.0D, 2.0D,
"example.Work#run", 4, 3, 1.0D,
List.of("example.Work.run(Work.java:10)"));
return new UnknownStackDetailsPayload(requestId, reportId, 2L,
UnknownStackDetailsPayload.Status.OK, 4L, 4,
List.of(cluster), List.of(cluster));
List.of(cluster));
}
private static final class Fixture {
@@ -36,7 +36,7 @@ class DetailSampleAllocatorTest {
}
@Test
void handlerDetailsOnlyRefineExistingGenericHandlerSamples() {
void handlerEvidenceRemainsAvailableForCrossTickAllocation() {
TickSamples tick = new TickSamples(null);
PerfWorkTracker.Snapshot generic = snapshot(TickType.HANDLER, "Handler");
for (int sample = 0; sample < 8; sample++) {
@@ -51,9 +51,9 @@ class DetailSampleAllocatorTest {
assertEquals(8, tick.totalSamples);
assertEquals(8, attributedSamples(tick));
assertEquals(4, samples(tick, "FirstEvents#first(ServerTickEvent.Pre)"));
assertEquals(4, samples(tick, "SecondEvents#second(ServerTickEvent.Pre)"));
assertEquals(0, samples(tick, "Handler"));
assertEquals(8, samples(tick, "Handler"));
assertEquals(4, tick.details.handlerEvidence().values().stream()
.mapToInt(Integer::intValue).sum());
}
@Test
@@ -69,6 +69,33 @@ class DetailSampleAllocatorTest {
assertEquals(0, samples(tick, "Entity Dispatch"));
}
@Test
void blockEntityWrapperLookupReassignsUnknownToDispatch() {
TickSamples tick = unknownTick(5);
record(tick, 2, blockEntityPositionLookupStack(),
PrimaryObservationRing.CoarseState.UNKNOWN);
tick.applyDetails(new UnknownStackReservoir(12L));
assertEquals(5, tick.totalSamples);
assertEquals(5, attributedSamples(tick));
assertEquals(5, samples(tick, "Block Entity Dispatch"));
assertEquals(0, samples(tick, "Unknown"));
}
@Test
void entityIterationReassignsUnknownToDispatch() {
TickSamples tick = unknownTick(5);
record(tick, 2, entityIterationStack(), PrimaryObservationRing.CoarseState.UNKNOWN);
tick.applyDetails(new UnknownStackReservoir(14L));
assertEquals(5, tick.totalSamples);
assertEquals(5, attributedSamples(tick));
assertEquals(5, samples(tick, "Entity Dispatch"));
assertEquals(0, samples(tick, "Unknown"));
}
@Test
void absentJfrPreservesKnownAndCoarsePrimaryCategories() {
TickSamples tick = new TickSamples(null);
@@ -122,15 +149,17 @@ class DetailSampleAllocatorTest {
}
@Test
void unknownPrimaryRetainsMissedTrackedAndHandlerStacks() {
void unknownPrimaryRetainsOnlyUnclassifiedStacks() {
TickSamples tick = unknownTick(6);
tick.tickId = 1L;
record(tick, 1, entityExecutionStack(), PrimaryObservationRing.CoarseState.UNKNOWN);
record(tick, 1, blockEntityExecutionStack(), PrimaryObservationRing.CoarseState.UNKNOWN);
record(tick, 1, createBlockEntityExecutionStack(),
PrimaryObservationRing.CoarseState.UNKNOWN);
record(tick, 1, chunkExecutionStack(), PrimaryObservationRing.CoarseState.UNKNOWN);
record(tick, 1, taskExecutionStack(), PrimaryObservationRing.CoarseState.UNKNOWN);
record(tick, 1, handlerStack("MissedEvents", "tick"),
PrimaryObservationRing.CoarseState.UNKNOWN);
record(tick, 2, unknownStack(), PrimaryObservationRing.CoarseState.UNKNOWN);
UnknownStackReservoir stacks = new UnknownStackReservoir(19L);
tick.applyDetails(stacks);
@@ -139,8 +168,12 @@ class DetailSampleAllocatorTest {
assertEquals(6, attributedSamples(tick));
assertEquals(6, samples(tick, "Unknown"));
assertEquals(5L, snapshot.totalObserved());
assertEquals(2L, snapshot.totalObserved());
assertEquals(6L, snapshot.totalPrimarySamples());
assertTrue(java.util.stream.IntStream.range(0, snapshot.retainedCount())
.mapToObj(snapshot::stackAt)
.flatMap(java.util.Arrays::stream)
.noneMatch(frame -> frame.getClassName().startsWith("com.simibubi.create.")));
}
private static TickSamples unknownTick(int samples) {
@@ -190,6 +223,15 @@ class DetailSampleAllocatorTest {
"net.minecraft.world.level.chunk.LevelChunk$BoundTickingBlockEntity", "tick"));
}
private static StackTraceElement[] blockEntityPositionLookupStack() {
return stack(
frame("net.minecraft.world.level.chunk.LevelChunk$BoundTickingBlockEntity",
"getPos"),
frame("net.minecraft.world.level.chunk.LevelChunk$RebindableTickingBlockEntityWrapper",
"getPos"),
frame("net.minecraft.world.level.Level", "tickBlockEntities"));
}
private static StackTraceElement[] unknownStack() {
return stack(frame("com.example.TickWork", "calculate"));
}
@@ -200,6 +242,13 @@ class DetailSampleAllocatorTest {
frame("net.minecraft.server.level.ServerLevel", "tickNonPassenger"));
}
private static StackTraceElement[] entityIterationStack() {
return stack(
frame("com.example.SomeEntity", "checkDespawn"),
frame("net.minecraft.server.level.ServerLevel", "lambda$tick$4"),
frame("net.minecraft.world.level.entity.EntityTickList", "forEach"));
}
private static StackTraceElement[] blockEntityExecutionStack() {
return stack(
frame("com.example.SomeBlockEntity", "serverTick"),
@@ -207,6 +256,23 @@ class DetailSampleAllocatorTest {
frame("net.minecraft.world.level.chunk.LevelChunk$BoundTickingBlockEntity", "tick"));
}
private static StackTraceElement[] createBlockEntityExecutionStack() {
return stack(
frame("com.simibubi.create.foundation.blockEntity.SmartBlockEntity$$Lambda.0x0",
"accept"),
frame("it.unimi.dsi.fastutil.objects.Reference2ObjectArrayMap$ValuesCollection",
"forEach"),
frame("com.simibubi.create.foundation.blockEntity.SmartBlockEntity",
"forEachBehaviour"),
frame("com.simibubi.create.foundation.blockEntity.SmartBlockEntity", "tick"),
frame("com.simibubi.create.content.kinetics.base.KineticBlockEntity", "tick"),
frame("com.simibubi.create.foundation.blockEntity.SmartBlockEntityTicker", "tick"),
frame("net.minecraft.world.level.chunk.LevelChunk$BoundTickingBlockEntity", "tick"),
frame("net.minecraft.world.level.chunk.LevelChunk$RebindableTickingBlockEntityWrapper",
"tick"),
frame("net.minecraft.world.level.Level", "tickBlockEntities"));
}
private static StackTraceElement[] chunkExecutionStack() {
return stack(
frame("com.example.ChunkWork", "run"),
@@ -0,0 +1,105 @@
package com.moepus.serverwarashi.common.modules.performance.sampling;
import com.moepus.serverwarashi.common.modules.performance.report.PerfReportData;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class HandlerDetailAccumulatorTest {
@Test
void evidenceAcrossTicksRefinesAllPrimaryHandlerSamples() {
PerfSampleAggregator aggregator = new PerfSampleAggregator(1L);
accept(aggregator, handlerTick(3, handlerStack("ExampleEvents", "first")));
PerfReportData early = aggregator.build(50_000_000L, 3L, 1_000);
assertEquals(3L, samples(early, "Handler"));
accept(aggregator, handlerTick(5, handlerStack("ExampleEvents", "first")));
PerfReportData refined = aggregator.build(100_000_000L, 8L, 1_000);
assertEquals(8L, samples(refined,
"ExampleEvents#first(ServerTickEvent.Pre)"));
assertEquals(0L, samples(refined, "Handler"));
assertEquals(8L, handlerSamples(refined));
}
@Test
void oneEvidenceSampleLeavesCrossTickHandlerWeightGeneric() {
PerfSampleAggregator aggregator = new PerfSampleAggregator(2L);
accept(aggregator, handlerTick(4, handlerStack("ExampleEvents", "first")));
accept(aggregator, handlerTick(6));
PerfReportData report = aggregator.build(100_000_000L, 10L, 1_000);
assertEquals(10L, samples(report, "Handler"));
assertEquals(10L, handlerSamples(report));
}
@Test
void crossTickRatiosPreserveHandlerWeightAndOriginalTickMaximums() {
PerfSampleAggregator aggregator = new PerfSampleAggregator(3L);
accept(aggregator, handlerTick(4, handlerStack("FirstEvents", "run")));
accept(aggregator, handlerTick(8,
handlerStack("SecondEvents", "run"),
handlerStack("SecondEvents", "run"),
handlerStack("SecondEvents", "run")));
PerfReportData report = aggregator.build(100_000_000L, 12L, 1_000);
assertEquals(3L, samples(report, "FirstEvents#run(ServerTickEvent.Pre)"));
assertEquals(9L, samples(report, "SecondEvents#run(ServerTickEvent.Pre)"));
assertEquals(12L, handlerSamples(report));
assertTrue(entry(report, "SecondEvents#run(ServerTickEvent.Pre)").metric().max()
> entry(report, "FirstEvents#run(ServerTickEvent.Pre)").metric().max());
}
private static void accept(PerfSampleAggregator aggregator, TickSamples tick) {
tick.applyDetails(new UnknownStackReservoir(9L));
aggregator.accept(tick);
}
private static TickSamples handlerTick(int primarySamples, StackTraceElement[]... details) {
TickSamples tick = new TickSamples(null);
PerfWorkTracker.Snapshot generic = new PerfWorkTracker.Snapshot(
TickType.HANDLER, null, HandlerDetailAccumulatorTest.class, null,
TickDetailKind.NONE, -1, false, 0, 0, 0, false, "", "Handler");
for (int sample = 0; sample < primarySamples; sample++) {
tick.sample(generic);
}
for (StackTraceElement[] detail : details) {
tick.recordDetail(new AsyncStackSample(1L, detail),
PrimaryObservationRing.CoarseState.HANDLER);
}
return tick;
}
private static StackTraceElement[] handlerStack(String owner, String method) {
return new StackTraceElement[]{
frame("com.example." + owner, method),
frame("net.neoforged.bus.EventBus", "post"),
frame("net.neoforged.neoforge.event.EventHooks", "fireServerTickPre")
};
}
private static StackTraceElement frame(String className, String method) {
return new StackTraceElement(className, method, "Test.java", 1);
}
private static long handlerSamples(PerfReportData report) {
return report.tickables().stream().filter(entry -> entry.type() == TickType.HANDLER)
.mapToLong(entry -> entry.metric().samples()).sum();
}
private static long samples(PerfReportData report, String name) {
return report.tickables().stream().filter(entry -> name.equals(entry.className()))
.mapToLong(entry -> entry.metric().samples()).sum();
}
private static PerfReportData.TickableEntry entry(PerfReportData report, String name) {
return report.tickables().stream().filter(candidate -> name.equals(candidate.className()))
.findFirst().orElseThrow();
}
}
@@ -10,8 +10,12 @@ class PerfStackAttributorTest {
private static final String EVENT_BUS = "net.neoforged.bus.EventBus";
private static final String EVENT_HOOKS = "net.neoforged.neoforge.event.EventHooks";
private static final String SERVER_LEVEL = "net.minecraft.server.level.ServerLevel";
private static final String ENTITY_TICK_LIST =
"net.minecraft.world.level.entity.EntityTickList";
private static final String BOUND_BLOCK_ENTITY =
"net.minecraft.world.level.chunk.LevelChunk$BoundTickingBlockEntity";
private static final String REBINDABLE_BLOCK_ENTITY =
"net.minecraft.world.level.chunk.LevelChunk$RebindableTickingBlockEntityWrapper";
private static final String TICK_PACKAGE = "net.neoforged.neoforge.event.tick.";
@Test
@@ -102,6 +106,27 @@ class PerfStackAttributorTest {
assertEquals(PerfStackAttributor.Kind.TRACKED, detail.kind());
}
@Test
void createBlockEntityExecutionIsTrackedRatherThanUnknown() {
PerfStackAttributor.Classification detail = PerfStackAttributor.classify(stack(
frame("com.simibubi.create.foundation.blockEntity.SmartBlockEntity$$Lambda.0x0",
"accept"),
frame("it.unimi.dsi.fastutil.objects.Reference2ObjectArrayMap$ValuesCollection",
"forEach"),
frame("com.simibubi.create.foundation.blockEntity.SmartBlockEntity",
"forEachBehaviour"),
frame("com.simibubi.create.foundation.blockEntity.SmartBlockEntity", "tick"),
frame("com.simibubi.create.content.kinetics.base.KineticBlockEntity", "tick"),
frame("com.simibubi.create.foundation.blockEntity.SmartBlockEntityTicker", "tick"),
frame(BOUND_BLOCK_ENTITY, "tick"),
frame("net.minecraft.world.level.chunk.LevelChunk$RebindableTickingBlockEntityWrapper",
"tick"),
frame("net.minecraft.world.level.Level", "tickBlockEntities")
));
assertEquals(PerfStackAttributor.Kind.TRACKED, detail.kind());
}
@Test
void wrapperExecutionIsDispatch() {
assertEquals(PerfStackAttributor.Kind.ENTITY_DISPATCH,
@@ -116,6 +141,49 @@ class PerfStackAttributorTest {
frame(SERVER_LEVEL, "tickNonPassenger"))).kind());
}
@Test
void blockEntityWrapperPositionLookupIsDispatch() {
PerfStackAttributor.Classification detail = PerfStackAttributor.classify(stack(
frame(BOUND_BLOCK_ENTITY, "getPos"),
frame(REBINDABLE_BLOCK_ENTITY, "getPos"),
frame("net.minecraft.world.level.Level", "tickBlockEntities"),
frame("net.minecraft.server.level.ServerLevel", "tick")
));
assertEquals(PerfStackAttributor.Kind.BLOCK_ENTITY_DISPATCH, detail.kind());
assertEquals(TickType.TASK, detail.snapshot().type());
assertEquals("Block Entity Dispatch", detail.snapshot().displayName());
}
@Test
void entityIterationBeforeTickIsDispatch() {
PerfStackAttributor.Classification detail = PerfStackAttributor.classify(stack(
frame("com.example.SomeEntity", "checkDespawn"),
frame(SERVER_LEVEL, "lambda$tick$4"),
frame(ENTITY_TICK_LIST, "forEach"),
frame(SERVER_LEVEL, "tick")
));
assertEquals(PerfStackAttributor.Kind.ENTITY_DISPATCH, detail.kind());
assertEquals(TickType.TASK, detail.snapshot().type());
assertEquals("Entity Dispatch", detail.snapshot().displayName());
}
@Test
void passengerWrapperAndActualRideTickAreSeparated() {
PerfStackAttributor.Classification wrapper = PerfStackAttributor.classify(stack(
frame("net.minecraft.world.entity.Entity", "getVehicle"),
frame(SERVER_LEVEL, "tickPassenger")
));
PerfStackAttributor.Classification actual = PerfStackAttributor.classify(stack(
frame("com.example.SomePassenger", "rideTick"),
frame(SERVER_LEVEL, "tickPassenger")
));
assertEquals(PerfStackAttributor.Kind.ENTITY_DISPATCH, wrapper.kind());
assertEquals(PerfStackAttributor.Kind.TRACKED, actual.kind());
}
@Test
void knownChunkAndTaskStacksAreTracked() {
List<StackTraceElement[]> stacks = List.of(
@@ -3,9 +3,7 @@ package com.moepus.serverwarashi.common.modules.performance.sampling;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -13,7 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
class UnknownStackClustererTest {
@Test
void separatesAverageFrequencyFromSingleTickBurst() {
void clustersByAverageFrequencyAndSelectsRepresentativePath() {
List<StackTraceElement[]> stacks = new ArrayList<>();
List<Long> ticks = new ArrayList<>();
add(stacks, ticks, alpha("java.lang.reflect.Method", 10), 1L);
@@ -32,10 +30,6 @@ class UnknownStackClustererTest {
assertEquals(4, result.average().getFirst().sampleCount());
assertEquals(3, result.average().getFirst().representativeCount());
assertEquals(1.0D, result.average().getFirst().estimatedAverageMillis());
assertEquals("com.example.Beta#work -> com.example.Helper#run",
result.burst().getFirst().anchor());
assertEquals(3, result.burst().getFirst().peakSampleCount());
assertEquals(3.0D, result.burst().getFirst().estimatedBurstMillis());
assertFalse(result.average().getFirst().anchor().contains("0x123"));
assertEquals("com.example.Alpha/0x123.work(Alpha.java:10)",
result.average().getFirst().frames().get(1));
@@ -58,38 +52,16 @@ class UnknownStackClustererTest {
assertEquals("com.example.Beta#run -> com.example.Helper#call",
result.average().getFirst().anchor());
assertEquals(0.5D, result.average().getFirst().estimatedAverageMillis());
assertEquals(0.5D, result.average().getFirst().estimatedBurstMillis());
assertEquals(result.average(), UnknownStackClusterer.cluster(snapshot).average());
}
@Test
void globalReservoirScaleDoesNotInflateSingleTickPeak() {
List<StackTraceElement[]> stacks = new ArrayList<>();
List<Long> ticks = new ArrayList<>();
for (long tick = 1L; tick <= 256L; tick++) {
add(stacks, ticks, beta((int) tick), tick);
}
long[] tickTotals = new long[256];
java.util.Arrays.fill(tickTotals, 1L);
var metadata = new UnknownStackSnapshot.Metadata(
8L, 2L, 25_600L, 25_600L, 25_600L, 1_000, true);
var samples = new UnknownStackSnapshot.Samples(
stacks.toArray(StackTraceElement[][]::new), toLongArray(ticks), tickTotals);
UnknownStackClusterer.Result result = UnknownStackClusterer.cluster(
new UnknownStackSnapshot(metadata, samples));
assertEquals(1.0D, result.burst().getFirst().estimatedBurstMillis());
assertEquals(1.0D, result.average().getFirst().estimatedAverageMillis());
}
@Test
void reliableJfrSharesDistributeAllPrimaryUnknownWeight() {
var metadata = new UnknownStackSnapshot.Metadata(
9L, 2L, 2L, 100L, 10L, 1_000, true);
var samples = new UnknownStackSnapshot.Samples(
new StackTraceElement[][]{alpha("java.lang.reflect.Method", 1), beta(2)},
new long[]{1L, 2L}, new long[]{10L, 10L});
new long[]{1L, 2L});
UnknownStackClusterer.Result result = UnknownStackClusterer.cluster(
new UnknownStackSnapshot(metadata, samples));
@@ -105,13 +77,26 @@ class UnknownStackClustererTest {
10L, 2L, 1L, 100L, 10L, 1_000, true);
var samples = new UnknownStackSnapshot.Samples(
new StackTraceElement[][]{alpha("java.lang.reflect.Method", 1)},
new long[]{1L}, new long[]{10L});
new long[]{1L});
UnknownStackClusterer.Result result = UnknownStackClusterer.cluster(
new UnknownStackSnapshot(metadata, samples));
assertTrue(result.average().isEmpty());
assertTrue(result.burst().isEmpty());
}
@Test
void sourceLessJfrFramesDoNotAddUnknownSourceText() {
StackTraceElement sourceLess = new StackTraceElement(
"com.example.GeneratedWork", "run", null, 42);
UnknownStackSnapshot snapshot = snapshot(12L, 2L, 1_000,
List.of(stack(sourceLess), stack(sourceLess)), List.of(1L, 2L));
UnknownStackClusterer.Result result = UnknownStackClusterer.cluster(snapshot);
assertEquals("com.example.GeneratedWork.run",
result.average().getFirst().frames().getFirst());
assertFalse(result.average().getFirst().frames().getFirst().contains("Unknown Source"));
}
private static StackTraceElement[] alpha(String wrapper, int line) {
@@ -143,19 +128,11 @@ class UnknownStackClustererTest {
List<StackTraceElement[]> stacks,
List<Long> ticks) {
long[] tickIds = toLongArray(ticks);
Map<Long, Long> totals = new HashMap<>();
for (long tickId : tickIds) {
totals.merge(tickId, 1L, Long::sum);
}
long[] tickTotals = new long[tickIds.length];
for (int index = 0; index < tickIds.length; index++) {
tickTotals[index] = totals.get(tickIds[index]);
}
var metadata = new UnknownStackSnapshot.Metadata(
reportId, 2L, stacks.size(), stacks.size(),
tickCount, intervalMicros, true);
var samples = new UnknownStackSnapshot.Samples(
stacks.toArray(StackTraceElement[][]::new), tickIds, tickTotals);
stacks.toArray(StackTraceElement[][]::new), tickIds);
return new UnknownStackSnapshot(metadata, samples);
}
@@ -16,7 +16,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
class UnknownStackDetailsPayloadTest {
@Test
void roundTripsRequestAndTwoTopThreeSections() {
void roundTripsRequestAndAverageTopThree() {
UnknownStackDetailsRequestPayload request =
new UnknownStackDetailsRequestPayload(8L, 12L);
UnknownStackDetailsPayload.Cluster cluster = cluster(List.of(
@@ -24,7 +24,7 @@ class UnknownStackDetailsPayloadTest {
"net.minecraft.server.MinecraftServer.tickServer(MinecraftServer.java:1)"));
UnknownStackDetailsPayload response = new UnknownStackDetailsPayload(
8L, 12L, 14L, UnknownStackDetailsPayload.Status.OK,
400L, 256, List.of(cluster), List.of(cluster));
400L, 256, List.of(cluster));
RegistryFriendlyByteBuf buffer = buffer();
try {
UnknownStackDetailsRequestPayload.STREAM_CODEC.encode(buffer, request);
@@ -42,23 +42,23 @@ class UnknownStackDetailsPayloadTest {
UnknownStackDetailsPayload.Cluster boundary = cluster(frames);
new UnknownStackDetailsPayload(1L, 1L, 2L,
UnknownStackDetailsPayload.Status.OK, 256L, 256,
List.of(boundary), List.of());
List.of(boundary));
assertThrows(IllegalArgumentException.class, () -> new UnknownStackDetailsPayload(
1L, 1L, 2L, UnknownStackDetailsPayload.Status.OK, 256L, 256,
List.of(cluster(Collections.nCopies(97, "frame"))), List.of()));
List.of(cluster(Collections.nCopies(97, "frame")))));
assertThrows(IllegalArgumentException.class, () -> new UnknownStackDetailsPayload(
1L, 1L, 2L, UnknownStackDetailsPayload.Status.OK, 256L, 256,
List.of(cluster(List.of("x".repeat(513)))), List.of()));
List.of(cluster(List.of("x".repeat(513))))));
assertThrows(IllegalArgumentException.class, () -> new UnknownStackDetailsPayload(
1L, 1L, 2L, UnknownStackDetailsPayload.Status.OK, 256L, 256,
Collections.nCopies(4, boundary), List.of()));
Collections.nCopies(4, boundary)));
String multibyte = "界".repeat(512);
UnknownStackDetailsPayload.Cluster large = cluster(Collections.nCopies(96, multibyte));
assertThrows(IllegalArgumentException.class, () -> new UnknownStackDetailsPayload(
1L, 1L, 2L, UnknownStackDetailsPayload.Status.OK, 256L, 256,
List.of(large), List.of()));
List.of(large)));
}
@Test
@@ -71,34 +71,33 @@ class UnknownStackDetailsPayloadTest {
}
assertThrows(IllegalArgumentException.class, () -> new UnknownStackDetailsPayload(
1L, 2L, 0L, UnknownStackDetailsPayload.Status.BUSY,
1L, 1, List.of(cluster(List.of("frame"))), List.of()));
1L, 1, List.of(cluster(List.of("frame")))));
}
@Test
void deliveryTruncatesPathFramesToTheTotalTextBudget() {
String multibyte = "界".repeat(512);
var cluster = new UnknownStackClusterer.Cluster(
"example.Work#run", 4, 3, 2, 1.25D, 4.0D,
"example.Work#run", 4, 3, 1.25D,
Collections.nCopies(96, multibyte));
var result = new UnknownStackClusterer.Result(
2L, 4L, 256L, 256,
Collections.nCopies(3, cluster), Collections.nCopies(3, cluster));
Collections.nCopies(3, cluster));
UnknownStackDetailsPayload payload = UnknownStackDetailDelivery.response(
new UnknownStackDetailsRequestPayload(1L, 2L), result);
assertEquals(3, payload.average().size());
assertEquals(3, payload.burst().size());
assertEquals(7, payload.average().getFirst().frames().size());
assertEquals(14, payload.average().getFirst().frames().size());
}
@Test
void preparedDetailsShareBoundedContentAcrossRequestPayloads() {
var cluster = new UnknownStackClusterer.Cluster(
"example.Work#run", 4, 3, 2, 1.25D, 4.0D,
"example.Work#run", 4, 3, 1.25D,
List.of("example.Work.run(Work.java:10)"));
var result = new UnknownStackClusterer.Result(
2L, 4L, 256L, 256, List.of(cluster), List.of(cluster));
2L, 4L, 256L, 256, List.of(cluster));
UnknownStackPreparedDetails details = UnknownStackPreparedDetails.from(result);
UnknownStackDetailsPayload first = details.payload(1L);
@@ -107,7 +106,6 @@ class UnknownStackDetailsPayloadTest {
assertEquals(1L, first.requestId());
assertEquals(2L, second.requestId());
assertSame(first.average(), second.average());
assertSame(first.burst(), second.burst());
}
@Test
@@ -120,7 +118,7 @@ class UnknownStackDetailsPayloadTest {
private static UnknownStackDetailsPayload.Cluster cluster(List<String> frames) {
return new UnknownStackDetailsPayload.Cluster(
"example.Work#run", 4, 3, 2, 1.25D, 4.0D, frames);
"example.Work#run", 4, 3, 1.25D, frames);
}
private static RegistryFriendlyByteBuf buffer() {