Merge pull request #225 from mikeprimm/master

Add support for throttling chunk load rate, splitting tile loads across multiple server ticks if needed
This commit is contained in:
mikeprimm 2011-06-18 17:50:26 -07:00
commit 287d3cbe63
8 changed files with 188 additions and 102 deletions

View File

@ -100,6 +100,9 @@ disable-webserver: false
# Period between tile renders for fullrender, in seconds (non-zero to pace fullrenders, lessen CPU load)
timesliceinterval: 0.0
# Maximum chunk loads per server tick (1/20th of a second) - reducing this below 90 will impact render performance, but also will reduce server thread load
maxchunkspertick: 200
# Interval the browser should poll for updates.
updaterate: 2000

View File

@ -9,10 +9,10 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.scheduler.BukkitScheduler;
@ -25,13 +25,18 @@ import org.dynmap.utils.NewMapChunkCache;
public class MapManager {
public AsynchronousQueue<MapTile> tileQueue;
private static final int DEFAULT_CHUNKS_PER_TICK = 200;
public List<DynmapWorld> worlds = new ArrayList<DynmapWorld>();
public Map<String, DynmapWorld> worldsLookup = new HashMap<String, DynmapWorld>();
private BukkitScheduler scheduler;
private DynmapPlugin plug_in;
private long timeslice_int = 0; /* In milliseconds */
private int max_chunk_loads_per_tick = DEFAULT_CHUNKS_PER_TICK;
/* Which fullrenders are active */
private HashMap<String, FullWorldRenderState> active_renders = new HashMap<String, FullWorldRenderState>();
/* List of MapChunkCache requests to be processed */
private ConcurrentLinkedQueue<MapChunkCache> chunkloads = new ConcurrentLinkedQueue<MapChunkCache>();
/* Tile hash manager */
public TileHashManager hashman;
/* lock for our data structures */
@ -60,10 +65,22 @@ public class MapManager {
public Collection<DynmapWorld> getWorlds() {
return worlds;
}
private static class OurThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
t.setPriority(Thread.MIN_PRIORITY);
t.setName("Dynmap Render Thread");
return t;
}
}
private class DynmapScheduledThreadPoolExecutor extends ScheduledThreadPoolExecutor {
DynmapScheduledThreadPoolExecutor() {
super(POOL_SIZE);
this.setThreadFactory(new OurThreadFactory());
}
protected void afterExecute(Runnable r, Throwable x) {
@ -163,7 +180,7 @@ public class MapManager {
}
World w = world.world;
/* Fetch chunk cache from server thread */
DynmapChunk[] requiredChunks = tile.getMap().getRequiredChunks(tile);
List<DynmapChunk> requiredChunks = tile.getMap().getRequiredChunks(tile);
MapChunkCache cache = createMapChunkCache(world, requiredChunks);
if(cache == null) {
cleanup();
@ -222,6 +239,25 @@ public class MapManager {
}
}
private class ProcessChunkLoads implements Runnable {
public void run() {
int cnt = max_chunk_loads_per_tick;
while(cnt > 0) {
MapChunkCache c = chunkloads.peek();
if(c == null)
return;
cnt = cnt - c.loadChunks(cnt);
if(c.isDoneLoading()) {
chunkloads.poll();
synchronized(c) {
c.notify();
}
}
}
}
}
public MapManager(DynmapPlugin plugin, ConfigurationNode configuration) {
plug_in = plugin;
mapman = this;
@ -235,7 +271,8 @@ public class MapManager {
/* On dedicated thread, so default to no delays */
timeslice_int = (long)(configuration.getDouble("timesliceinterval", 0.0) * 1000);
max_chunk_loads_per_tick = configuration.getInteger("maxchunkspertick", DEFAULT_CHUNKS_PER_TICK);
if(max_chunk_loads_per_tick < 5) max_chunk_loads_per_tick = 5;
scheduler = plugin.getServer().getScheduler();
hashman = new TileHashManager(DynmapPlugin.tilesDirectory, configuration.getBoolean("enabletilehash", true));
@ -247,7 +284,7 @@ public class MapManager {
}
scheduler.scheduleSyncRepeatingTask(plugin, new CheckWorldTimes(), 5*20, 5*20); /* Check very 5 seconds */
scheduler.scheduleSyncRepeatingTask(plugin, new ProcessChunkLoads(), 1, 1); /* Chunk loader task */
}
void renderFullWorld(Location l, CommandSender sender) {
@ -439,35 +476,32 @@ public class MapManager {
/**
* Render processor helper - used by code running on render threads to request chunk snapshot cache from server/sync thread
*/
public MapChunkCache createMapChunkCache(final DynmapWorld w, final DynmapChunk[] chunks) {
Callable<MapChunkCache> job = new Callable<MapChunkCache>() {
public MapChunkCache call() {
MapChunkCache c = null;
try {
if(!use_legacy)
c = new NewMapChunkCache();
} catch (NoClassDefFoundError ncdfe) {
use_legacy = true;
}
if(c == null)
c = new LegacyMapChunkCache();
if(w.visibility_limits != null) {
for(MapChunkCache.VisibilityLimit limit: w.visibility_limits) {
c.setVisibleRange(limit);
}
c.setHiddenFillStyle(w.hiddenchunkstyle);
}
c.loadChunks(w.world, chunks);
return c;
}
};
Future<MapChunkCache> rslt = scheduler.callSyncMethod(plug_in, job);
public MapChunkCache createMapChunkCache(final DynmapWorld w, final List<DynmapChunk> chunks) {
MapChunkCache c = null;
try {
return rslt.get();
} catch (Exception x) {
Log.info("createMapChunk - " + x);
return null;
if(!use_legacy)
c = new NewMapChunkCache();
} catch (NoClassDefFoundError ncdfe) {
use_legacy = true;
}
if(c == null)
c = new LegacyMapChunkCache();
if(w.visibility_limits != null) {
for(MapChunkCache.VisibilityLimit limit: w.visibility_limits) {
c.setVisibleRange(limit);
}
c.setHiddenFillStyle(w.hiddenchunkstyle);
}
c.setChunks(w.world, chunks);
synchronized(c) {
chunkloads.add(c);
try {
c.wait();
} catch (InterruptedException ix) {
return null;
}
}
return c;
}
/**
* Update map tile statistics

View File

@ -1,6 +1,7 @@
package org.dynmap;
import java.io.File;
import java.util.List;
import org.bukkit.Location;
import org.dynmap.utils.MapChunkCache;
@ -13,7 +14,7 @@ public abstract class MapType {
public abstract MapTile[] getAdjecentTiles(MapTile tile);
public abstract DynmapChunk[] getRequiredChunks(MapTile tile);
public abstract List<DynmapChunk> getRequiredChunks(MapTile tile);
public abstract boolean render(MapChunkCache cache, MapTile tile, File outputFile);

View File

@ -5,6 +5,8 @@ import static org.dynmap.JSONUtils.a;
import static org.dynmap.JSONUtils.s;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.imageio.ImageIO;
@ -101,18 +103,16 @@ public class FlatMap extends MapType {
}
@Override
public DynmapChunk[] getRequiredChunks(MapTile tile) {
public List<DynmapChunk> getRequiredChunks(MapTile tile) {
FlatMapTile t = (FlatMapTile) tile;
int chunksPerTile = t.size / 16;
int sx = t.x * chunksPerTile;
int sz = t.y * chunksPerTile;
DynmapChunk[] result = new DynmapChunk[chunksPerTile * chunksPerTile];
int index = 0;
ArrayList<DynmapChunk> result = new ArrayList<DynmapChunk>(chunksPerTile * chunksPerTile);
for (int x = 0; x < chunksPerTile; x++)
for (int z = 0; z < chunksPerTile; z++) {
result[index] = new DynmapChunk(sx + x, sz + z);
index++;
result.add(new DynmapChunk(sx + x, sz + z));
}
return result;
}

View File

@ -164,7 +164,7 @@ public class KzedMap extends MapType {
return false;
}
@Override
public DynmapChunk[] getRequiredChunks(MapTile tile) {
public List<DynmapChunk> getRequiredChunks(MapTile tile) {
if (tile instanceof KzedMapTile) {
KzedMapTile t = (KzedMapTile) tile;
@ -216,12 +216,9 @@ public class KzedMap extends MapType {
chunks.add(chunk);
}
}
DynmapChunk[] result = new DynmapChunk[chunks.size()];
chunks.toArray(result);
return result;
return chunks;
} else {
return new DynmapChunk[0];
return new ArrayList<DynmapChunk>();
}
}

View File

@ -2,6 +2,7 @@ package org.dynmap.utils;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.ListIterator;
import java.lang.reflect.Field;
import org.bukkit.World;
import org.bukkit.Chunk;
@ -20,7 +21,11 @@ public class LegacyMapChunkCache implements MapChunkCache {
private static Method poppreservedchunk = null;
private static Field heightmap = null;
private static boolean initialized = false;
private World w;
private List<DynmapChunk> chunks;
private ListIterator<DynmapChunk> iterator;
private int x_min, x_max, z_min, z_max;
private int x_dim;
private HiddenChunkStyle hidestyle = HiddenChunkStyle.FILL_AIR;
@ -177,17 +182,15 @@ public class LegacyMapChunkCache implements MapChunkCache {
public LegacyMapChunkCache() {
}
/**
* Create chunk cache container
* @param w - world
* @param x_min - minimum chunk x coordinate
* @param z_min - minimum chunk z coordinate
* @param x_max - maximum chunk x coordinate
* @param z_max - maximum chunk z coordinate
* Set chunks to load, and world to load from
*/
@SuppressWarnings({ "unchecked" })
public void loadChunks(World w, DynmapChunk[] chunks) {
@SuppressWarnings({ "rawtypes", "unchecked" })
public void setChunks(World w, List<DynmapChunk> chunks) {
this.w = w;
this.chunks = chunks;
/* Compute range */
if(chunks.length == 0) {
if(chunks.size() == 0) {
this.x_min = 0;
this.x_max = 0;
this.z_min = 0;
@ -195,17 +198,17 @@ public class LegacyMapChunkCache implements MapChunkCache {
x_dim = 1;
}
else {
x_min = x_max = chunks[0].x;
z_min = z_max = chunks[0].z;
for(int i = 1; i < chunks.length; i++) {
if(chunks[i].x > x_max)
x_max = chunks[i].x;
if(chunks[i].x < x_min)
x_min = chunks[i].x;
if(chunks[i].z > z_max)
z_max = chunks[i].z;
if(chunks[i].z < z_min)
z_min = chunks[i].z;
x_min = x_max = chunks.get(0).x;
z_min = z_max = chunks.get(0).z;
for(DynmapChunk c : chunks) {
if(c.x > x_max)
x_max = c.x;
if(c.x < x_min)
x_min = c.x;
if(c.z > z_max)
z_max = c.z;
if(c.z < z_min)
z_min = c.z;
}
x_dim = x_max - x_min + 1;
}
@ -238,9 +241,17 @@ public class LegacyMapChunkCache implements MapChunkCache {
}
}
snaparray = new LegacyChunkSnapshot[x_dim * (z_max-z_min+1)];
if(gethandle != null) {
// Load the required chunks.
for (DynmapChunk chunk : chunks) {
}
public int loadChunks(int max_to_load) {
int cnt = 0;
if(iterator == null)
iterator = chunks.listIterator();
// Load the required chunks.
while((cnt < max_to_load) && iterator.hasNext()) {
DynmapChunk chunk = iterator.next();
if(gethandle != null) {
boolean vis = true;
if(visible_limits != null) {
vis = false;
@ -302,12 +313,25 @@ public class LegacyMapChunkCache implements MapChunkCache {
}
}
}
cnt++;
}
/* Fill missing chunks with empty dummy chunk */
for(int i = 0; i < snaparray.length; i++) {
if(snaparray[i] == null)
snaparray[i] = EMPTY;
/* If done, finish table */
if(iterator.hasNext() == false) {
/* Fill missing chunks with empty dummy chunk */
for(int i = 0; i < snaparray.length; i++) {
if(snaparray[i] == null)
snaparray[i] = EMPTY;
}
}
return cnt;
}
/**
* Test if done loading
*/
public boolean isDoneLoading() {
if(iterator != null)
return !iterator.hasNext();
return false;
}
/**
* Unload chunks

View File

@ -1,5 +1,6 @@
package org.dynmap.utils;
import org.bukkit.World;
import java.util.List;
import org.dynmap.DynmapChunk;
public interface MapChunkCache {
@ -12,11 +13,19 @@ public interface MapChunkCache {
public int x0, x1, z0, z1;
}
/**
* Load chunks into cache
* @param w - world
* @param chunks - chunks to be loaded
* Set chunks to load, and world to load from
*/
void loadChunks(World w, DynmapChunk[] chunks);
void setChunks(World w, List<DynmapChunk> chunks);
/**
* Load chunks into cache
* @param maxToLoad - maximum number to load at once
* @return number loaded
*/
int loadChunks(int maxToLoad);
/**
* Test if done loading
*/
boolean isDoneLoading();
/**
* Unload chunks
*/

View File

@ -3,6 +3,7 @@ package org.dynmap.utils;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import org.bukkit.World;
import org.bukkit.Chunk;
@ -18,6 +19,9 @@ import org.dynmap.Log;
public class NewMapChunkCache implements MapChunkCache {
private static Method poppreservedchunk = null;
private World w;
private List<DynmapChunk> chunks;
private ListIterator<DynmapChunk> iterator;
private int x_min, x_max, z_min, z_max;
private int x_dim;
private HiddenChunkStyle hidestyle = HiddenChunkStyle.FILL_AIR;
@ -189,16 +193,10 @@ public class NewMapChunkCache implements MapChunkCache {
*/
public NewMapChunkCache() {
}
/**
* Create chunk cache container
* @param w - world
* @param x_min - minimum chunk x coordinate
* @param z_min - minimum chunk z coordinate
* @param x_max - maximum chunk x coordinate
* @param z_max - maximum chunk z coordinate
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public void loadChunks(World w, DynmapChunk[] chunks) {
public void setChunks(World w, List<DynmapChunk> chunks) {
this.w = w;
this.chunks = chunks;
if(poppreservedchunk == null) {
/* Get CraftWorld.popPreservedChunk(x,z) - reduces memory bloat from map traversals (optional) */
try {
@ -209,7 +207,7 @@ public class NewMapChunkCache implements MapChunkCache {
}
}
/* Compute range */
if(chunks.length == 0) {
if(chunks.size() == 0) {
this.x_min = 0;
this.x_max = 0;
this.z_min = 0;
@ -217,24 +215,32 @@ public class NewMapChunkCache implements MapChunkCache {
x_dim = 1;
}
else {
x_min = x_max = chunks[0].x;
z_min = z_max = chunks[0].z;
for(int i = 1; i < chunks.length; i++) {
if(chunks[i].x > x_max)
x_max = chunks[i].x;
if(chunks[i].x < x_min)
x_min = chunks[i].x;
if(chunks[i].z > z_max)
z_max = chunks[i].z;
if(chunks[i].z < z_min)
z_min = chunks[i].z;
x_min = x_max = chunks.get(0).x;
z_min = z_max = chunks.get(0).z;
for(DynmapChunk c : chunks) {
if(c.x > x_max)
x_max = c.x;
if(c.x < x_min)
x_min = c.x;
if(c.z > z_max)
z_max = c.z;
if(c.z < z_min)
z_min = c.z;
}
x_dim = x_max - x_min + 1;
}
snaparray = new ChunkSnapshot[x_dim * (z_max-z_min+1)];
}
public int loadChunks(int max_to_load) {
int cnt = 0;
if(iterator == null)
iterator = chunks.listIterator();
// Load the required chunks.
for (DynmapChunk chunk : chunks) {
while((cnt < max_to_load) && iterator.hasNext()) {
DynmapChunk chunk = iterator.next();
boolean vis = true;
if(visible_limits != null) {
vis = false;
@ -286,12 +292,24 @@ public class NewMapChunkCache implements MapChunkCache {
Log.severe("Cannot pop preserved chunk - " + x.toString());
}
}
cnt++;
}
/* Fill missing chunks with empty dummy chunk */
for(int i = 0; i < snaparray.length; i++) {
if(snaparray[i] == null)
snaparray[i] = EMPTY;
if(iterator.hasNext() == false) { /* If we're done */
/* Fill missing chunks with empty dummy chunk */
for(int i = 0; i < snaparray.length; i++) {
if(snaparray[i] == null)
snaparray[i] = EMPTY;
}
}
return cnt;
}
/**
* Test if done loading
*/
public boolean isDoneLoading() {
if(iterator != null)
return !iterator.hasNext();
return false;
}
/**
* Unload chunks