Allow deleting entities from strong load chunk
This commit is contained in:
1 parent
da5293c880
commit
52db9920b3
18 files changed
+988
-118
No files matched your search
@@ -3,6 +3,7 @@ package com.moepus.serverwarashi.client.heatmap;
|
||||
import com.moepus.serverwarashi.api.client.map.MapViewport;
|
||||
|
||||
import com.moepus.serverwarashi.common.modules.performance.report.PerfReportData;
|
||||
import com.moepus.serverwarashi.common.modules.performance.report.ChunkPerformanceRisk;
|
||||
import com.moepus.serverwarashi.common.modules.weakload.WeakChunkSnapshot;
|
||||
import com.moepus.serverwarashi.common.modules.weakload.WeakEntityType;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
@@ -17,16 +18,14 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public final class ChunkHeatmapModel {
|
||||
static final double MSPT_THRESHOLD = 0.5D;
|
||||
static final double MAX_THRESHOLD = 3.0D;
|
||||
private static final double LOG_TWO = Math.log(2.0D);
|
||||
static final double MSPT_THRESHOLD = ChunkPerformanceRisk.MSPT_THRESHOLD;
|
||||
static final double MAX_THRESHOLD = ChunkPerformanceRisk.MAX_THRESHOLD;
|
||||
private static final int MAX_LOADER_QUADS = 16_384;
|
||||
private static final int MAX_LOADER_ASSOCIATIONS = 65_536;
|
||||
private static final int MAX_SOURCES_PER_CHUNK = 4;
|
||||
private static final HeatmapColorStop LOADER_GREEN = HeatmapRiskScale.GREEN.withAlpha(36);
|
||||
private static final HeatmapColorStop LOADER_CYAN = new HeatmapColorStop(48, 45, 220, 235);
|
||||
static final int INACTIVE_LOADER_COLOR = 0x385C6268;
|
||||
private static final double MAX_BURST_CONTRIBUTION = HeatmapRiskScale.RED_CONTRIBUTION - 0.25D;
|
||||
|
||||
private ChunkHeatmapModel() {
|
||||
}
|
||||
@@ -212,11 +211,7 @@ public final class ChunkHeatmapModel {
|
||||
}
|
||||
|
||||
static double severity(PerfReportData.Metric metric) {
|
||||
double normalizedMspt = metric.mspt() / MSPT_THRESHOLD;
|
||||
double burst = Math.max(0.0D, metric.max() - metric.mspt()) / MAX_THRESHOLD;
|
||||
double burstContribution = Math.min(
|
||||
MAX_BURST_CONTRIBUTION, Math.log1p(burst) / LOG_TWO);
|
||||
return normalizedMspt + burstContribution;
|
||||
return ChunkPerformanceRisk.severity(metric);
|
||||
}
|
||||
|
||||
static int color(PerfReportData.Metric metric) {
|
||||
|
||||
@@ -60,20 +60,18 @@ public final class MapContextMenuService {
|
||||
Objects.requireNonNull(clients, "clients");
|
||||
List<MapMenuItem> items = new ArrayList<>();
|
||||
MenuContext context = new MenuContext(target, state, clients);
|
||||
addWeakClearItem(items, context);
|
||||
addEntityClearItem(items, context);
|
||||
addLoaderItems(items, context);
|
||||
return List.copyOf(items);
|
||||
}
|
||||
|
||||
private static void addWeakClearItem(List<MapMenuItem> items,
|
||||
MenuContext context) {
|
||||
WeakChunkSnapshot.ChunkEntry chunk = context.state().weakChunk();
|
||||
private static void addEntityClearItem(List<MapMenuItem> items,
|
||||
MenuContext context) {
|
||||
ClientActions clients = context.clients();
|
||||
if (!clients.canClearWeakChunk() || chunk == null
|
||||
|| chunk.entityCount() < WeakChunkSnapshot.ENTITY_RED_THRESHOLD) {
|
||||
WeakEntityType type = clearTarget(context);
|
||||
if (!clients.canClearWeakChunk() || type == null) {
|
||||
return;
|
||||
}
|
||||
WeakEntityType type = chunk.topTypes(1).getFirst().getKey();
|
||||
PendingClear selection = new PendingClear(context.target(), type);
|
||||
boolean confirming = selection.equals(pendingClear);
|
||||
String key = confirming
|
||||
@@ -84,6 +82,43 @@ public final class MapContextMenuService {
|
||||
() -> selectWeakClear(selection, confirming, clients)));
|
||||
}
|
||||
|
||||
private static WeakEntityType clearTarget(MenuContext context) {
|
||||
WeakChunkSnapshot.ChunkEntry weak = context.state().weakChunk();
|
||||
if (weak != null) {
|
||||
return weak.topTypes(1).stream()
|
||||
.filter(entry -> entry.getValue() >= WeakChunkSnapshot.ENTITY_CLEAR_THRESHOLD)
|
||||
.map(Map.Entry::getKey)
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
PerfReportData report = context.state().report();
|
||||
if (report == null) {
|
||||
return null;
|
||||
}
|
||||
return report.chunks().stream()
|
||||
.filter(chunk -> matchesStrongChunk(chunk, context.target()))
|
||||
.flatMap(chunk -> chunk.topEntities().stream())
|
||||
.filter(object -> object.count() >= WeakChunkSnapshot.ENTITY_CLEAR_THRESHOLD)
|
||||
.map(MapContextMenuService::clearType)
|
||||
.filter(Objects::nonNull)
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
|
||||
private static boolean matchesStrongChunk(PerfReportData.ChunkEntry chunk, MapTarget target) {
|
||||
return chunk.dimension().equals(target.dimension().toString())
|
||||
&& chunk.chunkX() == target.chunkX()
|
||||
&& chunk.chunkZ() == target.chunkZ();
|
||||
}
|
||||
|
||||
private static WeakEntityType clearType(PerfReportData.ObjectCount object) {
|
||||
WeakEntityType.Kind kind = switch (object.detailKind()) {
|
||||
case ITEM -> WeakEntityType.Kind.ITEM;
|
||||
case ENTITY -> WeakEntityType.Kind.ENTITY;
|
||||
case BLOCK, NONE -> null;
|
||||
};
|
||||
return kind == null || ResourceLocation.tryParse(object.detailId()) == null
|
||||
? null : new WeakEntityType(kind, object.detailId());
|
||||
}
|
||||
|
||||
private static void selectWeakClear(PendingClear selection,
|
||||
boolean confirming,
|
||||
ClientActions clients) {
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.moepus.serverwarashi.common.modules.performance.report;
|
||||
|
||||
public final class ChunkPerformanceRisk {
|
||||
public static final double MSPT_THRESHOLD = 0.5D;
|
||||
public static final double MAX_THRESHOLD = 3.0D;
|
||||
public static final double RED_THRESHOLD = 4.0D;
|
||||
private static final double LOG_TWO = Math.log(2.0D);
|
||||
private static final double MAX_BURST_CONTRIBUTION = RED_THRESHOLD - 0.25D;
|
||||
|
||||
private ChunkPerformanceRisk() {
|
||||
}
|
||||
|
||||
public static boolean isRedOrHotter(PerfReportData.Metric metric) {
|
||||
return metric != null && severity(metric) >= RED_THRESHOLD;
|
||||
}
|
||||
|
||||
public static double severity(PerfReportData.Metric metric) {
|
||||
double normalizedMspt = metric.mspt() / MSPT_THRESHOLD;
|
||||
double burst = Math.max(0.0D, metric.max() - metric.mspt()) / MAX_THRESHOLD;
|
||||
double burstContribution = Math.min(
|
||||
MAX_BURST_CONTRIBUTION, Math.log1p(burst) / LOG_TWO);
|
||||
return normalizedMspt + burstContribution;
|
||||
}
|
||||
}
|
||||
+13
@@ -85,6 +85,13 @@ public record PerfReportData(long reportId,
|
||||
omittedLoaderSources, chunks, tickables, owners, nextLoaderSources);
|
||||
}
|
||||
|
||||
public PerfReportData withChunks(List<ChunkEntry> nextChunks) {
|
||||
return chunks.equals(nextChunks) ? this : new PerfReportData(
|
||||
reportId, serverMspt, elapsedMillis, generatedAtEpochMillis, tickCount, totalSamples,
|
||||
intervalMicros, omittedChunks, omittedTickables, omittedOwnerSources,
|
||||
omittedLoaderSources, nextChunks, tickables, owners, loaderSources);
|
||||
}
|
||||
|
||||
public record Metric(double mspt, double max, long samples) {
|
||||
}
|
||||
|
||||
@@ -156,6 +163,12 @@ public record PerfReportData(long reportId,
|
||||
topEntities = List.copyOf(topEntities);
|
||||
topBlockEntities = List.copyOf(topBlockEntities);
|
||||
}
|
||||
|
||||
public ChunkEntry withEntities(int nextEntityCount, List<ObjectCount> nextTopEntities) {
|
||||
return entityCount == nextEntityCount && topEntities.equals(nextTopEntities)
|
||||
? this : new ChunkEntry(dimension, chunkX, chunkZ,
|
||||
nextEntityCount, blockEntityCount, nextTopEntities, topBlockEntities, metric);
|
||||
}
|
||||
}
|
||||
|
||||
public record ObjectCount(String className,
|
||||
|
||||
+157
-29
@@ -14,14 +14,13 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public final class WeakChunkCollector {
|
||||
public static final int CHUNKS_PER_TICK = 64;
|
||||
|
||||
private WeakChunkCollector() {
|
||||
public final class ChunkEntityCollector {
|
||||
private ChunkEntityCollector() {
|
||||
}
|
||||
|
||||
public static ScanResult scan(ServerLevel level, ScanCursor cursor, int chunkBudget) {
|
||||
public static ScanResult scanWeak(ServerLevel level, ScanCursor cursor, int chunkBudget) {
|
||||
if (chunkBudget <= 0) {
|
||||
throw new IllegalArgumentException("Chunk budget must be positive");
|
||||
}
|
||||
@@ -56,7 +55,7 @@ public final class WeakChunkCollector {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
WeakChunkSnapshot.ChunkEntry entry = collectChunk(storage, dimension, chunkPos);
|
||||
WeakChunkSnapshot.ChunkEntry entry = collectTrackedChunk(storage, dimension, chunkPos);
|
||||
if (entry != null) {
|
||||
warnings.add(entry);
|
||||
}
|
||||
@@ -70,48 +69,177 @@ public final class WeakChunkCollector {
|
||||
return new ScanResult(List.copyOf(warnings), cursor.sweepId(), false, scanned, false);
|
||||
}
|
||||
|
||||
public static WeakChunkSnapshot.ChunkEntry inspect(ServerLevel level, long chunkPos) {
|
||||
return collectChunk(WeakChunkEntityAccess.storage(level),
|
||||
public static WeakChunkSnapshot.ChunkEntry inspectTracked(ServerLevel level, long chunkPos) {
|
||||
return collectTrackedChunk(WeakChunkEntityAccess.storage(level),
|
||||
level.dimension().location().toString(), chunkPos);
|
||||
}
|
||||
|
||||
private static WeakChunkSnapshot.ChunkEntry collectChunk(EntitySectionStorage<Entity> storage,
|
||||
String dimension,
|
||||
long chunkPos) {
|
||||
int entityCount = trackedEntityCount(storage, chunkPos);
|
||||
static WeakChunkSnapshot.ChunkEntry inspectAccessible(ServerLevel level, long chunkPos) {
|
||||
return inspect(level, chunkPos, Visibility::isAccessible).toWeakEntry();
|
||||
}
|
||||
|
||||
public static ChunkEntitySummary inspectTicking(ServerLevel level, long chunkPos) {
|
||||
return inspect(level, chunkPos, Visibility::isTicking);
|
||||
}
|
||||
|
||||
public static VisibilitySummaries inspectByVisibility(ServerLevel level, long chunkPos) {
|
||||
EntitySectionStorage<Entity> storage = WeakChunkEntityAccess.storage(level);
|
||||
String dimension = level.dimension().location().toString();
|
||||
List<EntitySection<Entity>> tracked = new ArrayList<>();
|
||||
List<EntitySection<Entity>> ticking = new ArrayList<>();
|
||||
storage.getExistingSectionsInChunk(chunkPos).forEach(section -> {
|
||||
if (section.getStatus() == Visibility.TRACKED) {
|
||||
tracked.add(section);
|
||||
} else if (section.getStatus().isTicking()) {
|
||||
ticking.add(section);
|
||||
}
|
||||
});
|
||||
return new VisibilitySummaries(
|
||||
summarize(tracked, dimension, chunkPos),
|
||||
summarize(ticking, dimension, chunkPos));
|
||||
}
|
||||
|
||||
private static WeakChunkSnapshot.ChunkEntry collectTrackedChunk(
|
||||
EntitySectionStorage<Entity> storage, String dimension, long chunkPos) {
|
||||
List<EntitySection<Entity>> sections = sections(
|
||||
storage, chunkPos, visibility -> visibility == Visibility.TRACKED);
|
||||
int entityCount = entityCount(sections);
|
||||
if (entityCount < WeakChunkSnapshot.ENTITY_WARNING_THRESHOLD) {
|
||||
return null;
|
||||
}
|
||||
List<EntitySection<Entity>> sections = storage.getExistingSectionsInChunk(chunkPos)
|
||||
.filter(section -> section.getStatus() == Visibility.TRACKED)
|
||||
.toList();
|
||||
Map<WeakEntityType, Integer> typeCounts = sampleTypes(
|
||||
sections, dimension, chunkPos, entityCount);
|
||||
return summarize(sections, dimension, chunkPos, entityCount).toWeakEntry();
|
||||
}
|
||||
|
||||
private static ChunkEntitySummary inspect(ServerLevel level,
|
||||
long chunkPos,
|
||||
Predicate<Visibility> visibility) {
|
||||
EntitySectionStorage<Entity> storage = WeakChunkEntityAccess.storage(level);
|
||||
return summarize(sections(storage, chunkPos, visibility),
|
||||
level.dimension().location().toString(), chunkPos);
|
||||
}
|
||||
|
||||
private static ChunkEntitySummary summarize(List<EntitySection<Entity>> sections,
|
||||
String dimension,
|
||||
long chunkPos) {
|
||||
return summarize(sections, dimension, chunkPos, entityCount(sections));
|
||||
}
|
||||
|
||||
private static ChunkEntitySummary summarize(List<EntitySection<Entity>> sections,
|
||||
String dimension,
|
||||
long chunkPos,
|
||||
int entityCount) {
|
||||
ChunkPos position = new ChunkPos(chunkPos);
|
||||
return new WeakChunkSnapshot.ChunkEntry(position.x, position.z, entityCount, typeCounts);
|
||||
if (entityCount == 0) {
|
||||
return new ChunkEntitySummary(position.x, position.z, 0, Map.of());
|
||||
}
|
||||
Map<WeakEntityType, TypeEstimate> typeCounts = sampleTypes(
|
||||
sections, dimension, chunkPos, entityCount);
|
||||
return new ChunkEntitySummary(position.x, position.z, entityCount, typeCounts);
|
||||
}
|
||||
|
||||
private static int trackedEntityCount(EntitySectionStorage<Entity> storage, long chunkPos) {
|
||||
private static int entityCount(List<EntitySection<Entity>> sections) {
|
||||
return sections.stream().mapToInt(EntitySection::size).sum();
|
||||
}
|
||||
|
||||
private static List<EntitySection<Entity>> sections(
|
||||
EntitySectionStorage<Entity> storage,
|
||||
long chunkPos,
|
||||
Predicate<Visibility> visibility) {
|
||||
return storage.getExistingSectionsInChunk(chunkPos)
|
||||
.filter(section -> section.getStatus() == Visibility.TRACKED)
|
||||
.mapToInt(EntitySection::size)
|
||||
.sum();
|
||||
.filter(section -> visibility.test(section.getStatus()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static Map<WeakEntityType, Integer> sampleTypes(List<EntitySection<Entity>> sections,
|
||||
String dimension,
|
||||
long chunkPos,
|
||||
int entityCount) {
|
||||
private static Map<WeakEntityType, TypeEstimate> sampleTypes(
|
||||
List<EntitySection<Entity>> sections,
|
||||
String dimension,
|
||||
long chunkPos,
|
||||
int entityCount) {
|
||||
int[] indices = StableEntitySampler.sampleIndices(entityCount, StableEntitySampler.SAMPLE_LIMIT,
|
||||
StableEntitySampler.seed(dimension, chunkPos, entityCount));
|
||||
Map<WeakEntityType, Integer> samples = new HashMap<>();
|
||||
Map<WeakEntityType, MutableTypeSample> samples = new HashMap<>();
|
||||
List<List<Entity>> entitySections = sections.stream()
|
||||
.map(WeakChunkEntityAccess::entities)
|
||||
.toList();
|
||||
for (Entity entity : StableEntitySampler.sampleValues(entitySections, indices)) {
|
||||
samples.merge(WeakEntityClassifier.typeOf(entity), 1, Integer::sum);
|
||||
samples.computeIfAbsent(WeakEntityClassifier.typeOf(entity),
|
||||
ignored -> new MutableTypeSample(entity)).count++;
|
||||
}
|
||||
Map<WeakEntityType, Integer> sampleCounts = new HashMap<>();
|
||||
samples.forEach((type, sample) -> sampleCounts.put(type, sample.count));
|
||||
Map<WeakEntityType, Integer> estimates = StableEntitySampler.estimateCounts(
|
||||
sampleCounts, indices.length, entityCount);
|
||||
Map<WeakEntityType, TypeEstimate> result = new HashMap<>();
|
||||
estimates.forEach((type, count) -> {
|
||||
MutableTypeSample sample = samples.get(type);
|
||||
result.put(type, new TypeEstimate(count, sample.x, sample.y, sample.z));
|
||||
});
|
||||
return Map.copyOf(result);
|
||||
}
|
||||
|
||||
public record ChunkEntitySummary(int chunkX,
|
||||
int chunkZ,
|
||||
int entityCount,
|
||||
Map<WeakEntityType, TypeEstimate> typeEstimates) {
|
||||
public ChunkEntitySummary {
|
||||
if (entityCount < 0) {
|
||||
throw new IllegalArgumentException("Entity count must be non-negative");
|
||||
}
|
||||
typeEstimates = Map.copyOf(typeEstimates);
|
||||
long estimatedTotal = typeEstimates.values().stream()
|
||||
.mapToLong(TypeEstimate::count)
|
||||
.sum();
|
||||
if (estimatedTotal != entityCount) {
|
||||
throw new IllegalArgumentException("Type estimates must sum to the entity count");
|
||||
}
|
||||
}
|
||||
|
||||
public List<Map.Entry<WeakEntityType, TypeEstimate>> topTypes(int limit) {
|
||||
if (limit < 0) {
|
||||
throw new IllegalArgumentException("Top type limit must be non-negative");
|
||||
}
|
||||
return typeEstimates.entrySet().stream()
|
||||
.sorted(Map.Entry.<WeakEntityType, TypeEstimate>comparingByValue(
|
||||
java.util.Comparator.comparingInt(TypeEstimate::count))
|
||||
.reversed()
|
||||
.thenComparing(Map.Entry.comparingByKey()))
|
||||
.limit(limit)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public WeakChunkSnapshot.ChunkEntry toWeakEntry() {
|
||||
if (entityCount < WeakChunkSnapshot.ENTITY_WARNING_THRESHOLD) {
|
||||
return null;
|
||||
}
|
||||
Map<WeakEntityType, Integer> counts = new HashMap<>();
|
||||
typeEstimates.forEach((type, estimate) -> counts.put(type, estimate.count()));
|
||||
return new WeakChunkSnapshot.ChunkEntry(chunkX, chunkZ, entityCount, counts);
|
||||
}
|
||||
}
|
||||
|
||||
public record TypeEstimate(int count, int x, int y, int z) {
|
||||
public TypeEstimate {
|
||||
if (count <= 0) {
|
||||
throw new IllegalArgumentException("Estimated type count must be positive");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record VisibilitySummaries(ChunkEntitySummary tracked,
|
||||
ChunkEntitySummary ticking) {
|
||||
}
|
||||
|
||||
private static final class MutableTypeSample {
|
||||
private int count;
|
||||
private final int x;
|
||||
private final int y;
|
||||
private final int z;
|
||||
|
||||
private MutableTypeSample(Entity entity) {
|
||||
x = entity.getBlockX();
|
||||
y = entity.getBlockY();
|
||||
z = entity.getBlockZ();
|
||||
}
|
||||
return StableEntitySampler.estimateCounts(samples, indices.length, entityCount);
|
||||
}
|
||||
|
||||
public static final class ScanCursor {
|
||||
+34
-10
@@ -21,8 +21,8 @@ import java.util.Set;
|
||||
|
||||
public final class WeakChunkClearJobs {
|
||||
public static final int ENTITIES_PER_TICK = 1_000;
|
||||
static final int SERVER_CLEAR_THRESHOLD = 100;
|
||||
private static final int REMOVAL_BATCH_SIZE = 128;
|
||||
private static final int VALIDATION_TOP_TYPES = 3;
|
||||
private static final int ATTEMPT_PASSES = 2;
|
||||
private static final ArrayDeque<Job> JOBS = new ArrayDeque<>();
|
||||
private static final Set<JobKey> ACTIVE = new HashSet<>();
|
||||
@@ -83,21 +83,27 @@ public final class WeakChunkClearJobs {
|
||||
private static WeakChunkSnapshot.ChunkEntry validatedTarget(ServerLevel level,
|
||||
long chunkPos,
|
||||
WeakEntityType target) {
|
||||
WeakChunkSnapshot.ChunkEntry warning = WeakChunkCollector.inspect(level, chunkPos);
|
||||
WeakChunkSnapshot.ChunkEntry warning = ChunkEntityCollector.inspectAccessible(level, chunkPos);
|
||||
return isEstimatedTarget(warning, target) ? warning : null;
|
||||
}
|
||||
|
||||
static boolean isEstimatedTarget(WeakChunkSnapshot.ChunkEntry warning, WeakEntityType target) {
|
||||
return warning != null
|
||||
&& warning.entityCount() >= WeakChunkSnapshot.ENTITY_RED_THRESHOLD
|
||||
&& warning.topTypes(VALIDATION_TOP_TYPES).stream()
|
||||
.anyMatch(entry -> entry.getKey().equals(target));
|
||||
&& warning.entityTypeCounts().getOrDefault(target, 0) >= SERVER_CLEAR_THRESHOLD;
|
||||
}
|
||||
|
||||
static long attemptBudget(int initialEntityCount) {
|
||||
return (long) initialEntityCount * ATTEMPT_PASSES + ENTITIES_PER_TICK;
|
||||
}
|
||||
|
||||
static boolean isClearableVisibility(Visibility visibility) {
|
||||
return visibility.isAccessible();
|
||||
}
|
||||
|
||||
static boolean usesBatchRemoval(Visibility visibility) {
|
||||
return visibility == Visibility.TRACKED;
|
||||
}
|
||||
|
||||
private static BatchResult process(MinecraftServer server, Job job, int tickLimit) {
|
||||
ServerLevel level = server.getLevel(job.key().dimension());
|
||||
if (level == null) {
|
||||
@@ -124,21 +130,39 @@ public final class WeakChunkClearJobs {
|
||||
int limit) {
|
||||
long sectionId = job.currentSectionId();
|
||||
EntitySection<Entity> section = storage.getSection(sectionId);
|
||||
if (section == null || section.getStatus() != Visibility.TRACKED) {
|
||||
if (section == null || !isClearableVisibility(section.getStatus())) {
|
||||
job.cursor().finishSection();
|
||||
return 0;
|
||||
}
|
||||
SelectionSource source = new SelectionSource(section, job.target(), job.cursor());
|
||||
Selection selection = select(source, Math.min(REMOVAL_BATCH_SIZE, limit));
|
||||
WeakChunkBatchRemoval.Result result = selection.targets().isEmpty()
|
||||
? WeakChunkBatchRemoval.Result.EMPTY
|
||||
: WeakChunkBatchRemoval.clear(
|
||||
new WeakChunkBatchRemoval.SectionRef(storage, sectionId, section), selection.targets());
|
||||
var sectionRef = new WeakChunkBatchRemoval.SectionRef(storage, sectionId, section);
|
||||
WeakChunkBatchRemoval.Result result = removeSelected(sectionRef, selection.targets());
|
||||
job.cursor().recordRemovals(result.removed());
|
||||
advanceCursor(job.cursor(), selection, currentEntities(storage, sectionId));
|
||||
return result.attempted();
|
||||
}
|
||||
|
||||
private static WeakChunkBatchRemoval.Result removeSelected(
|
||||
WeakChunkBatchRemoval.SectionRef sectionRef,
|
||||
List<WeakChunkBatchRemoval.Target> targets) {
|
||||
if (targets.isEmpty()) {
|
||||
return WeakChunkBatchRemoval.Result.EMPTY;
|
||||
}
|
||||
if (usesBatchRemoval(sectionRef.section().getStatus())) {
|
||||
return WeakChunkBatchRemoval.clear(sectionRef, targets);
|
||||
}
|
||||
int removed = 0;
|
||||
for (WeakChunkBatchRemoval.Target target : targets) {
|
||||
Entity entity = target.entity();
|
||||
if (!entity.isRemoved()) {
|
||||
entity.discard();
|
||||
removed += entity.isRemoved() ? 1 : 0;
|
||||
}
|
||||
}
|
||||
return new WeakChunkBatchRemoval.Result(targets.size(), removed);
|
||||
}
|
||||
|
||||
private static Selection select(SelectionSource source, int limit) {
|
||||
// This selector is for removing a chunk's validated top entity types. Do not reuse it for
|
||||
// sparse matches: non-matching entities are scanned without consuming the removal budget.
|
||||
|
||||
@@ -13,6 +13,7 @@ public record WeakChunkSnapshot(String dimension,
|
||||
public static final int MAX_CHUNKS = 8;
|
||||
public static final int MAX_SAMPLED_TYPES = 16;
|
||||
public static final int ENTITY_WARNING_THRESHOLD = 50;
|
||||
public static final int ENTITY_CLEAR_THRESHOLD = 200;
|
||||
public static final int ENTITY_RED_THRESHOLD = 350;
|
||||
public static final int ENTITY_PURPLE_THRESHOLD = 500;
|
||||
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
package com.moepus.serverwarashi.common.network;
|
||||
|
||||
import com.moepus.serverwarashi.common.modules.performance.report.ChunkPerformanceRisk;
|
||||
import com.moepus.serverwarashi.common.modules.performance.report.PerfReportData;
|
||||
import com.moepus.serverwarashi.common.modules.performance.sampling.TickDetailKind;
|
||||
import com.moepus.serverwarashi.common.modules.weakload.ChunkEntityCollector;
|
||||
import com.moepus.serverwarashi.common.modules.weakload.ChunkEntityCollector.ChunkEntitySummary;
|
||||
import com.moepus.serverwarashi.common.modules.weakload.WeakChunkClearJobs;
|
||||
import com.moepus.serverwarashi.common.modules.weakload.WeakChunkSnapshot;
|
||||
import com.moepus.serverwarashi.common.modules.weakload.WeakEntityType;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
final class PerfChunkEntityScanQueue {
|
||||
private static final int TOP_TYPE_LIMIT = 3;
|
||||
private final LinkedHashSet<ChunkKey> deletionQueue = new LinkedHashSet<>();
|
||||
private final LinkedHashSet<ChunkKey> reportQueue = new LinkedHashSet<>();
|
||||
private final Map<ChunkKey, ChunkEntitySummary> cache = new HashMap<>();
|
||||
private Set<ChunkKey> currentKeys = Set.of();
|
||||
private Set<ChunkKey> currentRedKeys = Set.of();
|
||||
private final Set<ChunkKey> forcedCurrentKeys = new HashSet<>();
|
||||
private ReportVersion version;
|
||||
private PerfReportData sourceReport;
|
||||
private PerfReportData materializedReport;
|
||||
|
||||
PerfReportData accept(PerfReportData report) {
|
||||
if (report == null) {
|
||||
return null;
|
||||
}
|
||||
ReportVersion nextVersion = ReportVersion.from(report);
|
||||
sourceReport = report;
|
||||
if (!nextVersion.equals(version)) {
|
||||
version = nextVersion;
|
||||
currentKeys = indexKeys(report);
|
||||
currentRedKeys = indexRedKeys(report);
|
||||
forcedCurrentKeys.clear();
|
||||
reportQueue.clear();
|
||||
reportQueue.addAll(currentRedKeys);
|
||||
retainCurrentCache();
|
||||
}
|
||||
materializedReport = materialize();
|
||||
return materializedReport;
|
||||
}
|
||||
|
||||
void prioritize(List<WeakChunkClearJobs.CompletedChunk> completed) {
|
||||
for (WeakChunkClearJobs.CompletedChunk chunk : completed) {
|
||||
ChunkKey key = new ChunkKey(chunk.dimension(), chunk.chunkPos());
|
||||
deletionQueue.add(key);
|
||||
reportQueue.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
ScanBatch scan(MinecraftServer server) {
|
||||
return scan((key, deletion) -> scan(server, key, deletion));
|
||||
}
|
||||
|
||||
ScanBatch scan(EntityScanner scanner) {
|
||||
List<TrackedCorrection> trackedCorrections = new ArrayList<>();
|
||||
Set<ChunkKey> scanned = new HashSet<>();
|
||||
drainDeletionQueue(scanner, trackedCorrections, scanned);
|
||||
drainReportQueue(scanner, trackedCorrections, scanned);
|
||||
PerfReportData update = updatedReport();
|
||||
return new ScanBatch(scanned.size(), trackedCorrections, update);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
deletionQueue.clear();
|
||||
reportQueue.clear();
|
||||
cache.clear();
|
||||
currentKeys = Set.of();
|
||||
currentRedKeys = Set.of();
|
||||
forcedCurrentKeys.clear();
|
||||
version = null;
|
||||
sourceReport = null;
|
||||
materializedReport = null;
|
||||
}
|
||||
|
||||
private void drainDeletionQueue(EntityScanner scanner,
|
||||
List<TrackedCorrection> corrections,
|
||||
Set<ChunkKey> scanned) {
|
||||
while (!deletionQueue.isEmpty()) {
|
||||
ChunkKey key = removeFirst(deletionQueue);
|
||||
ScanValue value = scanner.scan(key, true);
|
||||
scanned.add(key);
|
||||
cache.put(key, value.ticking());
|
||||
if (currentKeys.contains(key)) {
|
||||
forcedCurrentKeys.add(key);
|
||||
}
|
||||
corrections.add(new TrackedCorrection(key, value.tracked()));
|
||||
}
|
||||
}
|
||||
|
||||
private void drainReportQueue(EntityScanner scanner,
|
||||
List<TrackedCorrection> corrections,
|
||||
Set<ChunkKey> scanned) {
|
||||
while (!reportQueue.isEmpty()) {
|
||||
ChunkKey key = removeFirst(reportQueue);
|
||||
if (!scanned.add(key)) {
|
||||
continue;
|
||||
}
|
||||
ScanValue value = scanner.scan(key, false);
|
||||
cache.put(key, value.ticking());
|
||||
corrections.add(new TrackedCorrection(key, value.tracked()));
|
||||
}
|
||||
}
|
||||
|
||||
private static ScanValue scan(MinecraftServer server, ChunkKey key, boolean deletion) {
|
||||
ServerLevel level = server.getLevel(key.dimension());
|
||||
if (level == null) {
|
||||
return new ScanValue(emptySummary(key), null);
|
||||
}
|
||||
ChunkEntityCollector.VisibilitySummaries summaries =
|
||||
ChunkEntityCollector.inspectByVisibility(level, key.chunkPos());
|
||||
return new ScanValue(summaries.ticking(), summaries.tracked().toWeakEntry());
|
||||
}
|
||||
|
||||
private PerfReportData updatedReport() {
|
||||
PerfReportData next = materialize();
|
||||
if (next == null || next.equals(materializedReport)) {
|
||||
return null;
|
||||
}
|
||||
materializedReport = next;
|
||||
return next;
|
||||
}
|
||||
|
||||
private PerfReportData materialize() {
|
||||
if (sourceReport == null) {
|
||||
return null;
|
||||
}
|
||||
List<PerfReportData.ChunkEntry> chunks = sourceReport.chunks().stream()
|
||||
.map(this::materializeChunk)
|
||||
.toList();
|
||||
return sourceReport.withChunks(chunks);
|
||||
}
|
||||
|
||||
private PerfReportData.ChunkEntry materializeChunk(PerfReportData.ChunkEntry chunk) {
|
||||
ChunkKey key = key(chunk);
|
||||
if (key == null || (!currentRedKeys.contains(key) && !forcedCurrentKeys.contains(key))) {
|
||||
return chunk;
|
||||
}
|
||||
ChunkEntitySummary summary = cache.get(key);
|
||||
return summary == null ? chunk : chunk.withEntities(
|
||||
summary.entityCount(), objectCounts(summary));
|
||||
}
|
||||
|
||||
private static List<PerfReportData.ObjectCount> objectCounts(ChunkEntitySummary summary) {
|
||||
return summary.topTypes(TOP_TYPE_LIMIT).stream().map(entry -> {
|
||||
WeakEntityType type = entry.getKey();
|
||||
ChunkEntityCollector.TypeEstimate estimate = entry.getValue();
|
||||
TickDetailKind detailKind = type.kind() == WeakEntityType.Kind.ITEM
|
||||
? TickDetailKind.ITEM : TickDetailKind.ENTITY;
|
||||
return new PerfReportData.ObjectCount("", type.id(), detailKind, estimate.count(),
|
||||
estimate.x(), estimate.y(), estimate.z());
|
||||
}).toList();
|
||||
}
|
||||
|
||||
private static Set<ChunkKey> indexKeys(PerfReportData report) {
|
||||
Set<ChunkKey> result = new HashSet<>();
|
||||
report.chunks().forEach(chunk -> addKey(result, chunk));
|
||||
return Set.copyOf(result);
|
||||
}
|
||||
|
||||
private static Set<ChunkKey> indexRedKeys(PerfReportData report) {
|
||||
Set<ChunkKey> result = new LinkedHashSet<>();
|
||||
report.chunks().stream()
|
||||
.filter(chunk -> ChunkPerformanceRisk.isRedOrHotter(chunk.metric()))
|
||||
.forEach(chunk -> addKey(result, chunk));
|
||||
return Set.copyOf(result);
|
||||
}
|
||||
|
||||
private static void addKey(Set<ChunkKey> target, PerfReportData.ChunkEntry chunk) {
|
||||
ChunkKey key = key(chunk);
|
||||
if (key != null) {
|
||||
target.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
private static ChunkKey key(PerfReportData.ChunkEntry chunk) {
|
||||
ResourceLocation id = ResourceLocation.tryParse(chunk.dimension());
|
||||
return id == null ? null : new ChunkKey(
|
||||
ResourceKey.create(Registries.DIMENSION, id),
|
||||
ChunkPos.asLong(chunk.chunkX(), chunk.chunkZ()));
|
||||
}
|
||||
|
||||
private void retainCurrentCache() {
|
||||
Set<ChunkKey> retained = new HashSet<>(currentKeys);
|
||||
retained.addAll(deletionQueue);
|
||||
cache.keySet().retainAll(retained);
|
||||
}
|
||||
|
||||
private static ChunkEntitySummary emptySummary(ChunkKey key) {
|
||||
ChunkPos position = new ChunkPos(key.chunkPos());
|
||||
return new ChunkEntitySummary(position.x, position.z, 0, Map.of());
|
||||
}
|
||||
|
||||
private static <T> T removeFirst(LinkedHashSet<T> values) {
|
||||
T first = values.getFirst();
|
||||
values.remove(first);
|
||||
return first;
|
||||
}
|
||||
|
||||
record ChunkKey(ResourceKey<Level> dimension, long chunkPos) {
|
||||
}
|
||||
|
||||
record TrackedCorrection(ChunkKey key, WeakChunkSnapshot.ChunkEntry entry) {
|
||||
}
|
||||
|
||||
record ScanBatch(int scannedChunks,
|
||||
List<TrackedCorrection> trackedCorrections,
|
||||
PerfReportData reportUpdate) {
|
||||
ScanBatch {
|
||||
trackedCorrections = List.copyOf(trackedCorrections);
|
||||
}
|
||||
}
|
||||
|
||||
record ScanValue(ChunkEntitySummary ticking, WeakChunkSnapshot.ChunkEntry tracked) {
|
||||
}
|
||||
|
||||
private record ReportVersion(long reportId, long tickCount, long generatedAt) {
|
||||
private static ReportVersion from(PerfReportData report) {
|
||||
return new ReportVersion(
|
||||
report.reportId(), report.tickCount(), report.generatedAtEpochMillis());
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface EntityScanner {
|
||||
ScanValue scan(ChunkKey key, boolean deletion);
|
||||
}
|
||||
}
|
||||
@@ -2,25 +2,31 @@ package com.moepus.serverwarashi.common.network;
|
||||
|
||||
import com.moepus.serverwarashi.common.modules.performance.report.PerfReportData;
|
||||
import com.moepus.serverwarashi.common.modules.performance.report.PerfReportTextRenderer;
|
||||
import com.moepus.serverwarashi.common.modules.weakload.WeakChunkClearJobs;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class PerfReportDelivery {
|
||||
private static final ViewerSubscriptions VIEWERS = new ViewerSubscriptions();
|
||||
private static final PerfChunkEntityScanQueue ENTITY_SCANS = new PerfChunkEntityScanQueue();
|
||||
private static final Set<UUID> ONE_SHOT_UPDATES = new HashSet<>();
|
||||
private static long lastPushedTickCount = -1L;
|
||||
private static boolean latestProfiling;
|
||||
|
||||
private PerfReportDelivery() {
|
||||
}
|
||||
|
||||
public static void toCommandSource(CommandSourceStack source, PerfReportData report) {
|
||||
if (source.getEntity() instanceof ServerPlayer player && WarashiNetwork.canSendPerfReport(player)) {
|
||||
sendToPlayer(player, report, false);
|
||||
ONE_SHOT_UPDATES.add(player.getUUID());
|
||||
sendToPlayer(player, prepare(report, false), false);
|
||||
return;
|
||||
}
|
||||
source.sendSuccess(() -> PerfReportTextRenderer.render(report), false);
|
||||
@@ -29,7 +35,8 @@ public final class PerfReportDelivery {
|
||||
public static void toPlayerOrConsole(MinecraftServer server, UUID playerId, PerfReportData report) {
|
||||
ServerPlayer player = playerId == null ? null : server.getPlayerList().getPlayer(playerId);
|
||||
if (player != null && WarashiNetwork.canSendPerfReport(player)) {
|
||||
sendToPlayer(player, report, false);
|
||||
ONE_SHOT_UPDATES.add(player.getUUID());
|
||||
sendToPlayer(player, prepare(report, false), false);
|
||||
return;
|
||||
}
|
||||
Component message = PerfReportTextRenderer.render(report);
|
||||
@@ -50,7 +57,8 @@ public final class PerfReportDelivery {
|
||||
if (deferred) {
|
||||
return true;
|
||||
}
|
||||
WarashiNetwork.sendPerfReport(player, report, PerfReportPayload.Mode.OPEN, profiling);
|
||||
WarashiNetwork.sendPerfReport(
|
||||
player, prepare(report, profiling), PerfReportPayload.Mode.OPEN, profiling);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -69,7 +77,8 @@ public final class PerfReportDelivery {
|
||||
VIEWERS.setMap(playerId, true);
|
||||
boolean paused = player.getServer() != null && player.getServer().isPaused();
|
||||
if (!shouldDeferInitialReport(report, profiling, paused)) {
|
||||
WarashiNetwork.sendPerfReport(player, report, PerfReportPayload.Mode.UPDATE, profiling);
|
||||
WarashiNetwork.sendPerfReport(
|
||||
player, prepare(report, profiling), PerfReportPayload.Mode.UPDATE, profiling);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,14 +94,26 @@ public final class PerfReportDelivery {
|
||||
return;
|
||||
}
|
||||
lastPushedTickCount = report.tickCount();
|
||||
pushToViewers(server, report, true);
|
||||
pushToViewers(server, prepare(report, true), true);
|
||||
}
|
||||
|
||||
public static void publishNow(MinecraftServer server,
|
||||
PerfReportData report,
|
||||
boolean profiling) {
|
||||
resetPushClock();
|
||||
pushToViewers(server, report, profiling);
|
||||
pushToViewers(server, prepare(report, profiling), profiling);
|
||||
}
|
||||
|
||||
static PerfChunkEntityScanQueue.ScanBatch scanEntityCounts(
|
||||
MinecraftServer server, List<WeakChunkClearJobs.CompletedChunk> completed) {
|
||||
ENTITY_SCANS.prioritize(completed);
|
||||
PerfChunkEntityScanQueue.ScanBatch batch = ENTITY_SCANS.scan(server);
|
||||
if (batch.reportUpdate() != null) {
|
||||
pushEntityCorrection(server, batch.reportUpdate());
|
||||
} else {
|
||||
ONE_SHOT_UPDATES.clear();
|
||||
}
|
||||
return batch;
|
||||
}
|
||||
|
||||
static boolean hasNewSnapshot(PerfReportData report, long previousTickCount) {
|
||||
@@ -105,15 +126,10 @@ public final class PerfReportDelivery {
|
||||
}
|
||||
for (UUID viewer : VIEWERS.recipients()) {
|
||||
ServerPlayer player = server.getPlayerList().getPlayer(viewer);
|
||||
if (player == null || !WarashiNetwork.canSendPerfReport(player)) {
|
||||
if (player == null || !canReceiveLiveUpdate(player, viewer)) {
|
||||
continue;
|
||||
}
|
||||
if (VIEWERS.isMapViewer(viewer) && !player.hasPermissions(2)) {
|
||||
VIEWERS.setMap(viewer, false);
|
||||
}
|
||||
if (VIEWERS.isScreenViewer(viewer) || VIEWERS.isMapViewer(viewer)) {
|
||||
WarashiNetwork.sendPerfReport(player, report, VIEWERS.nextMode(viewer), profiling);
|
||||
}
|
||||
WarashiNetwork.sendPerfReport(player, report, VIEWERS.nextMode(viewer), profiling);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +139,9 @@ public final class PerfReportDelivery {
|
||||
|
||||
public static void clearRuntimeState() {
|
||||
VIEWERS.clear();
|
||||
ENTITY_SCANS.clear();
|
||||
ONE_SHOT_UPDATES.clear();
|
||||
latestProfiling = false;
|
||||
resetPushClock();
|
||||
}
|
||||
|
||||
@@ -149,6 +168,36 @@ public final class PerfReportDelivery {
|
||||
WarashiNetwork.sendPerfReport(player, report, mode, profiling);
|
||||
}
|
||||
|
||||
private static PerfReportData prepare(PerfReportData report, boolean profiling) {
|
||||
latestProfiling = profiling;
|
||||
return ENTITY_SCANS.accept(report);
|
||||
}
|
||||
|
||||
private static void pushEntityCorrection(MinecraftServer server, PerfReportData report) {
|
||||
Set<UUID> recipients = VIEWERS.recipients();
|
||||
recipients.addAll(ONE_SHOT_UPDATES);
|
||||
for (UUID viewer : recipients) {
|
||||
ServerPlayer player = server.getPlayerList().getPlayer(viewer);
|
||||
if (player == null || !WarashiNetwork.canSendPerfReport(player)) {
|
||||
continue;
|
||||
}
|
||||
boolean liveViewer = VIEWERS.retainPermittedLiveViewer(
|
||||
viewer, player.hasPermissions(2));
|
||||
if (!liveViewer && !ONE_SHOT_UPDATES.contains(viewer)) {
|
||||
continue;
|
||||
}
|
||||
PerfReportPayload.Mode mode = liveViewer
|
||||
? VIEWERS.nextMode(viewer) : PerfReportPayload.Mode.UPDATE;
|
||||
WarashiNetwork.sendPerfReport(player, report, mode, latestProfiling);
|
||||
}
|
||||
ONE_SHOT_UPDATES.clear();
|
||||
}
|
||||
|
||||
private static boolean canReceiveLiveUpdate(ServerPlayer player, UUID playerId) {
|
||||
return WarashiNetwork.canSendPerfReport(player)
|
||||
&& VIEWERS.retainPermittedLiveViewer(playerId, player.hasPermissions(2));
|
||||
}
|
||||
|
||||
static boolean canSubscribeMap(boolean supportsChannel, boolean hasPermission) {
|
||||
return supportsChannel && hasPermission;
|
||||
}
|
||||
@@ -197,6 +246,13 @@ public final class PerfReportDelivery {
|
||||
return isScreenViewer(playerId) || isMapViewer(playerId);
|
||||
}
|
||||
|
||||
boolean retainPermittedLiveViewer(UUID playerId, boolean hasMapPermission) {
|
||||
if (!hasMapPermission) {
|
||||
setMap(playerId, false);
|
||||
}
|
||||
return isLiveViewer(playerId);
|
||||
}
|
||||
|
||||
PerfReportPayload.Mode nextMode(UUID playerId) {
|
||||
return pendingScreenOpen.remove(playerId)
|
||||
? PerfReportPayload.Mode.OPEN : PerfReportPayload.Mode.UPDATE;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.moepus.serverwarashi.common.network;
|
||||
|
||||
import com.moepus.serverwarashi.common.modules.weakload.WeakChunkClearJobs;
|
||||
import com.moepus.serverwarashi.common.modules.weakload.WeakChunkCollector;
|
||||
import com.moepus.serverwarashi.common.modules.weakload.ChunkEntityCollector;
|
||||
import com.moepus.serverwarashi.common.modules.weakload.WeakChunkSnapshot;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
@@ -19,9 +19,11 @@ import java.util.UUID;
|
||||
|
||||
public final class WeakChunkDelivery {
|
||||
static final int IDLE_SWEEP_COOLDOWN_TICKS = 200;
|
||||
static final int BASE_CHUNK_SCAN_BUDGET = 64;
|
||||
private static final Map<ServerLevel, ScanState> SCANNERS = new LinkedHashMap<>();
|
||||
private static final Map<UUID, ServerLevel> VIEWER_LEVELS = new HashMap<>();
|
||||
private static final Set<UUID> INITIAL_SYNC_PENDING = new HashSet<>();
|
||||
private static int nextScannerIndex;
|
||||
|
||||
private WeakChunkDelivery() {
|
||||
}
|
||||
@@ -40,14 +42,17 @@ public final class WeakChunkDelivery {
|
||||
updateLevels(server);
|
||||
updateViewers(server);
|
||||
publishInitialSnapshots();
|
||||
publishCorrections(server, completed);
|
||||
SCANNERS.forEach(WeakChunkDelivery::scanAndPublish);
|
||||
PerfChunkEntityScanQueue.ScanBatch priority =
|
||||
PerfReportDelivery.scanEntityCounts(server, completed);
|
||||
publishCorrections(server, priority.trackedCorrections());
|
||||
scanWeakChunks(remainingWeakBudget(priority.scannedChunks()));
|
||||
}
|
||||
|
||||
public static void clearRuntimeState() {
|
||||
SCANNERS.clear();
|
||||
VIEWER_LEVELS.clear();
|
||||
INITIAL_SYNC_PENDING.clear();
|
||||
nextScannerIndex = 0;
|
||||
}
|
||||
|
||||
private static void updateLevels(MinecraftServer server) {
|
||||
@@ -112,12 +117,33 @@ public final class WeakChunkDelivery {
|
||||
state.cursor.sweepId(), false, state.cache.currentEntries());
|
||||
}
|
||||
|
||||
private static void scanAndPublish(ServerLevel level, ScanState state) {
|
||||
if (!state.throttle.shouldScan(!state.viewers.isEmpty())) {
|
||||
static int remainingWeakBudget(int priorityScans) {
|
||||
return Math.max(0, BASE_CHUNK_SCAN_BUDGET - priorityScans);
|
||||
}
|
||||
|
||||
private static void scanWeakChunks(int budget) {
|
||||
if (budget == 0 || SCANNERS.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
WeakChunkCollector.ScanResult result = WeakChunkCollector.scan(
|
||||
level, state.cursor, WeakChunkCollector.CHUNKS_PER_TICK);
|
||||
List<Map.Entry<ServerLevel, ScanState>> scanners = List.copyOf(SCANNERS.entrySet());
|
||||
int start = Math.floorMod(nextScannerIndex, scanners.size());
|
||||
int remaining = budget;
|
||||
for (int offset = 0; offset < scanners.size() && remaining > 0; offset++) {
|
||||
Map.Entry<ServerLevel, ScanState> scanner =
|
||||
scanners.get((start + offset) % scanners.size());
|
||||
int scannersLeft = scanners.size() - offset;
|
||||
int allocation = Math.max(1, remaining / scannersLeft);
|
||||
remaining -= scanAndPublish(scanner.getKey(), scanner.getValue(), allocation);
|
||||
}
|
||||
nextScannerIndex = (start + 1) % scanners.size();
|
||||
}
|
||||
|
||||
private static int scanAndPublish(ServerLevel level, ScanState state, int budget) {
|
||||
if (!state.throttle.shouldScan(!state.viewers.isEmpty())) {
|
||||
return 0;
|
||||
}
|
||||
ChunkEntityCollector.ScanResult result = ChunkEntityCollector.scanWeak(
|
||||
level, state.cursor, budget);
|
||||
if (result.emptyStorage()) {
|
||||
state.cache.clearCurrent();
|
||||
}
|
||||
@@ -132,19 +158,22 @@ public final class WeakChunkDelivery {
|
||||
} else if (result.emptyStorage()) {
|
||||
state.throttle.onEmptyStorage(state.viewers.isEmpty());
|
||||
}
|
||||
return result.scannedChunks();
|
||||
}
|
||||
|
||||
private static void publishCorrections(
|
||||
MinecraftServer server, List<WeakChunkClearJobs.CompletedChunk> completed) {
|
||||
for (WeakChunkClearJobs.CompletedChunk chunk : completed) {
|
||||
ServerLevel level = server.getLevel(chunk.dimension());
|
||||
MinecraftServer server,
|
||||
List<PerfChunkEntityScanQueue.TrackedCorrection> corrections) {
|
||||
for (PerfChunkEntityScanQueue.TrackedCorrection correction : corrections) {
|
||||
ServerLevel level = server.getLevel(correction.key().dimension());
|
||||
ScanState state = level == null ? null : SCANNERS.get(level);
|
||||
if (state == null) {
|
||||
continue;
|
||||
}
|
||||
WeakChunkSnapshot.ChunkEntry entry = WeakChunkCollector.inspect(level, chunk.chunkPos());
|
||||
state.cache.correct(chunk.chunkPos(), entry);
|
||||
sendCorrection(level, state.viewers, chunk.chunkPos(), entry);
|
||||
long chunkPos = correction.key().chunkPos();
|
||||
if (state.cache.correct(chunkPos, correction.entry())) {
|
||||
sendCorrection(level, state.viewers, chunkPos, correction.entry());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,14 +241,14 @@ public final class WeakChunkDelivery {
|
||||
return snapshotDimension.equals(playerDimension) && subscribed && permitted;
|
||||
}
|
||||
|
||||
private static void applyCorrection(Map<Long, WeakChunkSnapshot.ChunkEntry> cache,
|
||||
long chunkPos,
|
||||
WeakChunkSnapshot.ChunkEntry entry) {
|
||||
private static boolean applyCorrection(Map<Long, WeakChunkSnapshot.ChunkEntry> cache,
|
||||
long chunkPos,
|
||||
WeakChunkSnapshot.ChunkEntry entry) {
|
||||
if (entry == null) {
|
||||
cache.remove(chunkPos);
|
||||
} else {
|
||||
cache.put(chunkPos, entry);
|
||||
return cache.remove(chunkPos) != null;
|
||||
}
|
||||
WeakChunkSnapshot.ChunkEntry previous = cache.put(chunkPos, entry);
|
||||
return !entry.equals(previous);
|
||||
}
|
||||
|
||||
private static void merge(Map<Long, WeakChunkSnapshot.ChunkEntry> target,
|
||||
@@ -234,7 +263,7 @@ public final class WeakChunkDelivery {
|
||||
}
|
||||
|
||||
private static final class ScanState {
|
||||
private final WeakChunkCollector.ScanCursor cursor = new WeakChunkCollector.ScanCursor();
|
||||
private final ChunkEntityCollector.ScanCursor cursor = new ChunkEntityCollector.ScanCursor();
|
||||
private final SweepCache cache = new SweepCache();
|
||||
private final ScanThrottle throttle = new ScanThrottle();
|
||||
private final List<ServerPlayer> viewers = new ArrayList<>();
|
||||
@@ -293,9 +322,10 @@ public final class WeakChunkDelivery {
|
||||
current.clear();
|
||||
}
|
||||
|
||||
void correct(long chunkPos, WeakChunkSnapshot.ChunkEntry entry) {
|
||||
applyCorrection(completed, chunkPos, entry);
|
||||
applyCorrection(current, chunkPos, entry);
|
||||
boolean correct(long chunkPos, WeakChunkSnapshot.ChunkEntry entry) {
|
||||
boolean completedChanged = applyCorrection(completed, chunkPos, entry);
|
||||
boolean currentChanged = applyCorrection(current, chunkPos, entry);
|
||||
return completedChanged || currentChanged;
|
||||
}
|
||||
|
||||
List<WeakChunkSnapshot.ChunkEntry> currentEntries() {
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.moepus.serverwarashi.client.map;
|
||||
|
||||
import com.moepus.serverwarashi.api.client.map.MapMenuItem;
|
||||
import com.moepus.serverwarashi.api.client.map.MapTarget;
|
||||
import com.moepus.serverwarashi.common.modules.performance.report.PerfReportData;
|
||||
import com.moepus.serverwarashi.common.modules.performance.sampling.TickDetailKind;
|
||||
import com.moepus.serverwarashi.common.modules.weakload.WeakChunkSnapshot;
|
||||
import com.moepus.serverwarashi.common.modules.weakload.WeakEntityType;
|
||||
import com.moepus.serverwarashi.common.network.LoaderFreezeActionPayload;
|
||||
@@ -53,7 +55,7 @@ class MapWeakChunkClearMenuTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void omitsUnavailableAndNonRedWeakChunkActionsAndClearResetsConfirmation() {
|
||||
void omitsUnavailableAndBelowThresholdWeakActionsAndClearResetsConfirmation() {
|
||||
MapTarget target = new MapTarget(OVERWORLD, 1, 2);
|
||||
var unavailable = new RecordingClients(false, false);
|
||||
assertTrue(items(target, chunk(ITEM), unavailable).isEmpty());
|
||||
@@ -69,6 +71,45 @@ class MapWeakChunkClearMenuTest {
|
||||
assertEquals("gui.serverwarashi.weak_clear", key(onlyItem(target, chunk(ITEM), clients)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void weakChunkUsesTheLargestTypeCountRatherThanTheChunkTotal() {
|
||||
MapTarget target = new MapTarget(OVERWORLD, 1, 2);
|
||||
var clients = new RecordingClients(true, false);
|
||||
var warning = new WeakChunkSnapshot.ChunkEntry(
|
||||
1, 2, 500, Map.of(ITEM, 199, ENTITY, 301));
|
||||
|
||||
onlyItem(target, warning, clients).action().run();
|
||||
onlyItem(target, warning, clients).action().run();
|
||||
|
||||
assertEquals(List.of(new WeakChunkClearPayload(
|
||||
OVERWORLD.toString(), 1, 2, ENTITY)), clients.weakRequests);
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowsStrongLoadedChunksByReceivedTypeCountWithoutCheckingHeatColor() {
|
||||
MapTarget target = new MapTarget(OVERWORLD, 1, 2);
|
||||
var clients = new RecordingClients(true, false);
|
||||
PerfReportData report = strongReport(ITEM, WeakChunkSnapshot.ENTITY_CLEAR_THRESHOLD);
|
||||
|
||||
MapMenuItem first = onlyItem(target, null, report, clients);
|
||||
assertEquals("gui.serverwarashi.weak_clear", key(first));
|
||||
first.action().run();
|
||||
onlyItem(target, null, report, clients).action().run();
|
||||
|
||||
assertEquals(List.of(new WeakChunkClearPayload(
|
||||
OVERWORLD.toString(), 1, 2, ITEM)), clients.weakRequests);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsStrongLoadedChunksWhoseTopTypeIsBelowTheClearThreshold() {
|
||||
MapTarget target = new MapTarget(OVERWORLD, 1, 2);
|
||||
var clients = new RecordingClients(true, false);
|
||||
PerfReportData report = strongReport(
|
||||
ITEM, 1_000, WeakChunkSnapshot.ENTITY_CLEAR_THRESHOLD - 1);
|
||||
|
||||
assertTrue(items(target, null, report, clients).isEmpty());
|
||||
}
|
||||
|
||||
private static MapMenuItem onlyItem(MapTarget target,
|
||||
WeakChunkSnapshot.ChunkEntry chunk,
|
||||
RecordingClients clients) {
|
||||
@@ -77,18 +118,51 @@ class MapWeakChunkClearMenuTest {
|
||||
return items.getFirst();
|
||||
}
|
||||
|
||||
private static MapMenuItem onlyItem(MapTarget target,
|
||||
WeakChunkSnapshot.ChunkEntry chunk,
|
||||
PerfReportData report,
|
||||
RecordingClients clients) {
|
||||
List<MapMenuItem> items = items(target, chunk, report, clients);
|
||||
assertEquals(1, items.size());
|
||||
return items.getFirst();
|
||||
}
|
||||
|
||||
private static List<MapMenuItem> items(MapTarget target,
|
||||
WeakChunkSnapshot.ChunkEntry chunk,
|
||||
RecordingClients clients) {
|
||||
var state = new MapContextMenuService.MenuState(chunk, null, 0L);
|
||||
return items(target, chunk, null, clients);
|
||||
}
|
||||
|
||||
private static List<MapMenuItem> items(MapTarget target,
|
||||
WeakChunkSnapshot.ChunkEntry chunk,
|
||||
PerfReportData report,
|
||||
RecordingClients clients) {
|
||||
var state = new MapContextMenuService.MenuState(chunk, report, report == null ? 0L : report.reportId());
|
||||
return MapContextMenuService.items(target, state, clients);
|
||||
}
|
||||
|
||||
private static WeakChunkSnapshot.ChunkEntry chunk(WeakEntityType type) {
|
||||
int count = WeakChunkSnapshot.ENTITY_RED_THRESHOLD;
|
||||
int count = WeakChunkSnapshot.ENTITY_CLEAR_THRESHOLD;
|
||||
return new WeakChunkSnapshot.ChunkEntry(1, 2, count, Map.of(type, count));
|
||||
}
|
||||
|
||||
private static PerfReportData strongReport(WeakEntityType type, int entityCount) {
|
||||
return strongReport(type, entityCount, entityCount);
|
||||
}
|
||||
|
||||
private static PerfReportData strongReport(
|
||||
WeakEntityType type, int entityCount, int typeCount) {
|
||||
TickDetailKind detailKind = type.kind() == WeakEntityType.Kind.ITEM
|
||||
? TickDetailKind.ITEM : TickDetailKind.ENTITY;
|
||||
var object = new PerfReportData.ObjectCount(
|
||||
"TestEntity", type.id(), detailKind, typeCount, 24, 64, 40);
|
||||
var chunk = new PerfReportData.ChunkEntry(
|
||||
OVERWORLD.toString(), 1, 2, entityCount, 0,
|
||||
List.of(object), List.of(), new PerfReportData.Metric(1.0D, 2.0D, 10L));
|
||||
return new PerfReportData(1.0D, 1_000L, 123_000L, 20L, 100L, 1_000,
|
||||
0, 0, 0, List.of(chunk), List.of(), List.of());
|
||||
}
|
||||
|
||||
private static String key(MapMenuItem item) {
|
||||
return ((TranslatableContents) item.label().getContents()).getKey();
|
||||
}
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.moepus.serverwarashi.common.modules.performance.report;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ChunkPerformanceRiskTest {
|
||||
@Test
|
||||
void identifiesRedByMetricWithoutClientClasses() {
|
||||
assertFalse(ChunkPerformanceRisk.isRedOrHotter(
|
||||
new PerfReportData.Metric(1.0D, 1.0D, 10L)));
|
||||
assertTrue(ChunkPerformanceRisk.isRedOrHotter(
|
||||
new PerfReportData.Metric(2.0D, 2.0D, 10L)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aSingleBurstRemainsBelowRed() {
|
||||
assertFalse(ChunkPerformanceRisk.isRedOrHotter(
|
||||
new PerfReportData.Metric(0.0D, 50.0D, 1L)));
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.moepus.serverwarashi.common.modules.weakload;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
class ChunkEntityCollectorTest {
|
||||
@Test
|
||||
void restartKeepsSweepIdsMonotonic() {
|
||||
var cursor = new ChunkEntityCollector.ScanCursor();
|
||||
|
||||
cursor.restart();
|
||||
cursor.restart();
|
||||
|
||||
assertEquals(2L, cursor.sweepId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void summaryPreservesEstimatedCountsAndRepresentativeCoordinates() {
|
||||
WeakEntityType cow = new WeakEntityType(WeakEntityType.Kind.ENTITY, "minecraft:cow");
|
||||
WeakEntityType item = new WeakEntityType(WeakEntityType.Kind.ITEM, "minecraft:stone");
|
||||
var summary = new ChunkEntityCollector.ChunkEntitySummary(1, 2, 300, Map.of(
|
||||
cow, new ChunkEntityCollector.TypeEstimate(100, 20, 64, 36),
|
||||
item, new ChunkEntityCollector.TypeEstimate(200, 21, 65, 37)));
|
||||
|
||||
assertEquals(item, summary.topTypes(1).getFirst().getKey());
|
||||
assertEquals(21, summary.topTypes(1).getFirst().getValue().x());
|
||||
assertEquals(Map.of(cow, 100, item, 200), summary.toWeakEntry().entityTypeCounts());
|
||||
}
|
||||
|
||||
@Test
|
||||
void summaryBelowWarningThresholdDoesNotBecomeAWeakWarning() {
|
||||
WeakEntityType cow = new WeakEntityType(WeakEntityType.Kind.ENTITY, "minecraft:cow");
|
||||
var summary = new ChunkEntityCollector.ChunkEntitySummary(1, 2, 49, Map.of(
|
||||
cow, new ChunkEntityCollector.TypeEstimate(49, 20, 64, 36)));
|
||||
|
||||
assertNull(summary.toWeakEntry());
|
||||
}
|
||||
}
|
||||
+21
-5
@@ -1,5 +1,6 @@
|
||||
package com.moepus.serverwarashi.common.modules.weakload;
|
||||
|
||||
import net.minecraft.world.level.entity.Visibility;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -17,22 +18,27 @@ class WeakChunkClearJobsTest {
|
||||
private static final WeakEntityType FOURTH = entity("test:fourth");
|
||||
|
||||
@Test
|
||||
void acceptsCurrentTopThreeButNotFourth() {
|
||||
void acceptsAnyRequestedTypeAtTheServerThreshold() {
|
||||
var warning = new WeakChunkSnapshot.ChunkEntry(0, 0, 1_000,
|
||||
Map.of(FIRST, 400, SECOND, 300, THIRD, 200, FOURTH, 100));
|
||||
Map.of(FIRST, 701, SECOND, 100, THIRD, 100, FOURTH, 99));
|
||||
|
||||
assertTrue(WeakChunkClearJobs.isEstimatedTarget(warning, FIRST));
|
||||
assertTrue(WeakChunkClearJobs.isEstimatedTarget(warning, SECOND));
|
||||
assertTrue(WeakChunkClearJobs.isEstimatedTarget(warning, THIRD));
|
||||
assertFalse(WeakChunkClearJobs.isEstimatedTarget(warning, FOURTH));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsWarningBelowDangerThreshold() {
|
||||
int belowDanger = WeakChunkSnapshot.ENTITY_RED_THRESHOLD - 1;
|
||||
void acceptsServerThresholdAndRejectsOneBelowIt() {
|
||||
int belowThreshold = WeakChunkClearJobs.SERVER_CLEAR_THRESHOLD - 1;
|
||||
var warning = new WeakChunkSnapshot.ChunkEntry(
|
||||
0, 0, belowDanger, Map.of(FIRST, belowDanger));
|
||||
0, 0, belowThreshold, Map.of(FIRST, belowThreshold));
|
||||
var clearable = new WeakChunkSnapshot.ChunkEntry(
|
||||
0, 0, WeakChunkClearJobs.SERVER_CLEAR_THRESHOLD,
|
||||
Map.of(FIRST, WeakChunkClearJobs.SERVER_CLEAR_THRESHOLD));
|
||||
|
||||
assertFalse(WeakChunkClearJobs.isEstimatedTarget(warning, FIRST));
|
||||
assertTrue(WeakChunkClearJobs.isEstimatedTarget(clearable, FIRST));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -45,6 +51,16 @@ class WeakChunkClearJobsTest {
|
||||
assertEquals(4_294_968_294L, WeakChunkClearJobs.attemptBudget(Integer.MAX_VALUE));
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowsTrackedAndTickingSectionsButNotHiddenSections() {
|
||||
assertFalse(WeakChunkClearJobs.isClearableVisibility(Visibility.HIDDEN));
|
||||
assertTrue(WeakChunkClearJobs.isClearableVisibility(Visibility.TRACKED));
|
||||
assertTrue(WeakChunkClearJobs.isClearableVisibility(Visibility.TICKING));
|
||||
assertTrue(WeakChunkClearJobs.usesBatchRemoval(Visibility.TRACKED));
|
||||
assertFalse(WeakChunkClearJobs.usesBatchRemoval(Visibility.TICKING));
|
||||
assertFalse(WeakChunkClearJobs.usesBatchRemoval(Visibility.HIDDEN));
|
||||
}
|
||||
|
||||
@Test
|
||||
void restoresOnlyWhenManagerDidNotEnterAndEntityHasNoManagedLocation() {
|
||||
assertEquals(WeakChunkBatchRemoval.RestoreAction.RESTORE,
|
||||
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
package com.moepus.serverwarashi.common.modules.weakload;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class WeakChunkCollectorTest {
|
||||
@Test
|
||||
void restartKeepsSweepIdsMonotonic() {
|
||||
var cursor = new WeakChunkCollector.ScanCursor();
|
||||
|
||||
cursor.restart();
|
||||
cursor.restart();
|
||||
|
||||
assertEquals(2L, cursor.sweepId());
|
||||
}
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
package com.moepus.serverwarashi.common.network;
|
||||
|
||||
import com.moepus.serverwarashi.common.modules.performance.report.PerfReportData;
|
||||
import com.moepus.serverwarashi.common.modules.performance.sampling.TickDetailKind;
|
||||
import com.moepus.serverwarashi.common.modules.weakload.ChunkEntityCollector;
|
||||
import com.moepus.serverwarashi.common.modules.weakload.WeakChunkClearJobs;
|
||||
import com.moepus.serverwarashi.common.modules.weakload.WeakChunkSnapshot;
|
||||
import com.moepus.serverwarashi.common.modules.weakload.WeakEntityType;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.Level;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
class PerfChunkEntityScanQueueTest {
|
||||
private static final WeakEntityType COW =
|
||||
new WeakEntityType(WeakEntityType.Kind.ENTITY, "minecraft:cow");
|
||||
|
||||
@Test
|
||||
void scansOnlyRedChunksAndPreservesOtherReportData() {
|
||||
var queue = new PerfChunkEntityScanQueue();
|
||||
PerfReportData.Metric red = new PerfReportData.Metric(2.0D, 2.0D, 10L);
|
||||
PerfReportData report = report(20L, List.of(
|
||||
chunk(1, 2, 7, 4, red),
|
||||
chunk(3, 4, 9, 5, new PerfReportData.Metric(1.0D, 1.0D, 8L))));
|
||||
List<PerfChunkEntityScanQueue.ChunkKey> scanned = new ArrayList<>();
|
||||
|
||||
queue.accept(report);
|
||||
PerfChunkEntityScanQueue.ScanBatch batch = queue.scan((key, deletion) -> {
|
||||
scanned.add(key);
|
||||
return new PerfChunkEntityScanQueue.ScanValue(summary(key, 300), null);
|
||||
});
|
||||
|
||||
assertEquals(1, batch.scannedChunks());
|
||||
assertEquals(1, scanned.size());
|
||||
assertEquals(1, batch.trackedCorrections().size());
|
||||
assertNull(batch.trackedCorrections().getFirst().entry());
|
||||
PerfReportData.ChunkEntry corrected = batch.reportUpdate().chunks().getFirst();
|
||||
assertEquals(300, corrected.entityCount());
|
||||
assertEquals(300, corrected.topEntities().getFirst().count());
|
||||
assertEquals(4, corrected.blockEntityCount());
|
||||
assertEquals(red, corrected.metric());
|
||||
assertEquals(9, batch.reportUpdate().chunks().get(1).entityCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void deletionRefreshOverridesAndDeduplicatesAQueuedRedScan() {
|
||||
var queue = new PerfChunkEntityScanQueue();
|
||||
queue.accept(report(20L, List.of(chunk(1, 2, 7, 0,
|
||||
new PerfReportData.Metric(2.0D, 2.0D, 10L)))));
|
||||
long position = ChunkPos.asLong(1, 2);
|
||||
queue.prioritize(List.of(new WeakChunkClearJobs.CompletedChunk(Level.OVERWORLD, position)));
|
||||
List<Boolean> deletionFlags = new ArrayList<>();
|
||||
|
||||
PerfChunkEntityScanQueue.ScanBatch batch = queue.scan((key, deletion) -> {
|
||||
deletionFlags.add(deletion);
|
||||
WeakChunkSnapshot.ChunkEntry tracked = new WeakChunkSnapshot.ChunkEntry(
|
||||
1, 2, 200, Map.of(COW, 200));
|
||||
return new PerfChunkEntityScanQueue.ScanValue(summary(key, 250), tracked);
|
||||
});
|
||||
|
||||
assertEquals(1, batch.scannedChunks());
|
||||
assertEquals(List.of(true), deletionFlags);
|
||||
assertEquals(1, batch.trackedCorrections().size());
|
||||
assertEquals(250, batch.reportUpdate().chunks().getFirst().entityCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void aNewReportDropsStaleOrdinaryTasksButReusesCachedCounts() {
|
||||
var queue = new PerfChunkEntityScanQueue();
|
||||
PerfReportData.Metric red = new PerfReportData.Metric(2.0D, 2.0D, 10L);
|
||||
queue.accept(report(20L, List.of(chunk(1, 2, 7, 0, red))));
|
||||
queue.scan((key, deletion) ->
|
||||
new PerfChunkEntityScanQueue.ScanValue(summary(key, 300), null));
|
||||
|
||||
PerfReportData cached = queue.accept(report(40L, List.of(chunk(1, 2, 8, 0, red))));
|
||||
queue.accept(report(60L, List.of(chunk(3, 4, 9, 0, red))));
|
||||
List<PerfChunkEntityScanQueue.ChunkKey> scanned = new ArrayList<>();
|
||||
queue.scan((key, deletion) -> {
|
||||
scanned.add(key);
|
||||
return new PerfChunkEntityScanQueue.ScanValue(summary(key, 400), null);
|
||||
});
|
||||
|
||||
assertEquals(300, cached.chunks().getFirst().entityCount());
|
||||
assertEquals(1, scanned.size());
|
||||
assertEquals(ChunkPos.asLong(3, 4), scanned.getFirst().chunkPos());
|
||||
}
|
||||
|
||||
@Test
|
||||
void redThresholdDoesNotIncludeAnOrangeChunk() {
|
||||
var queue = new PerfChunkEntityScanQueue();
|
||||
queue.accept(report(20L, List.of(chunk(1, 2, 300, 0,
|
||||
new PerfReportData.Metric(1.0D, 1.0D, 10L)))));
|
||||
|
||||
PerfChunkEntityScanQueue.ScanBatch batch = queue.scan((key, deletion) -> {
|
||||
throw new AssertionError("Orange chunks must not be scanned");
|
||||
});
|
||||
|
||||
assertEquals(0, batch.scannedChunks());
|
||||
assertNull(batch.reportUpdate());
|
||||
}
|
||||
|
||||
@Test
|
||||
void drainsEveryRedChunkEvenWhenTheQueueExceedsTheBaseBudget() {
|
||||
var queue = new PerfChunkEntityScanQueue();
|
||||
PerfReportData.Metric red = new PerfReportData.Metric(2.0D, 2.0D, 10L);
|
||||
List<PerfReportData.ChunkEntry> chunks = new ArrayList<>();
|
||||
for (int index = 0; index < 70; index++) {
|
||||
chunks.add(chunk(index, 0, 200, 0, red));
|
||||
}
|
||||
queue.accept(report(20L, chunks));
|
||||
|
||||
PerfChunkEntityScanQueue.ScanBatch batch = queue.scan((key, deletion) ->
|
||||
new PerfChunkEntityScanQueue.ScanValue(summary(key, 200), null));
|
||||
|
||||
assertEquals(70, batch.scannedChunks());
|
||||
assertEquals(0, WeakChunkDelivery.remainingWeakBudget(batch.scannedChunks()));
|
||||
}
|
||||
|
||||
private static PerfReportData report(long tickCount, List<PerfReportData.ChunkEntry> chunks) {
|
||||
return new PerfReportData(7L, 1.0D, 1_000L, tickCount * 50L, tickCount, 100L, 1_000,
|
||||
0, 0, 0, 0, chunks, List.of(), List.of(), List.of());
|
||||
}
|
||||
|
||||
private static PerfReportData.ChunkEntry chunk(
|
||||
int x, int z, int entities, int blockEntities, PerfReportData.Metric metric) {
|
||||
var sampled = new PerfReportData.ObjectCount(
|
||||
"Cow", COW.id(), TickDetailKind.ENTITY, entities, x * 16, 64, z * 16);
|
||||
return new PerfReportData.ChunkEntry(Level.OVERWORLD.location().toString(), x, z,
|
||||
entities, blockEntities, List.of(sampled), List.of(), metric);
|
||||
}
|
||||
|
||||
private static ChunkEntityCollector.ChunkEntitySummary summary(
|
||||
PerfChunkEntityScanQueue.ChunkKey key, int count) {
|
||||
ChunkPos position = new ChunkPos(key.chunkPos());
|
||||
var estimate = new ChunkEntityCollector.TypeEstimate(
|
||||
count, position.getMiddleBlockX(), 64, position.getMiddleBlockZ());
|
||||
return new ChunkEntityCollector.ChunkEntitySummary(
|
||||
position.x, position.z, count, Map.of(COW, estimate));
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,22 @@ class PerfReportDeliveryTest {
|
||||
assertEquals(Set.of(screen, both), subscriptions.recipients());
|
||||
}
|
||||
|
||||
@Test
|
||||
void revokedMapPermissionDropsOnlyTheMapSubscription() {
|
||||
UUID mapOnly = UUID.randomUUID();
|
||||
UUID screenAndMap = UUID.randomUUID();
|
||||
var subscriptions = new PerfReportDelivery.ViewerSubscriptions();
|
||||
subscriptions.setMap(mapOnly, true);
|
||||
subscriptions.openScreen(screenAndMap);
|
||||
subscriptions.setMap(screenAndMap, true);
|
||||
|
||||
assertFalse(subscriptions.retainPermittedLiveViewer(mapOnly, false));
|
||||
assertFalse(subscriptions.isMapViewer(mapOnly));
|
||||
assertTrue(subscriptions.retainPermittedLiveViewer(screenAndMap, false));
|
||||
assertFalse(subscriptions.isMapViewer(screenAndMap));
|
||||
assertTrue(subscriptions.isScreenViewer(screenAndMap));
|
||||
}
|
||||
|
||||
private static PerfReportData reportAtTick(long tickCount) {
|
||||
return new PerfReportData(10.0D, 1_000L, 123_000L, tickCount, 10L, 1_000,
|
||||
0, 0, 0, java.util.List.of(), java.util.List.of(), java.util.List.of());
|
||||
|
||||
@@ -13,6 +13,14 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class WeakChunkDeliveryTest {
|
||||
@Test
|
||||
void priorityScansConsumeOnlyTheWeakBudgetAndMayExceedTheBase() {
|
||||
assertEquals(64, WeakChunkDelivery.remainingWeakBudget(0));
|
||||
assertEquals(1, WeakChunkDelivery.remainingWeakBudget(63));
|
||||
assertEquals(0, WeakChunkDelivery.remainingWeakBudget(64));
|
||||
assertEquals(0, WeakChunkDelivery.remainingWeakBudget(100));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendsOnlyToSubscribedPermittedPlayersStillInSnapshotDimension() {
|
||||
assertTrue(WeakChunkDelivery.shouldSend(
|
||||
@@ -46,15 +54,29 @@ class WeakChunkDeliveryTest {
|
||||
cache.complete(0L, 10L);
|
||||
cache.mergeCurrent(List.of(chunk(1, 2, 60)));
|
||||
|
||||
cache.correct(ChunkPos.asLong(1, 2), chunk(1, 2, 80));
|
||||
assertTrue(cache.correct(ChunkPos.asLong(1, 2), chunk(1, 2, 80)));
|
||||
assertEquals(List.of(chunk(1, 2, 80)), cache.completedEntries());
|
||||
assertEquals(List.of(chunk(1, 2, 80)), cache.currentEntries());
|
||||
|
||||
cache.correct(ChunkPos.asLong(1, 2), null);
|
||||
assertTrue(cache.correct(ChunkPos.asLong(1, 2), null));
|
||||
assertTrue(cache.completedEntries().isEmpty());
|
||||
assertTrue(cache.currentEntries().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void correctionReportsOnlyActualCacheChanges() {
|
||||
var cache = new WeakChunkDelivery.SweepCache();
|
||||
long position = ChunkPos.asLong(1, 2);
|
||||
WeakChunkSnapshot.ChunkEntry entry = chunk(1, 2, 50);
|
||||
|
||||
assertFalse(cache.correct(position, null));
|
||||
assertTrue(cache.correct(position, entry));
|
||||
assertFalse(cache.correct(position, entry));
|
||||
assertTrue(cache.correct(position, chunk(1, 2, 60)));
|
||||
assertTrue(cache.correct(position, null));
|
||||
assertFalse(cache.correct(position, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyStorageDiscardsPartialSweepBeforeCompletion() {
|
||||
var cache = new WeakChunkDelivery.SweepCache();
|
||||
|
||||
Reference in new issue
Block a user