|
| 1 | +package com.fastasyncworldedit.core.queue.implementation; |
| 2 | + |
| 3 | +import com.fastasyncworldedit.core.Fawe; |
| 4 | +import com.fastasyncworldedit.core.extent.filter.block.ChunkFilterBlock; |
| 5 | +import com.fastasyncworldedit.core.internal.exception.FaweException; |
| 6 | +import com.fastasyncworldedit.core.queue.Filter; |
| 7 | +import com.sk89q.worldedit.internal.util.LogManagerCompat; |
| 8 | +import com.sk89q.worldedit.math.BlockVector3; |
| 9 | +import com.sk89q.worldedit.regions.Region; |
| 10 | +import org.apache.logging.log4j.Logger; |
| 11 | + |
| 12 | +import java.util.Collection; |
| 13 | +import java.util.concurrent.ConcurrentHashMap; |
| 14 | +import java.util.concurrent.ConcurrentMap; |
| 15 | +import java.util.concurrent.ForkJoinTask; |
| 16 | +import java.util.concurrent.RecursiveAction; |
| 17 | + |
| 18 | +class ApplyTask<F extends Filter> extends RecursiveAction implements Runnable { |
| 19 | + |
| 20 | + private static final Logger LOGGER = LogManagerCompat.getLogger(); |
| 21 | + |
| 22 | + private static final int INITIAL_REGION_SHIFT = 5; |
| 23 | + private static final int SHIFT_REDUCTION = 1; |
| 24 | + |
| 25 | + private final CommonState<F> commonState; |
| 26 | + private final ApplyTask<F> before; |
| 27 | + private final int minChunkX; |
| 28 | + private final int minChunkZ; |
| 29 | + private final int maxChunkX; |
| 30 | + private final int maxChunkZ; |
| 31 | + // Note: shift == INITIAL_REGION_SHIFT means we are in the root node. |
| 32 | + // compute() relies on that when triggering postProcess |
| 33 | + private final int shift; |
| 34 | + |
| 35 | + @Override |
| 36 | + public void run() { |
| 37 | + compute(); |
| 38 | + } |
| 39 | + |
| 40 | + private record CommonState<F extends Filter>( |
| 41 | + F originalFilter, |
| 42 | + Region region, |
| 43 | + ParallelQueueExtent parallelQueueExtent, |
| 44 | + ConcurrentMap<Thread, ThreadState<F>> stateCache, |
| 45 | + boolean full, |
| 46 | + boolean[] faweExceptionReasonsUsed |
| 47 | + ) { |
| 48 | + |
| 49 | + } |
| 50 | + |
| 51 | + private static final class ThreadState<F extends Filter> { |
| 52 | + |
| 53 | + private final SingleThreadQueueExtent queue; |
| 54 | + private final F filter; |
| 55 | + private ChunkFilterBlock block; |
| 56 | + |
| 57 | + private ThreadState(SingleThreadQueueExtent queue, F filter) { |
| 58 | + this.queue = queue; |
| 59 | + this.filter = filter; |
| 60 | + } |
| 61 | + |
| 62 | + } |
| 63 | + |
| 64 | + ApplyTask( |
| 65 | + final Region region, |
| 66 | + final F filter, |
| 67 | + final ParallelQueueExtent parallelQueueExtent, |
| 68 | + final boolean full, final boolean[] faweExceptionReasonsUsed |
| 69 | + ) { |
| 70 | + this.commonState = new CommonState<>( |
| 71 | + filter, |
| 72 | + region.clone(), // clone only once, assuming the filter doesn't modify that clone |
| 73 | + parallelQueueExtent, |
| 74 | + new ConcurrentHashMap<>(), |
| 75 | + full, |
| 76 | + faweExceptionReasonsUsed |
| 77 | + ); |
| 78 | + this.before = null; |
| 79 | + final BlockVector3 minimumPoint = region.getMinimumPoint(); |
| 80 | + this.minChunkX = minimumPoint.x() >> 4; |
| 81 | + this.minChunkZ = minimumPoint.z() >> 4; |
| 82 | + final BlockVector3 maximumPoint = region.getMaximumPoint(); |
| 83 | + this.maxChunkX = maximumPoint.x() >> 4; |
| 84 | + this.maxChunkZ = maximumPoint.z() >> 4; |
| 85 | + this.shift = INITIAL_REGION_SHIFT; |
| 86 | + |
| 87 | + } |
| 88 | + |
| 89 | + private ApplyTask( |
| 90 | + final CommonState<F> commonState, |
| 91 | + final ApplyTask<F> before, |
| 92 | + final int minChunkX, |
| 93 | + final int maxChunkX, |
| 94 | + final int minChunkZ, |
| 95 | + final int maxChunkZ, |
| 96 | + final int higherShift |
| 97 | + ) { |
| 98 | + this.commonState = commonState; |
| 99 | + this.minChunkX = minChunkX; |
| 100 | + this.maxChunkX = maxChunkX; |
| 101 | + this.minChunkZ = minChunkZ; |
| 102 | + this.maxChunkZ = maxChunkZ; |
| 103 | + this.before = before; |
| 104 | + this.shift = Math.max(0, higherShift - SHIFT_REDUCTION); |
| 105 | + } |
| 106 | + |
| 107 | + @Override |
| 108 | + protected void compute() { |
| 109 | + if (this.minChunkX != this.maxChunkX || this.minChunkZ != this.maxChunkZ) { |
| 110 | + ApplyTask<F> subtask = null; |
| 111 | + int minRegionX = this.minChunkX >> this.shift; |
| 112 | + int minRegionZ = this.minChunkZ >> this.shift; |
| 113 | + int maxRegionX = this.maxChunkX >> this.shift; |
| 114 | + int maxRegionZ = this.maxChunkZ >> this.shift; |
| 115 | + // This task covers multiple regions. Create one subtask per region |
| 116 | + for (int regionX = minRegionX; regionX <= maxRegionX; regionX++) { |
| 117 | + for (int regionZ = minRegionZ; regionZ <= maxRegionZ; regionZ++) { |
| 118 | + if (shouldProcessDirectly()) { |
| 119 | + // assume we should do a bigger batch of work here - the other threads are busy for a while |
| 120 | + processRegion(regionX, regionZ, this.shift); |
| 121 | + continue; |
| 122 | + } |
| 123 | + if (this.shift == 0 && !this.commonState.region.containsChunk(regionX, regionZ)) { |
| 124 | + // if shift == 0, region coords are chunk coords |
| 125 | + continue; // chunks not intersecting with the region don't need a task |
| 126 | + } |
| 127 | + |
| 128 | + // creating more tasks will likely help parallelism as other threads aren't *that* busy |
| 129 | + subtask = new ApplyTask<>( |
| 130 | + this.commonState, |
| 131 | + subtask, |
| 132 | + regionX << this.shift, |
| 133 | + ((regionX + 1) << this.shift) - 1, |
| 134 | + regionZ << this.shift, |
| 135 | + ((regionZ + 1) << this.shift) - 1, |
| 136 | + this.shift |
| 137 | + ); |
| 138 | + subtask.fork(); |
| 139 | + } |
| 140 | + } |
| 141 | + // try processing tasks in reverse order if not processed already, otherwise "wait" for completion |
| 142 | + while (subtask != null) { |
| 143 | + if (subtask.tryUnfork()) { |
| 144 | + subtask.invoke(); |
| 145 | + } else { |
| 146 | + subtask.join(); |
| 147 | + } |
| 148 | + subtask = subtask.before; |
| 149 | + } |
| 150 | + } else { |
| 151 | + // we reached a task for a single chunk, let's process it |
| 152 | + processChunk(this.minChunkX, this.minChunkZ); |
| 153 | + } |
| 154 | + if (this.shift == INITIAL_REGION_SHIFT) { |
| 155 | + onCompletion(); |
| 156 | + } |
| 157 | + } |
| 158 | + |
| 159 | + private boolean shouldProcessDirectly() { |
| 160 | + return ForkJoinTask.getSurplusQueuedTaskCount() > Math.max(3, 1 << this.shift); |
| 161 | + } |
| 162 | + |
| 163 | + private void processRegion(int regionX, int regionZ, int shift) { |
| 164 | + final ThreadState<F> state = getState(); |
| 165 | + this.commonState.parallelQueueExtent.enter(state.queue); |
| 166 | + try { |
| 167 | + for (int chunkX = regionX << shift; chunkX <= ((regionX + 1) << shift) - 1; chunkX++) { |
| 168 | + for (int chunkZ = regionZ << shift; chunkZ <= ((regionZ + 1) << shift) - 1; chunkZ++) { |
| 169 | + if (!this.commonState.region.containsChunk(chunkX, chunkZ)) { |
| 170 | + continue; // chunks not intersecting with the region must not be processed |
| 171 | + } |
| 172 | + applyChunk(chunkX, chunkZ, state); |
| 173 | + } |
| 174 | + } |
| 175 | + } finally { |
| 176 | + this.commonState.parallelQueueExtent.exit(); |
| 177 | + } |
| 178 | + |
| 179 | + } |
| 180 | + |
| 181 | + @SuppressWarnings("unchecked") |
| 182 | + private ThreadState<F> getState() { |
| 183 | + return this.commonState.stateCache.computeIfAbsent( |
| 184 | + Thread.currentThread(), |
| 185 | + __ -> new ThreadState<>( |
| 186 | + (SingleThreadQueueExtent) this.commonState.parallelQueueExtent.getNewQueue(), |
| 187 | + (F) this.commonState.originalFilter.fork() |
| 188 | + ) |
| 189 | + ); |
| 190 | + } |
| 191 | + |
| 192 | + private void processChunk(int chunkX, int chunkZ) { |
| 193 | + final ThreadState<F> state = getState(); |
| 194 | + this.commonState.parallelQueueExtent.enter(state.queue); |
| 195 | + try { |
| 196 | + applyChunk(chunkX, chunkZ, state); |
| 197 | + } finally { |
| 198 | + this.commonState.parallelQueueExtent.exit(); |
| 199 | + } |
| 200 | + } |
| 201 | + |
| 202 | + private void applyChunk(int chunkX, int chunkZ, ThreadState<F> state) { |
| 203 | + try { |
| 204 | + state.block = state.queue.apply( |
| 205 | + state.block, |
| 206 | + state.filter, |
| 207 | + this.commonState.region, |
| 208 | + chunkX, |
| 209 | + chunkZ, |
| 210 | + this.commonState.full |
| 211 | + ); |
| 212 | + } catch (Throwable t) { |
| 213 | + if (t instanceof FaweException faweException) { |
| 214 | + Fawe.handleFaweException(this.commonState.faweExceptionReasonsUsed, faweException, LOGGER); |
| 215 | + } else if (t.getCause() instanceof FaweException faweException) { |
| 216 | + Fawe.handleFaweException(this.commonState.faweExceptionReasonsUsed, faweException, LOGGER); |
| 217 | + } else { |
| 218 | + throw t; |
| 219 | + } |
| 220 | + } |
| 221 | + } |
| 222 | + |
| 223 | + private void onCompletion() { |
| 224 | + for (ForkJoinTask<?> task : flushQueues()) { |
| 225 | + if (task.tryUnfork()) { |
| 226 | + task.invoke(); |
| 227 | + } else { |
| 228 | + task.join(); |
| 229 | + } |
| 230 | + } |
| 231 | + } |
| 232 | + |
| 233 | + private ForkJoinTask<?>[] flushQueues() { |
| 234 | + final Collection<ThreadState<F>> values = this.commonState.stateCache.values(); |
| 235 | + ForkJoinTask<?>[] tasks = new ForkJoinTask[values.size()]; |
| 236 | + int i = values.size() - 1; |
| 237 | + for (final ThreadState<F> value : values) { |
| 238 | + tasks[i] = ForkJoinTask.adapt(value.queue::flush).fork(); |
| 239 | + i--; |
| 240 | + } |
| 241 | + return tasks; |
| 242 | + } |
| 243 | + |
| 244 | +} |
0 commit comments