Refine performance detail attribution
This commit is contained in:
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) {
|
||||
|
||||
+13
-25
@@ -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();
|
||||
|
||||
|
||||
+3
-19
@@ -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) {
|
||||
|
||||
+162
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-1
@@ -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();
|
||||
}
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+25
-4
@@ -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());
|
||||
}
|
||||
|
||||
+16
-73
@@ -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 {
|
||||
|
||||
+4
-20
@@ -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) {
|
||||
}
|
||||
}
|
||||
+2
-10
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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(
|
||||
|
||||
+13
-28
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
+5
-8
@@ -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",
|
||||
|
||||
+8
-6
@@ -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 {
|
||||
|
||||
+73
-7
@@ -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"),
|
||||
|
||||
+105
@@ -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();
|
||||
}
|
||||
}
|
||||
+68
@@ -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(
|
||||
|
||||
+18
-41
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+14
-16
@@ -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() {
|
||||
|
||||
Reference in new issue
Block a user