fix(perf): measure fallback Unknown samples directly
This commit is contained in:
1 parent
6597a789fc
commit
80abcafafc
11 files changed
+154
-33
No files matched your search
+8
-9
@@ -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()));
|
||||
|
||||
+9
-6
@@ -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) {
|
||||
|
||||
+4
-2
@@ -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;
|
||||
}
|
||||
|
||||
+5
@@ -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;
|
||||
|
||||
+16
@@ -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;
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.moepus.serverwarashi.common.modules.performance.sampling;
|
||||
|
||||
enum SampleMetricMode {
|
||||
STATISTICAL,
|
||||
DIRECT_INTERVAL
|
||||
}
|
||||
+15
-6
@@ -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) {
|
||||
|
||||
+31
-10
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+28
@@ -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);
|
||||
|
||||
+3
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+29
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user