FastAsyncWorldEdit

FastAsyncWorldEdit

152k Downloads

EditSession#replaceBlocks + adaptWorld slower when async

Anarchick opened this issue ยท 6 comments

commented

Server Implementation

Paper

Server Version

1.20.4

Describe the bug

Context : I'm creating a world generator doing a large amount of operation and I tried to optimized the render time. I had the idea to execute some task of a large region into smaller region with their own thread but I was surprised to get the same render times without this method. After more test I have conclude that some FAWE operation are slower in other async thread (and almost instant when you did it via command).

When you run an async task from Bukkit sheduler :

  • EditSession#setBlocks(Region region, Pattern pattern) is faster
  • AdaptWorld is slower
  • EditSession#replaceBlocks(Region region, Mask mask, Pattern pattern) is slower

test async: false, replace: false
adapt world : 2ms
create edit session : 4ms
create region : 7ms
create mask : 15ms
create pattern : 17ms
operation : 356ms

test async: true, replace: false
adapt world : 39ms
create edit session : 39ms
create region : 40ms
create mask : 43ms
create pattern : 44ms
operation : 196ms

test async: false, replace: true
adapt world : 0ms
create edit session : 0ms
create region : 1ms
create mask : 5ms
create pattern : 5ms
operation : 549ms

test async: true, replace: true
adapt world : 40ms
create edit session : 40ms
create region : 41ms
create mask : 44ms
create pattern : 45ms
operation : 26164ms

To Reproduce

public static void test(Location loc, boolean isAsync, boolean isReplace) {
        PlayerUtils.broadcastMessage(String.format("<yellow>test async: %s, replace: %s", isAsync, isReplace));
        System.out.println("test async: " + isAsync + ", replace: " + isReplace);

        long start = System.currentTimeMillis();
        Runnable runnable = () -> {

            World world = BukkitAdapter.adapt(loc.getWorld());
            PlayerUtils.broadcastMessage("adapt world : " + (System.currentTimeMillis() - start) + "ms");
            System.out.println("adapt world : " + (System.currentTimeMillis() - start) + "ms");

            EditSession session = WorldEdit.getInstance().newEditSessionBuilder()
                    .world(world)
                    .checkMemory(false)
                    .fastMode(true)
                    .changeSetNull()
                    .allowedRegionsEverywhere()
                    .relightMode(RelightMode.NONE)
                    .limitUnlimited()
                    .build();
            PlayerUtils.broadcastMessage("create edit session : " + (System.currentTimeMillis() - start) + "ms");
            System.out.println("create edit session : " + (System.currentTimeMillis() - start) + "ms");

            //BlockVector3 pos2 = BlockVector3.at(loc.getBlockX(), loc.getBlockY() -1, loc.getBlockZ());
            //BlockVector3 pos1 = pos2.subtract(150, 150, 150);
            int r = 25;
            BlockVector3 pos2 = BlockVector3.at(loc.getBlockX() + r, loc.getBlockY() -1, loc.getBlockZ() + r);
            BlockVector3 pos1 = pos2.subtract(r*2, r*2, r*2);
            Region region = new CuboidRegion(world, pos1, pos2);
            PlayerUtils.broadcastMessage("create region : " + (System.currentTimeMillis() - start) + "ms");
            System.out.println("create region : " + (System.currentTimeMillis() - start) + "ms");

            Mask mask = toMask(">##wool,glass", session);
            PlayerUtils.broadcastMessage("create mask : " + (System.currentTimeMillis() - start) + "ms");
            System.out.println("create mask : " + (System.currentTimeMillis() - start) + "ms");

            Pattern pattern = toPattern("##wool", session);
            PlayerUtils.broadcastMessage("create pattern : " + (System.currentTimeMillis() - start) + "ms");
            System.out.println("create pattern : " + (System.currentTimeMillis() - start) + "ms");

            if (isReplace) {
                // This is slowest when async
                session.replaceBlocks(region, mask, pattern);
            } else {
                // this is fastest when async
                session.setBlocks(region, pattern);
            }

            //session.flushQueue();
            session.close();
            PlayerUtils.broadcastMessage("operation : " + (System.currentTimeMillis() - start) + "ms");
            System.out.println("operation : " + (System.currentTimeMillis() - start) + "ms");

        };

        if (isAsync) {
            Bukkit.getScheduler().runTaskAsynchronously(INSTANCE, runnable);
        } else {
            runnable.run();
        }
    }

    @Nullable
    public static Mask toMask(final String input, final EditSession session) {
        if (input.isEmpty()) return null;
        MaskFactory maskFactory = WorldEdit.getInstance().getMaskFactory();
        //LocalSession local = new LocalSession();
        ParserContext context = toContext(session);

        //local.remember(session);

        try {
            return maskFactory.parseFromInput(input, context);
        } catch (InputParseException ignored) {}
        return null;
    }

    @Nullable
    public static Pattern toPattern(final String input, final EditSession session) {
        PatternFactory patternFactory = WorldEdit.getInstance().getPatternFactory();
        ParserContext context = toContext(session);
        try {
            return patternFactory.parseFromInput(input, context);
        } catch(InputParseException ignored) {}
        return null;
    }

    public static ParserContext toContext(final EditSession session) {
        ParserContext context = new ParserContext();
        context.setActor(BukkitAdapter.adapt(Bukkit.getConsoleSender()));
        context.setWorld(session.getWorld());
        context.setSession(new LocalSession());
        context.setExtent(session.getWorld());
        return context;
    }

Expected behaviour

test(loc, true, true) should run as fast as //replace >##wool,glass ##wool wich is almost instant.

  • Split a huge operation into some threads (limited to the amount of logical Thread availaible by the CPU) should be faster than doing this on 1 Thread

Screenshots / Videos

No response

Error log (if applicable)

No response

Fawe Debugpaste

https://athion.net/ISPaster/paste/view/76191c6340b840a789f18f4430e62daf

Fawe Version

FastAsyncWorldEdit version 2.11.1-SNAPSHOT-858;ddacb97

Checklist

Anything else?

Tell me if I'm wrong, but I have read that FAWE should not be executed in main Thread, that's why I use Bukkit.getScheduler().runTaskAsynchronously or Executors.newFixedThreadPool(threads)

Note : I have not test other operations

Intel Core I7 8750H
2x8GB Ram 3600MHz DDR4 CAS17 in Dual Channel
SSD M.2 Pcie3.0
I'm not using 100% of my CPU or Ram when I did all this tests

commented

Are you sure the much of the extra time isn't waiting for the bukkit scheduler to run the task?

When setting blocks FAWE doesn't have to read from the world when setting so can just do the operation. When replacing, it must read from the world so relies on loading the chunks, relying on synchronous tasks to do so. If the area isn't loaded this can be slow (i.e. a player is or is not in the area).

commented

After more test :

  • Yes it was not the World Adapter but the Thread call wich cost 16-50ms
  • All of the chunks are loaded around my player

I have modified the code for this ;

            int r = 25;
            BlockVector3 pos2 = BlockVector3.at(loc.getBlockX() + r, loc.getBlockY() -1, loc.getBlockZ() + r);
            BlockVector3 pos1 = pos2.subtract(r*2, r*2, r*2);

With this small cuboid the total time is 28ms on main Thread and 809ms async. This difference is not from Thread creation.

Does FAWE //replace is call on the main Minecraft Thread ?

commented

It would be useful to see a profile of the async replace

commented

On a fresh started server running since ~3minutes , server and client on different pc
#test() is running from the plugin FloatingIsleGenerator , Anapi is only used for the commandBuilder and broadcast Minimessage

using the command /spark profiler start --thread *

test(loc, false, true) 37ms https://spark.lucko.me/6bQ4rN3fh2
test(loc, true, true) 684ms https://spark.lucko.me/dIu6eXoQUZ
//replace ##wool ##wool https://spark.lucko.me/twooQbachI

commented

I think I have found the solution :

new MaskTraverser(mask).setNewExtent(session);
int modified = session.replaceBlocks(region, mask, pattern);

Using MaskTraverser is The most important part that allows to get the same execution time as //replace

I recommend adding a class description and writing it in the Wiki, I don't undestand why this is so important.

commented

I think that this is basically wanted behaviour. FAWE shouldn't be overwriting the extent set to masks that the API uses, and there is not really a good way to assuredly set editsessions when parsing masks, etc. Given the use of parser here, it can only know about what you give it, which in this case is the world. In FAWE itself, the mask extent is replaced with the editsession in relevant commands. This is therefore not a bug and is FAWE doing what it is told