fix(perf): measure fallback Unknown samples directly

This commit is contained in:
MoePus committed 2026-09-01 22:08:11 +09:00
1 parent 6597a789fc
commit 80abcafafc
11 files changed
+154 -33

No files matched your search

@@ -45,9 +45,8 @@ final class PerfObjectSnapshotBuilder {
}
static Build build(Map<TickSamples.TickableKey, PerfSampleAggregator.TickableAggregate> tickables,
long tickCount,
double nanosPerSample) {
Builder builder = new Builder(tickCount, nanosPerSample);
SampleMetricContext metricContext) {
Builder builder = new Builder(metricContext);
tickables.forEach(builder::add);
return builder.finish(tickables.size());
}
@@ -68,15 +67,15 @@ final class PerfObjectSnapshotBuilder {
private static final class Builder {
private final long tickCount;
private final double nanosPerSample;
private final SampleMetricContext metricContext;
private final Map<TickSamples.ChunkKey, ObjectCounts> chunkCounts = new HashMap<>();
private final Map<CoordinateGroup, PriorityQueue<ObjectCandidate>> groups = new HashMap<>();
private final Map<HotspotIdentity, ObjectCandidate> hotspots = new HashMap<>();
private final List<PerfReportData.TickableEntry> reportEntries = new ArrayList<>();
private Builder(long tickCount, double nanosPerSample) {
this.tickCount = tickCount;
this.nanosPerSample = nanosPerSample;
private Builder(SampleMetricContext metricContext) {
this.tickCount = metricContext.currentTick();
this.metricContext = metricContext;
}
void add(TickSamples.TickableKey key,
@@ -86,11 +85,11 @@ final class PerfObjectSnapshotBuilder {
chunkCounts.computeIfAbsent(chunk, ignored -> new ObjectCounts()).add(key, aggregate);
}
if (!isObject(key.type())) {
reportEntries.add(aggregate.toReport(key, tickCount, nanosPerSample));
reportEntries.add(aggregate.toReport(key, metricContext));
return;
}
ObjectCandidate candidate = ObjectCandidate.create(
key, aggregate, aggregate.metric(tickCount, nanosPerSample), tickCount);
key, aggregate, aggregate.metric(key, metricContext), tickCount);
PriorityQueue<ObjectCandidate> group = groups.computeIfAbsent(
CoordinateGroup.from(key), ignored -> new PriorityQueue<>(
MAX_COORDINATES_PER_GROUP, HOTTEST_FIRST.reversed()));
@@ -83,8 +83,10 @@ final class PerfSampleAggregator {
BuildSnapshot buildSnapshot(long elapsedNanos, long totalSamples, int intervalMicros) {
double nanosPerSample = SampleDensity.nanosPerAttempt(
elapsedNanos, totalSamples, intervalMicros);
SampleMetricContext metricContext = new SampleMetricContext(
tickCount, tickCount, nanosPerSample, intervalMicros * 1_000.0D);
PerfObjectSnapshotBuilder.Build objectBuild =
PerfObjectSnapshotBuilder.build(tickables, tickCount, nanosPerSample);
PerfObjectSnapshotBuilder.build(tickables, metricContext);
var limitedChunks = PerfReportLimits.topMetricUnion(
chunkEntries(objectBuild.chunkCounts(), nanosPerSample), CHUNK_VIEW_LIMIT_PER_METRIC,
PerfReportData.ChunkEntry::metric, STABLE_CHUNK_ORDER);
@@ -273,9 +275,8 @@ final class PerfSampleAggregator {
PerfReportData.TickableEntry toReport(
TickSamples.TickableKey key,
long ticks,
double nanosPerSample) {
return toReport(key, metric(ticks, nanosPerSample));
SampleMetricContext context) {
return toReport(key, metric(key, context));
}
PerfReportData.TickableEntry toReport(TickSamples.TickableKey key,
@@ -287,8 +288,10 @@ final class PerfSampleAggregator {
metric);
}
PerfReportData.Metric metric(long ticks, double nanosPerSample) {
return stats.metric(ticks, measurementTicks(ticks), nanosPerSample);
PerfReportData.Metric metric(TickSamples.TickableKey key,
SampleMetricContext context) {
return stats.metric(key.metricMode(),
context.withMeasurementTicks(measurementTicks(context.currentTick())));
}
long measurementTicks(long ticks) {
@@ -240,8 +240,10 @@ public final class PerfSamplerSession {
}
TickSamples expected = currentTick;
PerfWorkTracker.Snapshot snapshot = PerfWorkTracker.snapshot();
SampleMetricMode metricMode = SampleMetricMode.STATISTICAL;
if (snapshot == null) {
snapshot = PerfStackAttributor.capture(PerfWorkTracker.profiledThread());
metricMode = PerfStackAttributor.metricMode(snapshot);
} else if (snapshot.type() == TickType.ENTITY) {
PerfWorkTracker.Snapshot handler =
PerfStackAttributor.captureTickHandler(PerfWorkTracker.profiledThread());
@@ -256,9 +258,9 @@ public final class PerfSamplerSession {
}
sampleAttempts++;
if (expected != null && currentTick == expected) {
expected.sample(snapshot);
expected.sample(snapshot, metricMode);
} else if (expected == null && currentTick == null && snapshot != null) {
betweenTicks.sample(snapshot);
betweenTicks.sample(snapshot, metricMode);
}
return SamplerStatus.RUNNING;
}
@@ -37,6 +37,11 @@ final class PerfStackAttributor {
return stack == null ? null : findTickHandler(stack);
}
static SampleMetricMode metricMode(PerfWorkTracker.Snapshot snapshot) {
return snapshot != null && snapshot.workClass() == UnknownWork.class
? SampleMetricMode.DIRECT_INTERVAL : SampleMetricMode.STATISTICAL;
}
private static StackTraceElement[] captureStack(Thread thread) {
if (thread == null || !thread.isAlive()) {
return null;
@@ -0,0 +1,16 @@
package com.moepus.serverwarashi.common.modules.performance.sampling;
record SampleMetricContext(long currentTick,
long measurementTicks,
double statisticalNanosPerSample,
double directNanosPerSample) {
SampleMetricContext withMeasurementTicks(long ticks) {
return new SampleMetricContext(currentTick, ticks,
statisticalNanosPerSample, directNanosPerSample);
}
double nanosPerSample(SampleMetricMode mode) {
return mode == SampleMetricMode.DIRECT_INTERVAL
? directNanosPerSample : statisticalNanosPerSample;
}
}
@@ -0,0 +1,6 @@
package com.moepus.serverwarashi.common.modules.performance.sampling;
enum SampleMetricMode {
STATISTICAL,
DIRECT_INTERVAL
}
@@ -11,6 +11,7 @@ final class SampleStatistics {
private static final double HALF_SAMPLE = 0.5D;
private static final double NANOS_PER_MILLISECOND = 1_000_000.0D;
private double totalSamples;
private double maxObservedSamples;
private double maxBurstSamples;
private long primaryWindow = NO_WINDOW;
private double primarySamples;
@@ -27,6 +28,7 @@ final class SampleStatistics {
}
advanceWindows(tick);
totalSamples += samples;
maxObservedSamples = Math.max(maxObservedSamples, samples);
recordBurst(samples);
primarySamples += samples;
if (tick > WINDOW_STAGGER_TICKS) {
@@ -40,12 +42,19 @@ final class SampleStatistics {
PerfReportData.Metric metric(long currentTick, long measurementTicks,
double nanosPerSample) {
advanceWindows(currentTick);
double mspt = measurementTicks == 0L ? 0.0D
: totalSamples * nanosPerSample / NANOS_PER_MILLISECOND / measurementTicks;
double estimatedMaxSamples = estimatedMaxSamples();
double estimatedMax = estimatedMaxSamples * nanosPerSample / NANOS_PER_MILLISECOND;
return new PerfReportData.Metric(mspt, Math.max(mspt, estimatedMax), Math.round(totalSamples));
return metric(SampleMetricMode.STATISTICAL, new SampleMetricContext(
currentTick, measurementTicks, nanosPerSample, nanosPerSample));
}
PerfReportData.Metric metric(SampleMetricMode mode, SampleMetricContext context) {
advanceWindows(context.currentTick());
double nanosPerSample = context.nanosPerSample(mode);
double mspt = context.measurementTicks() == 0L ? 0.0D
: totalSamples * nanosPerSample / NANOS_PER_MILLISECOND / context.measurementTicks();
double maximumSamples = mode == SampleMetricMode.DIRECT_INTERVAL
? maxObservedSamples : estimatedMaxSamples();
double maximum = maximumSamples * nanosPerSample / NANOS_PER_MILLISECOND;
return new PerfReportData.Metric(mspt, Math.max(mspt, maximum), Math.round(totalSamples));
}
PerfWorldHotspotIndex.Cost hotspotCost(long totalTicks) {
@@ -26,23 +26,42 @@ final class TickSamples {
}
void sample(PerfWorkTracker.Snapshot snapshot) {
sample(snapshot, SampleMetricMode.STATISTICAL);
}
void sample(PerfWorkTracker.Snapshot snapshot, SampleMetricMode metricMode) {
totalSamples++;
if (snapshot == null) {
return;
}
TickableKey key = TickableKey.from(snapshot);
tickables.computeIfAbsent(key, ignored -> new TickCount()).record(snapshot);
TickableKey key = TickableKey.from(snapshot, metricMode);
TickCount count = tickables.get(key);
if (count == null) {
count = new TickCount();
tickables.put(key, count);
}
count.record(snapshot);
ChunkKey chunk = ChunkKey.from(snapshot);
if (chunk != null) {
chunks.merge(chunk, 1, Integer::sum);
Integer previous = chunks.get(chunk);
chunks.put(chunk, previous == null ? 1 : previous + 1);
}
}
void mergeSamples(TickSamples source) {
totalSamples += source.totalSamples;
source.tickables.forEach((key, count) ->
tickables.computeIfAbsent(key, ignored -> new TickCount()).merge(count));
source.chunks.forEach((key, count) -> chunks.merge(key, count, Integer::sum));
for (var entry : source.tickables.entrySet()) {
TickCount count = tickables.get(entry.getKey());
if (count == null) {
count = new TickCount();
tickables.put(entry.getKey(), count);
}
count.merge(entry.getValue());
}
for (var entry : source.chunks.entrySet()) {
Integer previous = chunks.get(entry.getKey());
chunks.put(entry.getKey(), previous == null ? entry.getValue() : previous + entry.getValue());
}
}
record ChunkKey(String dimension, long position) {
@@ -70,8 +89,9 @@ final class TickSamples {
int entityId,
long fixedPosition,
String eventLoop,
String displayName) {
static TickableKey from(PerfWorkTracker.Snapshot snapshot) {
String displayName,
SampleMetricMode metricMode) {
static TickableKey from(PerfWorkTracker.Snapshot snapshot, SampleMetricMode metricMode) {
String dimension = snapshot.dimension() == null ? "" : snapshot.dimension().toString();
long position = switch (snapshot.type()) {
case CHUNK -> ChunkPos.asLong(snapshot.x(), snapshot.z());
@@ -80,7 +100,7 @@ final class TickSamples {
};
return new TickableKey(snapshot.type(), dimension, snapshot.workClass(), snapshot.detailId(),
snapshot.detailKind(),
snapshot.entityId(), position, snapshot.eventLoop(), snapshot.displayName());
snapshot.entityId(), position, snapshot.eventLoop(), snapshot.displayName(), metricMode);
}
@Override
@@ -93,7 +113,8 @@ final class TickSamples {
result = 31 * result + entityId;
result = 31 * result + PackedPositionHash.hash(fixedPosition);
result = 31 * result + Objects.hashCode(eventLoop);
return 31 * result + Objects.hashCode(displayName);
result = 31 * result + Objects.hashCode(displayName);
return 31 * result + metricMode.hashCode();
}
}
@@ -19,6 +19,34 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PerfSampleAggregatorTest {
@Test
void unknownUsesDirectIntervalAfterBetweenTickSamplesAreMerged() {
PerfWorkTracker.Snapshot unknown = PerfStackAttributor.classify(new StackTraceElement[]{
new StackTraceElement("com.example.TickWork", "calculate", "TickWork.java", 1)
});
SampleMetricMode mode = PerfStackAttributor.metricMode(unknown);
TickSamples betweenTicks = new TickSamples(null);
for (int sample = 0; sample < 3; sample++) {
betweenTicks.sample(unknown, mode);
}
TickSamples first = new TickSamples(null);
first.mergeSamples(betweenTicks);
first.sample(unknown, mode);
TickSamples second = new TickSamples(null);
second.sample(unknown, mode);
PerfSampleAggregator aggregator = new PerfSampleAggregator(24L);
aggregator.accept(first);
aggregator.accept(second);
PerfReportData report = aggregator.build(milliseconds(1_000L), 100L, 500);
PerfReportData.Metric metric = report.tickables().getFirst().metric();
assertEquals(1.25D, metric.mspt(), 0.000_001D);
assertEquals(2.0D, metric.max(), 0.000_001D);
assertEquals(5L, metric.samples());
}
@Test
void reportsHandlerAsAnUnpositionedIndependentTickable() {
PerfSampleAggregator aggregator = new PerfSampleAggregator(12L);
@@ -35,6 +35,7 @@ class PerfStackAttributorTest {
));
assertEquals(TickType.HANDLER, sample.type());
assertEquals(SampleMetricMode.STATISTICAL, PerfStackAttributor.metricMode(sample));
assertEquals("ServerEvents#onTick(" + tickCase.event() + ")", sample.displayName());
assertEquals("com.example.ServerEvents#onTick(" + TICK_PACKAGE + tickCase.event() + ")",
sample.eventLoop());
@@ -83,6 +84,7 @@ class PerfStackAttributorTest {
assertEquals(TickType.TASK, sample.type());
assertEquals("Unknown", sample.displayName());
assertEquals(SampleMetricMode.DIRECT_INTERVAL, PerfStackAttributor.metricMode(sample));
}
@Test
@@ -108,6 +110,7 @@ class PerfStackAttributorTest {
assertEquals(TickType.TASK, sample.type());
assertEquals("Unknown", sample.displayName());
assertEquals("", sample.eventLoop());
assertEquals(SampleMetricMode.DIRECT_INTERVAL, PerfStackAttributor.metricMode(sample));
}
}
@@ -8,6 +8,35 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
class SampleStatisticsTest {
private static final double ONE_MILLISECOND = 1_000_000.0D;
@Test
void directIntervalDoesNotApplyTheStatisticalUpperBound() {
SampleStatistics statistics = new SampleStatistics();
for (long tick = 1L; tick <= 20L; tick++) {
statistics.record(1.0D, tick);
}
var metric = statistics.metric(SampleMetricMode.DIRECT_INTERVAL,
new SampleMetricContext(20L, 20L, 8_000_000.0D, ONE_MILLISECOND));
assertEquals(1.0D, metric.mspt(), 0.000_001D);
assertEquals(1.0D, metric.max(), 0.000_001D);
assertEquals(20L, metric.samples());
}
@Test
void directIntervalUsesTheConfiguredDurationAndRawTickMaximum() {
SampleStatistics statistics = new SampleStatistics();
statistics.record(1.0D, 1L);
statistics.record(4.0D, 2L);
var metric = statistics.metric(SampleMetricMode.DIRECT_INTERVAL,
new SampleMetricContext(2L, 2L, 10_000_000.0D, 250_000.0D));
assertEquals(0.625D, metric.mspt(), 0.000_001D);
assertEquals(1.0D, metric.max(), 0.000_001D);
assertEquals(5L, metric.samples());
}
@Test
void estimatesMaxFromRepeatedHitProbability() {
SampleStatistics statistics = new SampleStatistics();