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) # Period between tile renders for fullrender, in seconds (non-zero to pace fullrenders, lessen CPU load)
timesliceinterval: 0.0 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. # Interval the browser should poll for updates.
updaterate: 2000 updaterate: 2000

View File

@ -9,10 +9,10 @@ import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.scheduler.BukkitScheduler; import org.bukkit.scheduler.BukkitScheduler;
@ -25,13 +25,18 @@ import org.dynmap.utils.NewMapChunkCache;
public class MapManager { public class MapManager {
public AsynchronousQueue<MapTile> tileQueue; public AsynchronousQueue<MapTile> tileQueue;
private static final int DEFAULT_CHUNKS_PER_TICK = 200;
public List<DynmapWorld> worlds = new ArrayList<DynmapWorld>(); public List<DynmapWorld> worlds = new ArrayList<DynmapWorld>();
public Map<String, DynmapWorld> worldsLookup = new HashMap<String, DynmapWorld>(); public Map<String, DynmapWorld> worldsLookup = new HashMap<String, DynmapWorld>();
private BukkitScheduler scheduler; private BukkitScheduler scheduler;
private DynmapPlugin plug_in; private DynmapPlugin plug_in;
private long timeslice_int = 0; /* In milliseconds */ private long timeslice_int = 0; /* In milliseconds */
private int max_chunk_loads_per_tick = DEFAULT_CHUNKS_PER_TICK;
/* Which fullrenders are active */ /* Which fullrenders are active */
private HashMap<String, FullWorldRenderState> active_renders = new HashMap<String, FullWorldRenderState>(); 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 */ /* Tile hash manager */
public TileHashManager hashman; public TileHashManager hashman;
/* lock for our data structures */ /* lock for our data structures */
@ -61,9 +66,21 @@ public class MapManager {
return worlds; 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 { private class DynmapScheduledThreadPoolExecutor extends ScheduledThreadPoolExecutor {
DynmapScheduledThreadPoolExecutor() { DynmapScheduledThreadPoolExecutor() {
super(POOL_SIZE); super(POOL_SIZE);
this.setThreadFactory(new OurThreadFactory());
} }
protected void afterExecute(Runnable r, Throwable x) { protected void afterExecute(Runnable r, Throwable x) {
@ -163,7 +180,7 @@ public class MapManager {
} }
World w = world.world; World w = world.world;
/* Fetch chunk cache from server thread */ /* Fetch chunk cache from server thread */
DynmapChunk[] requiredChunks = tile.getMap().getRequiredChunks(tile); List<DynmapChunk> requiredChunks = tile.getMap().getRequiredChunks(tile);
MapChunkCache cache = createMapChunkCache(world, requiredChunks); MapChunkCache cache = createMapChunkCache(world, requiredChunks);
if(cache == null) { if(cache == null) {
cleanup(); 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) { public MapManager(DynmapPlugin plugin, ConfigurationNode configuration) {
plug_in = plugin; plug_in = plugin;
mapman = this; mapman = this;
@ -235,7 +271,8 @@ public class MapManager {
/* On dedicated thread, so default to no delays */ /* On dedicated thread, so default to no delays */
timeslice_int = (long)(configuration.getDouble("timesliceinterval", 0.0) * 1000); 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(); scheduler = plugin.getServer().getScheduler();
hashman = new TileHashManager(DynmapPlugin.tilesDirectory, configuration.getBoolean("enabletilehash", true)); 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 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) { void renderFullWorld(Location l, CommandSender sender) {
@ -439,9 +476,7 @@ public class MapManager {
/** /**
* Render processor helper - used by code running on render threads to request chunk snapshot cache from server/sync thread * 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) { public MapChunkCache createMapChunkCache(final DynmapWorld w, final List<DynmapChunk> chunks) {
Callable<MapChunkCache> job = new Callable<MapChunkCache>() {
public MapChunkCache call() {
MapChunkCache c = null; MapChunkCache c = null;
try { try {
if(!use_legacy) if(!use_legacy)
@ -457,18 +492,17 @@ public class MapManager {
} }
c.setHiddenFillStyle(w.hiddenchunkstyle); c.setHiddenFillStyle(w.hiddenchunkstyle);
} }
c.loadChunks(w.world, chunks); c.setChunks(w.world, chunks);
return c; synchronized(c) {
} chunkloads.add(c);
};
Future<MapChunkCache> rslt = scheduler.callSyncMethod(plug_in, job);
try { try {
return rslt.get(); c.wait();
} catch (Exception x) { } catch (InterruptedException ix) {
Log.info("createMapChunk - " + x);
return null; return null;
} }
} }
return c;
}
/** /**
* Update map tile statistics * Update map tile statistics
*/ */

View File

@ -1,6 +1,7 @@
package org.dynmap; package org.dynmap;
import java.io.File; import java.io.File;
import java.util.List;
import org.bukkit.Location; import org.bukkit.Location;
import org.dynmap.utils.MapChunkCache; import org.dynmap.utils.MapChunkCache;
@ -13,7 +14,7 @@ public abstract class MapType {
public abstract MapTile[] getAdjecentTiles(MapTile tile); 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); 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 static org.dynmap.JSONUtils.s;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
@ -101,18 +103,16 @@ public class FlatMap extends MapType {
} }
@Override @Override
public DynmapChunk[] getRequiredChunks(MapTile tile) { public List<DynmapChunk> getRequiredChunks(MapTile tile) {
FlatMapTile t = (FlatMapTile) tile; FlatMapTile t = (FlatMapTile) tile;
int chunksPerTile = t.size / 16; int chunksPerTile = t.size / 16;
int sx = t.x * chunksPerTile; int sx = t.x * chunksPerTile;
int sz = t.y * chunksPerTile; int sz = t.y * chunksPerTile;
DynmapChunk[] result = new DynmapChunk[chunksPerTile * chunksPerTile]; ArrayList<DynmapChunk> result = new ArrayList<DynmapChunk>(chunksPerTile * chunksPerTile);
int index = 0;
for (int x = 0; x < chunksPerTile; x++) for (int x = 0; x < chunksPerTile; x++)
for (int z = 0; z < chunksPerTile; z++) { for (int z = 0; z < chunksPerTile; z++) {
result[index] = new DynmapChunk(sx + x, sz + z); result.add(new DynmapChunk(sx + x, sz + z));
index++;
} }
return result; return result;
} }

View File

@ -164,7 +164,7 @@ public class KzedMap extends MapType {
return false; return false;
} }
@Override @Override
public DynmapChunk[] getRequiredChunks(MapTile tile) { public List<DynmapChunk> getRequiredChunks(MapTile tile) {
if (tile instanceof KzedMapTile) { if (tile instanceof KzedMapTile) {
KzedMapTile t = (KzedMapTile) tile; KzedMapTile t = (KzedMapTile) tile;
@ -216,12 +216,9 @@ public class KzedMap extends MapType {
chunks.add(chunk); chunks.add(chunk);
} }
} }
return chunks;
DynmapChunk[] result = new DynmapChunk[chunks.size()];
chunks.toArray(result);
return result;
} else { } 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.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.ListIterator;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.Chunk; import org.bukkit.Chunk;
@ -21,6 +22,10 @@ public class LegacyMapChunkCache implements MapChunkCache {
private static Field heightmap = null; private static Field heightmap = null;
private static boolean initialized = false; 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_min, x_max, z_min, z_max;
private int x_dim; private int x_dim;
private HiddenChunkStyle hidestyle = HiddenChunkStyle.FILL_AIR; private HiddenChunkStyle hidestyle = HiddenChunkStyle.FILL_AIR;
@ -177,17 +182,15 @@ public class LegacyMapChunkCache implements MapChunkCache {
public LegacyMapChunkCache() { public LegacyMapChunkCache() {
} }
/** /**
* Create chunk cache container * Set chunks to load, and world to load from
* @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" }) @SuppressWarnings({ "rawtypes", "unchecked" })
public void loadChunks(World w, DynmapChunk[] chunks) { public void setChunks(World w, List<DynmapChunk> chunks) {
this.w = w;
this.chunks = chunks;
/* Compute range */ /* Compute range */
if(chunks.length == 0) { if(chunks.size() == 0) {
this.x_min = 0; this.x_min = 0;
this.x_max = 0; this.x_max = 0;
this.z_min = 0; this.z_min = 0;
@ -195,17 +198,17 @@ public class LegacyMapChunkCache implements MapChunkCache {
x_dim = 1; x_dim = 1;
} }
else { else {
x_min = x_max = chunks[0].x; x_min = x_max = chunks.get(0).x;
z_min = z_max = chunks[0].z; z_min = z_max = chunks.get(0).z;
for(int i = 1; i < chunks.length; i++) { for(DynmapChunk c : chunks) {
if(chunks[i].x > x_max) if(c.x > x_max)
x_max = chunks[i].x; x_max = c.x;
if(chunks[i].x < x_min) if(c.x < x_min)
x_min = chunks[i].x; x_min = c.x;
if(chunks[i].z > z_max) if(c.z > z_max)
z_max = chunks[i].z; z_max = c.z;
if(chunks[i].z < z_min) if(c.z < z_min)
z_min = chunks[i].z; z_min = c.z;
} }
x_dim = x_max - x_min + 1; 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)]; snaparray = new LegacyChunkSnapshot[x_dim * (z_max-z_min+1)];
if(gethandle != null) { }
public int loadChunks(int max_to_load) {
int cnt = 0;
if(iterator == null)
iterator = chunks.listIterator();
// Load the required chunks. // Load the required chunks.
for (DynmapChunk chunk : chunks) { while((cnt < max_to_load) && iterator.hasNext()) {
DynmapChunk chunk = iterator.next();
if(gethandle != null) {
boolean vis = true; boolean vis = true;
if(visible_limits != null) { if(visible_limits != null) {
vis = false; vis = false;
@ -302,13 +313,26 @@ public class LegacyMapChunkCache implements MapChunkCache {
} }
} }
} }
cnt++;
} }
/* If done, finish table */
if(iterator.hasNext() == false) {
/* Fill missing chunks with empty dummy chunk */ /* Fill missing chunks with empty dummy chunk */
for(int i = 0; i < snaparray.length; i++) { for(int i = 0; i < snaparray.length; i++) {
if(snaparray[i] == null) if(snaparray[i] == null)
snaparray[i] = EMPTY; snaparray[i] = EMPTY;
} }
} }
return cnt;
}
/**
* Test if done loading
*/
public boolean isDoneLoading() {
if(iterator != null)
return !iterator.hasNext();
return false;
}
/** /**
* Unload chunks * Unload chunks
*/ */

View File

@ -1,5 +1,6 @@
package org.dynmap.utils; package org.dynmap.utils;
import org.bukkit.World; import org.bukkit.World;
import java.util.List;
import org.dynmap.DynmapChunk; import org.dynmap.DynmapChunk;
public interface MapChunkCache { public interface MapChunkCache {
@ -12,11 +13,19 @@ public interface MapChunkCache {
public int x0, x1, z0, z1; public int x0, x1, z0, z1;
} }
/** /**
* Load chunks into cache * Set chunks to load, and world to load from
* @param w - world
* @param chunks - chunks to be loaded
*/ */
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 * Unload chunks
*/ */

View File

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