Improving Editor performance¶
Introduction¶
The Editor class has a number of features that can significantly
improve performance, but which are disabled by default because they can
occasionally cause unexpected behavior.
Block Buffering¶
The performance feature with by far the greatest effect is block buffering. When buffering is enabled, blocks no longer get placed immediately, but are instead collected in a buffer until the buffer is full. They are then sent to the GDMC HTTP interface all at once, in a single request.
Buffering can be enabled by setting Editor.buffering to True:
editor.buffering = True
The buffer limit (the number of blocks that are buffered before the buffer is
flushed) can be accessed and modified through Editor.bufferLimit:
# Set buffer limit to 1024
editor.bufferLimit = 1024
If you disable buffering (with editor.buffering = False) or if the program
ends, the buffer is automatically flushed. You can also manually trigger a
buffer flush with Editor.flushBuffer().
Buffering is almost completely transparent: the behavior of your program will
probably not change if you turn it on. For example, block retrieval functions
like Editor.getBlock() take the buffer into account:
from gdpc import Editor, Block
editor = Editor()
editor.buffering = True
editor.placeBlock((1,2,3), Block("red_concrete"))
block = editor.getBlock((1,2,3))
print(block) # Prints "red_concrete" even though the block is still in the buffer.
The only side-effects are:
Block placements become delayed.
If the Minecraft world is changed by something other than the buffering
Editor(such as as a differentEditor, an in-game effect like flowing water or a piston, or an in-game player) after blocks have been buffered but before they have been placed, then the buffered blocks may overwrite those changes.
Because of its transparency and significant performance impact, it is highly recommended to enable buffering for most GDPC programs.
Some more notes on buffering:
If you call
Editor.placeBlock()with an Iterable of positions rather than a single one (as explained in Overview - Building shapes), buffering is automatically temporarily enabled for that call. The same holds for the functions from thegeometrymodule.Editor.runCommand()has an optional parametersyncWithBuffer. If buffering is enabled and this parameter is set toTrue, the command is deferred until after the next buffer flush. This can be useful if the command depends on possibly buffered block changes.
Block caching¶
Another toggleable performance feature is block caching. While buffering is
for speeding up block placement, caching is for speeding up block retrieval.
When caching is enabled, the Editor caches the last Editor.cacheLimit
placed or retrieved blocks and their positions. If a block at a cached position
is later accessed (such as with Editor.getBlock()), the block is retrieved
from the cache instead of the HTTP interface.
Caching can be enabled by setting Editor.caching to True:
editor.caching = True
The cache limit (the maximum number of cached positions) can be
accessed and modified through Editor.cacheLimit:
# Set cache limit to 8192
editor.cacheLimit = 8192
Note that if buffering is enabled, the buffer also acts as a cache (to guarantee transparency). If both buffering and caching are enabled, this “buffering cache” supersedes the “caching cache”.
Just like buffering, caching is mostly transparent. However, caching does have a
slightly more significant side-effect: if the world is changed by something
other than the caching Editor, the cache will not reflect those changes,
and Editor.getBlock() may return incorrect blocks.
Although regular caching can greatly improve performance when the same positions
are accessed multiple times, you do still need to pay the cost for each first
retrieval. If you need to access many blocks in a fixed area, it is recommended
to use a world slice instead. This can however
be simplified with the next Editor feature…
World slice caching¶
As was briefly mentioned in Overview - World slices and
heightmaps, it is also possible to set a world
slice as an Editor cache. If a block or biome is requested whose position is
contained in the world slice cache and which has not been changed since the
world slice was loaded, then the Editor reads it from the world slice instead
of sending a HTTP request.
To load a WorldSlice and use it as a cache, call Editor.loadWorldSlice()
with cache=True:
editor.loadWorldSlice(someRect, cache=True)
World slice caching and regular caching are quite similar, but there are some differences. The regular cache acts as a “sparse cache”, containing only a limited amount of blocks, but having no restrictions on their spatial distance. In contrast, a world slice cache acts is a “dense cache”: it contains all blocks within its bounds, but none outside of it. Furthermore, a world slice cache also works for biomes, while the regular cache does not.
It is possible to use both regular caching and world slice caching at the same time. If a block is available in both caches, the world slice cache takes precedence (because the lookup is faster).
Unlike the regular cache, a world slice cache is not updated if you edit the
world. The Editor instead maintains a boolean array that indicates which
blocks have been changed since the WorldSlice was cached, and it only uses
the cached WorldSlice for unchanged blocks.
World slice caching does have the same side-effect as regular caching: if a
block is changed by something other than the caching Editor, the block is not
marked as invalid in the boolean array, and Editor.getBlock() may therefore
return outdated blocks.
If you load a new world slice with Editor.loadWorldSlice(cache=True), the
stored world slice is replaced and all positions are marked as valid again.
There is a convenience method to load and cache a new world slice for the same
area as the currently cached one: Editor.updateWorldSlice().
If you need direct access to the stored WorldSlice (for example,
to access its heightmaps), it is available as Editor.worldSlice.
For advanced usage, the boolean array that indicates which blocks are valid is
available as Editor.worldSliceDecay.
Multithreaded buffer flushing¶
The Editor class has one more optional performance feature, but this one is a
bit more dangerous: it can use multiple threads to perform buffer flushing. The
speed impact of this feature seems to differ widely between systems. On some
machines, it has no effect at all, while on others, the effect can be
significant.
Using multithreaded buffer flushing with only one worker thread (the default
amount) is relatively safe, but usually does not improve performance much. To
use multithreaded buffer flushing, set Editor.multithreading to True.
Note that buffering must also be enabled.
editor.buffering = True
editor.multithreading = True
If you disable multithreading (with editor.multithreading = False) or if the
program ends, any remaining buffer flush tasks are automatically waited upon.
You can also manually await all pending buffer flushes with
Editor.awaitBufferFlushes(). (You may want to call
Editor.flushBuffer() first.)
It is also possible to use more than one worker thread. On some systems, this
has been shown to improve performance significantly. However, it comes with a
MAJOR side-effect: it will make the Editor unable to guarantee that blocks
will be placed in the same order as they were issued. If buffering or caching is
used, this can also cause the caches to become inconsistent with the actual
world, even if only the multithreading Editor is modifying it. Although the
speedup can be convenient during development, multithreading is therefore NOT
RECOMMENDED for production code. If you enable multithreaded buffer flushing
with more than one worker thread, GDPC will also log a warning.
Regardless, here’s how to enable it:
# Set the amount of worker threads to 4 (causing a warning to be logged).
editor.multithreadingWorkers = 4
Initializing an Editor with performance features enabled¶
Instead of using the properties (e.g. Editor.buffering), you can also
construct an Editor instance with certain features enabled immediately:
editor = Editor(buffering=True, caching=True, cacheLimit=1024)
See the API reference for more details.