From 3b4beba7d6037cb3c2e2a2b6d7c7f2e78381daf2 Mon Sep 17 00:00:00 2001 From: dordsor21 Date: Sat, 24 Jul 2021 15:47:22 +0100 Subject: [PATCH] Improve performance of various commands/actions - Add chunk preloading to RegionVisitor if supplied with a suitable Extent - Where extents are used in masks, set EditSession as the extent as they are otherwise initialised with WorldWrapper that is very slow - Fixes #1073 --- .../java/com/sk89q/worldedit/EditSession.java | 4 +- .../worldedit/command/BiomeCommands.java | 4 +- .../worldedit/command/GenerationCommands.java | 9 +- .../worldedit/command/RegionCommands.java | 34 +++- .../worldedit/command/SelectionCommands.java | 15 +- .../worldedit/command/UtilityCommands.java | 11 ++ .../com/sk89q/worldedit/extent/Extent.java | 8 +- .../worldedit/function/factory/Apply.java | 4 +- .../function/factory/ApplyRegion.java | 4 +- .../function/visitor/RegionVisitor.java | 171 ++++++++++++++++-- 10 files changed, 232 insertions(+), 32 deletions(-) diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/EditSession.java b/worldedit-core/src/main/java/com/sk89q/worldedit/EditSession.java index 9166892c2..1f1c49b0a 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/EditSession.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/EditSession.java @@ -1242,7 +1242,7 @@ public class EditSession extends PassthroughExtent implements AutoCloseable { } } return true; - }); + }, this); Operations.completeBlindly(visitor); return this.changes; } @@ -2783,7 +2783,7 @@ public class EditSession extends PassthroughExtent implements AutoCloseable { } catch (EvaluationException e) { throw new RuntimeException(e); } - }); + }, this); Operations.completeBlindly(visitor); changes += visitor.getAffected(); return changes; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/BiomeCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/BiomeCommands.java index 4b2cb1b88..34dcad7b3 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/BiomeCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/BiomeCommands.java @@ -177,7 +177,9 @@ public class BiomeCommands { if (mask != null) { replace = new RegionMaskingFilter(editSession, mask, replace); } - RegionVisitor visitor = new RegionVisitor(region, replace); + //FAWE start > add extent to RegionVisitor to allow chunk preloading + RegionVisitor visitor = new RegionVisitor(region, replace, editSession); + //FAWE end Operations.completeLegacy(visitor); player.print(Caption.of( diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/GenerationCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/GenerationCommands.java index 1360c7cd6..b6cce437e 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/GenerationCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/GenerationCommands.java @@ -35,6 +35,7 @@ import com.sk89q.worldedit.command.util.annotation.Confirm; import com.sk89q.worldedit.entity.Player; import com.sk89q.worldedit.extension.platform.Actor; import com.fastasyncworldedit.core.function.generator.CavesGen; +import com.sk89q.worldedit.function.mask.AbstractExtentMask; import com.sk89q.worldedit.function.mask.Mask; import com.sk89q.worldedit.function.operation.Operations; import com.sk89q.worldedit.function.pattern.Pattern; @@ -467,6 +468,9 @@ public class GenerationCommands { @Logging(PLACEMENT) @Confirm(Confirm.Processor.REGION) public void ores(Actor actor, LocalSession session, EditSession editSession, @Selection Region region, @Arg(desc = "Mask") Mask mask) throws WorldEditException { + if (mask instanceof AbstractExtentMask) { + ((AbstractExtentMask) mask).setExtent(editSession); + } editSession.addOres(region, mask); actor.print(Caption.of("fawe.worldedit.visitor.visitor.block", editSession.getBlockChangeCount())); } @@ -516,7 +520,7 @@ public class GenerationCommands { e.printStackTrace(); } return false; - }); + }, editSession); Operations.completeBlindly(visitor); actor.print(Caption.of("fawe.worldedit.visitor.visitor.block", editSession.getBlockChangeCount())); } @@ -536,6 +540,9 @@ public class GenerationCommands { @Arg(desc = "Ore vein rarity (% chance each attempt is placed)", def = "100") @Range(from = 0, to = 100) int rarity, @Arg(desc = "Ore vein min y", def = "0") @Range(from = 0, to = 255) int minY, @Arg(desc = "Ore vein max y", def = "63") @Range(from = 0, to = 255) int maxY) throws WorldEditException { + if (mask instanceof AbstractExtentMask) { + ((AbstractExtentMask) mask).setExtent(editSession); + } editSession.addOre(region, mask, material, size, freq, rarity, minY, maxY); actor.print(Caption.of("fawe.worldedit.visitor.visitor.block", editSession.getBlockChangeCount())); } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/RegionCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/RegionCommands.java index 29ca4ce88..5f403df64 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/RegionCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/RegionCommands.java @@ -299,6 +299,10 @@ public class RegionCommands { Pattern to) throws WorldEditException { if (from == null) { from = new ExistingBlockMask(editSession); + //FAWE start > the mask will have been initialised with a WorldWrapper extent (very bad/slow + } else if (from instanceof AbstractExtentMask) { + ((AbstractExtentMask) from).setExtent(editSession); + //FAWE end } if (from instanceof AbstractExtentMask) { ((AbstractExtentMask) from).setExtent(editSession); @@ -422,6 +426,11 @@ public class RegionCommands { Mask mask, @Switch(name = 's', desc = "The flag makes it only consider snow") boolean snow) throws WorldEditException { + //FAWE start > the mask will have been initialised with a WorldWrapper extent (very bad/slow) + if (mask instanceof AbstractExtentMask) { + ((AbstractExtentMask) mask).setExtent(editSession); + } + //FAWE end BlockVector3 min = region.getMinimumPoint(); BlockVector3 max = region.getMaximumPoint(); long volume = (((long) max.getX() - (long) min.getX() + 1) * ((long) max.getY() - (long) min.getY() + 1) * ((long) max.getZ() - (long) min.getZ() + 1)); @@ -502,7 +511,11 @@ public class RegionCommands { @ArgFlag(name = 'm', desc = "Set the include mask, non-matching blocks become air") Mask mask) throws WorldEditException { checkCommandArgument(count >= 1, "Count must be >= 1"); - + //FAWE start > the mask will have been initialised with a WorldWrapper extent (very bad/slow) + if (mask instanceof AbstractExtentMask) { + ((AbstractExtentMask) mask).setExtent(editSession); + } + //FAWE end Mask combinedMask; if (ignoreAirBlocks) { if (mask == null) { @@ -573,7 +586,11 @@ public class RegionCommands { boolean copyBiomes, @ArgFlag(name = 'm', desc = "Set the include mask, non-matching blocks become air") Mask mask) throws WorldEditException { - + //FAWE start > the mask will have been initialised with a WorldWrapper extent (very bad/slow) + if (mask instanceof AbstractExtentMask) { + ((AbstractExtentMask) mask).setExtent(editSession); + } + //FAWE end Mask combinedMask; if (ignoreAirBlocks) { if (mask == null) { @@ -731,7 +748,18 @@ public class RegionCommands { @ArgFlag(name = 'm', desc = "Mask to hollow with") Mask mask) throws WorldEditException { checkCommandArgument(thickness >= 0, "Thickness must be >= 0"); - Mask finalMask = mask == null ? new SolidBlockMask(editSession) : mask; + + //FAWE start > the mask will have been initialised with a WorldWrapper extent (very bad/slow) + Mask finalMask; + if (mask != null) { + if (mask instanceof AbstractExtentMask) { + ((AbstractExtentMask) mask).setExtent(editSession); + } + finalMask = mask; + } else { + finalMask = new SolidBlockMask(editSession); + } + //FAWE end int affected = editSession.hollowOutRegion(region, thickness, pattern, finalMask); actor.print(Caption.of("worldedit.hollow.changed", TextComponent.of(affected))); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SelectionCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SelectionCommands.java index 07f7b092e..3151f9cbf 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SelectionCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SelectionCommands.java @@ -42,6 +42,7 @@ import com.sk89q.worldedit.extension.platform.Locatable; import com.sk89q.worldedit.extension.platform.permission.ActorSelectorLimits; import com.sk89q.worldedit.extent.clipboard.Clipboard; import com.sk89q.worldedit.function.block.BlockDistributionCounter; +import com.sk89q.worldedit.function.mask.AbstractExtentMask; import com.sk89q.worldedit.function.mask.Mask; import com.sk89q.worldedit.function.operation.Operations; import com.sk89q.worldedit.function.visitor.RegionVisitor; @@ -534,6 +535,11 @@ public class SelectionCommands { public int count(Actor actor, World world, LocalSession session, EditSession editSession, @Arg(desc = "The mask of blocks to match") Mask mask) throws WorldEditException { + //FAWE start > the mask will have been initialised with a WorldWrapper extent (very bad/slow) + if (mask instanceof AbstractExtentMask) { + ((AbstractExtentMask) mask).setExtent(editSession); + } + //FAWE end int count = editSession.countBlocks(session.getSelection(world), mask); actor.print(Caption.of("worldedit.count.counted", TextComponent.of(count))); return count; @@ -545,6 +551,9 @@ public class SelectionCommands { ) @CommandPermissions("worldedit.analysis.distr") public void distr(Actor actor, World world, LocalSession session, + //FAWE start > add extent to RegionVisitor to allow chunk preloading + EditSession editSession, + //FAWE end @Switch(name = 'c', desc = "Get the distribution of the clipboard instead") boolean clipboardDistr, @Switch(name = 'd', desc = "Separate blocks by state") @@ -557,13 +566,13 @@ public class SelectionCommands { if (clipboardDistr) { Clipboard clipboard = session.getClipboard().getClipboard(); // throws if missing BlockDistributionCounter count = new BlockDistributionCounter(clipboard, separateStates); - RegionVisitor visitor = new RegionVisitor(clipboard.getRegion(), count); + //FAWE start > add extent to RegionVisitor to allow chunk preloading + RegionVisitor visitor = new RegionVisitor(clipboard.getRegion(), count, editSession); + //FAWE end Operations.completeBlindly(visitor); distribution = count.getDistribution(); } else { - try (EditSession editSession = session.createEditSession(actor)) { distribution = editSession.getBlockDistribution(session.getSelection(world), separateStates); - } } session.setLastDistribution(distribution); page = 1; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/UtilityCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/UtilityCommands.java index beb60eeaf..f43d5752d 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/UtilityCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/UtilityCommands.java @@ -49,6 +49,7 @@ import com.sk89q.worldedit.extension.platform.Actor; import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormat; import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormats; import com.sk89q.worldedit.function.EntityFunction; +import com.sk89q.worldedit.function.mask.AbstractExtentMask; import com.sk89q.worldedit.function.mask.BlockTypeMask; import com.sk89q.worldedit.function.mask.ExistingBlockMask; import com.sk89q.worldedit.function.mask.Mask; @@ -424,6 +425,11 @@ public class UtilityCommands { Mask mask, @Arg(desc = "The radius of the square to remove from", def = "50") int radius) throws WorldEditException { + //FAWE start > the mask will have been initialised with a WorldWrapper extent (very bad/slow) + if (mask instanceof AbstractExtentMask) { + ((AbstractExtentMask) mask).setExtent(editSession); + } + //FAWE end radius = Math.max(1, radius); we.checkMaxRadius(radius); @@ -446,6 +452,11 @@ public class UtilityCommands { Mask from, @Arg(desc = "The pattern of blocks to replace with") Pattern to) throws WorldEditException { + //FAWE start > the mask will have been initialised with a WorldWrapper extent (very bad/slow) + if (from instanceof AbstractExtentMask) { + ((AbstractExtentMask) from).setExtent(editSession); + } + //FAWE end radius = Math.max(1, radius); we.checkMaxRadius(radius); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/Extent.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/Extent.java index 638d1ec98..9f9841cd0 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/Extent.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/Extent.java @@ -549,7 +549,9 @@ public interface Extent extends InputExtent, OutputExtent { * @return the number of blocks that matched the mask */ default int countBlocks(Region region, Mask searchMask) { - RegionVisitor visitor = new RegionVisitor(region, searchMask::test); + //FAWE start > use slightly more performant RegionVisitor + RegionVisitor visitor = new RegionVisitor(region, searchMask::test, this); + //FAWE end Operations.completeBlindly(visitor); return visitor.getAffected(); } @@ -648,7 +650,9 @@ public interface Extent extends InputExtent, OutputExtent { BlockReplace replace = new BlockReplace(this, pattern); RegionMaskingFilter filter = new RegionMaskingFilter(this, mask, replace); - RegionVisitor visitor = new RegionVisitor(region, filter); + //FAWE start > add extent to RegionVisitor to allow chunk preloading + RegionVisitor visitor = new RegionVisitor(region, filter, this); + //FAWE end Operations.completeLegacy(visitor); return visitor.getAffected(); } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/function/factory/Apply.java b/worldedit-core/src/main/java/com/sk89q/worldedit/function/factory/Apply.java index baee7794a..252476e77 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/function/factory/Apply.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/function/factory/Apply.java @@ -55,7 +55,9 @@ public class Apply implements Contextual { @Override public Operation createFromContext(EditContext context) { - return new RegionVisitor(firstNonNull(context.getRegion(), region), function.createFromContext(context)); + //FAWE start > add extent to RegionVisitor to allow chunk preloading + return new RegionVisitor(firstNonNull(context.getRegion(), region), function.createFromContext(context), context.getDestination()); + //FAWE end } @Override diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/function/factory/ApplyRegion.java b/worldedit-core/src/main/java/com/sk89q/worldedit/function/factory/ApplyRegion.java index 3b51a6578..068cb3321 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/function/factory/ApplyRegion.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/function/factory/ApplyRegion.java @@ -48,7 +48,9 @@ public class ApplyRegion implements Contextual { @Override public Operation createFromContext(EditContext context) { - return new RegionVisitor(firstNonNull(context.getRegion(), region), function.createFromContext(context)); + //FAWE start > add extent to RegionVisitor to allow chunk preloading + return new RegionVisitor(firstNonNull(context.getRegion(), region), function.createFromContext(context), context.getDestination()); + //FAWE end } @Override diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/function/visitor/RegionVisitor.java b/worldedit-core/src/main/java/com/sk89q/worldedit/function/visitor/RegionVisitor.java index 4b819b8bf..7a56467bd 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/function/visitor/RegionVisitor.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/function/visitor/RegionVisitor.java @@ -19,9 +19,19 @@ package com.sk89q.worldedit.function.visitor; +import com.fastasyncworldedit.core.Fawe; import com.fastasyncworldedit.core.configuration.Caption; +import com.fastasyncworldedit.core.configuration.Settings; +import com.fastasyncworldedit.core.internal.exception.FaweException; +import com.fastasyncworldedit.core.queue.implementation.ParallelQueueExtent; +import com.fastasyncworldedit.core.queue.implementation.SingleThreadQueueExtent; +import com.fastasyncworldedit.core.queue.implementation.chunk.ChunkHolder; +import com.fastasyncworldedit.core.util.ExtentTraverser; +import com.fastasyncworldedit.core.util.MemUtil; +import com.fastasyncworldedit.core.util.TaskManager; import com.google.common.collect.ImmutableList; import com.sk89q.worldedit.WorldEditException; +import com.sk89q.worldedit.extent.Extent; import com.sk89q.worldedit.function.RegionFunction; import com.sk89q.worldedit.function.operation.Operation; import com.sk89q.worldedit.function.operation.RunContext; @@ -29,25 +39,45 @@ import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.regions.Region; import com.sk89q.worldedit.util.formatting.text.Component; import com.sk89q.worldedit.util.formatting.text.TextComponent; +import java.util.Iterator; /** * Utility class to apply region functions to {@link com.sk89q.worldedit.regions.Region}. + * * @deprecated - FAWE deprecation: Let the queue iterate, not the region function which lacks any kind of optimizations / parallelism */ -@Deprecated -public class RegionVisitor implements Operation { +@Deprecated public class RegionVisitor implements Operation { + public final Iterable iterable; private final Region region; private final RegionFunction function; private int affected = 0; + private SingleThreadQueueExtent singleQueue; /** * @deprecated Use other constructors which will preload chunks during iteration */ - @Deprecated - public RegionVisitor(Region region, RegionFunction function) { - this.region = region; + @Deprecated public RegionVisitor(Region region, RegionFunction function) { + this(region, function, null); + } + + /** + * Allows for preloading chunks, and non-specific "regions" to be visited. + * + * @param iterable Can be supplied as a region, or a raw iterator + * @param function The function to be applied to each BlockVector3 iterated over + * @param extent Supplied editsession/extent to attempt to extract {@link SingleThreadQueueExtent} from + */ + public RegionVisitor(Iterable iterable, RegionFunction function, Extent extent) { + region = iterable instanceof Region ? (Region) iterable : null; + this.iterable = iterable; this.function = function; + if (extent != null) { + ExtentTraverser queueTraverser = new ExtentTraverser<>(extent).find(ParallelQueueExtent.class); + this.singleQueue = queueTraverser != null ? (SingleThreadQueueExtent) queueTraverser.get().getExtent() : null; + } else { + this.singleQueue = null; + } } /** @@ -59,27 +89,132 @@ public class RegionVisitor implements Operation { return affected; } - @Override - public Operation resume(RunContext run) throws WorldEditException { - for (BlockVector3 pt : region) { - if (function.apply(pt)) { - affected++; + @Override public Operation resume(RunContext run) throws WorldEditException { + //FAWE start > allow chunk preloading + if (singleQueue != null && Settings.IMP.QUEUE.PRELOAD_CHUNKS > 1) { + /* + * The following is done to reduce iteration cost + * - Preload chunks just in time + * - Only check every 16th block for potential chunk loads + * - Stop iteration on exception instead of hasNext + * - Do not calculate the stacktrace as it is expensive + */ + Iterator trailIter = iterable.iterator(); + Iterator leadIter = iterable.iterator(); + int lastTrailChunkX = Integer.MIN_VALUE; + int lastTrailChunkZ = Integer.MIN_VALUE; + int lastLeadChunkX = Integer.MIN_VALUE; + int lastLeadChunkZ = Integer.MIN_VALUE; + int loadingTarget = Settings.IMP.QUEUE.PRELOAD_CHUNKS; + try { + for (; ; ) { + BlockVector3 pt = trailIter.next(); + apply(pt); + int cx = pt.getBlockX() >> 4; + int cz = pt.getBlockZ() >> 4; + if (cx != lastTrailChunkX || cz != lastTrailChunkZ) { + lastTrailChunkX = cx; + lastTrailChunkZ = cz; + int amount; + if (lastLeadChunkX == Integer.MIN_VALUE) { + lastLeadChunkX = cx; + lastLeadChunkZ = cz; + amount = loadingTarget; + } else { + amount = 1; + } + for (int count = 0; count < amount; ) { + BlockVector3 v = leadIter.next(); + int vcx = v.getBlockX() >> 4; + int vcz = v.getBlockZ() >> 4; + if (vcx != lastLeadChunkX || vcz != lastLeadChunkZ) { + lastLeadChunkX = vcx; + lastLeadChunkZ = vcz; + queueChunkLoad(vcx, vcz); + count++; + } + // Skip the next 15 blocks + leadIter.next(); + leadIter.next(); + leadIter.next(); + leadIter.next(); + leadIter.next(); + leadIter.next(); + leadIter.next(); + leadIter.next(); + leadIter.next(); + leadIter.next(); + leadIter.next(); + leadIter.next(); + leadIter.next(); + leadIter.next(); + leadIter.next(); + } + } + apply(trailIter.next()); + apply(trailIter.next()); + apply(trailIter.next()); + apply(trailIter.next()); + apply(trailIter.next()); + apply(trailIter.next()); + apply(trailIter.next()); + apply(trailIter.next()); + apply(trailIter.next()); + apply(trailIter.next()); + apply(trailIter.next()); + apply(trailIter.next()); + apply(trailIter.next()); + apply(trailIter.next()); + apply(trailIter.next()); + } + } catch (FaweException e) { + throw new RuntimeException(e); + } catch (Throwable ignore) { + } + try { + for (; ; ) { + apply(trailIter.next()); + apply(trailIter.next()); + } + } catch (FaweException e) { + throw new RuntimeException(e); + } catch (Throwable ignore) { + } + } else { + for (BlockVector3 pt : region) { + apply(pt); } } + //FAWE end return null; } - @Override - public void cancel() { + //FAWE start > extract methods for slightly clean resume method + private void apply(BlockVector3 pt) throws WorldEditException { + if (function.apply(pt)) { + affected++; + } } - @Override - public Iterable getStatusMessages() { - return ImmutableList.of(Caption.of( - "worldedit.operation.affected.block", - TextComponent.of(getAffected()) - )); + private void queueChunkLoad(int cx, int cz) { + TaskManager.IMP.sync(() -> { + boolean lowMem = MemUtil.isMemoryLimited(); + if (!singleQueue.isQueueEnabled() || (!(lowMem && singleQueue.size() > Settings.IMP.QUEUE.PARALLEL_THREADS + 8) + && singleQueue.size() < Settings.IMP.QUEUE.TARGET_SIZE && Fawe.get().getQueueHandler().isUnderutilized())) { + //The GET chunk is what will take longest. + ((ChunkHolder)singleQueue.getOrCreateChunk(cx, cz)).getOrCreateGet(); + } + return null; + }); + } + //FAWE end + + @Override public void cancel() { + } + + @Override public Iterable getStatusMessages() { + return ImmutableList.of(Caption.of("worldedit.operation.affected.block", TextComponent.of(getAffected()))); } }