working reentrant locked tile generation

master
Thomas Rudin 2018-12-18 15:07:34 +01:00
parent 574e58e40b
commit 9a2525566a
6 changed files with 229 additions and 168 deletions

View File

@ -8,12 +8,15 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.zip.DataFormatException;
import javax.imageio.ImageIO;
import javax.inject.Inject;
import javax.inject.Singleton;
import com.google.common.util.concurrent.Striped;
import io.prometheus.client.Histogram;
import io.rudin.minetest.tileserver.config.Layer;
import io.rudin.minetest.tileserver.config.TileServerConfig;
@ -67,6 +70,8 @@ public class TileRenderer {
private final int DEFAULT_BLOCK_ZOOM = 13;
private final Striped<ReadWriteLock> rwLockStripes = Striped.lazyWeakReadWriteLock(100);
public byte[] render(Layer layer, int tileX, int tileY, int zoom) throws IllegalArgumentException, DataFormatException, IOException, ExecutionException {
return render(layer, tileX, tileY, zoom, true);
}
@ -82,6 +87,7 @@ public class TileRenderer {
}
}
Range<MapBlockCoordinate> mapBlocksRange = CoordinateFactory.getMapBlocksInTile(new TileCoordinate(tileX, tileY, zoom));
int max = Math.max(
@ -98,47 +104,6 @@ public class TileRenderer {
//Out of range...
return WhiteTile.getPNG();
if (zoom < DEFAULT_BLOCK_ZOOM) {
//TODO: fail-fast for regions without map-blocks -> white
long start = System.currentTimeMillis();
Result<BlocksRecord> firstResult = ctx
.selectFrom(BLOCKS)
.where(
BLOCKS.POSX.ge(Math.min(mapBlocksRange.pos1.x, mapBlocksRange.pos2.x))
.and(BLOCKS.POSX.le(Math.max(mapBlocksRange.pos1.x, mapBlocksRange.pos2.x)))
.and(BLOCKS.POSZ.ge(Math.min(mapBlocksRange.pos1.z, mapBlocksRange.pos2.z)))
.and(BLOCKS.POSZ.le(Math.max(mapBlocksRange.pos1.z, mapBlocksRange.pos2.z)))
.and(yQueryBuilder.getCondition(layer))
)
.limit(1)
.fetch();
long diff = System.currentTimeMillis() - start;
if (diff > 250 && cfg.logQueryPerformance()){
logger.warn("white-count-query took {} ms", diff);
}
if (firstResult.isEmpty()) {
logger.debug("Fail-fast, got zero mapblock count for x=({})-({}) z=({})-({})",
mapBlocksRange.pos1.x, mapBlocksRange.pos2.x,
mapBlocksRange.pos1.z, mapBlocksRange.pos2.z);
byte[] data = WhiteTile.getPNG();
if (zoom < 11) {
//Only cache white space in upper zoom levels
cache.put(layer.id, tileX, tileY, zoom, data);
}
return data;
}
}
BufferedImage image = renderImage(layer, tileX, tileY, zoom, usecache);
@ -168,149 +133,219 @@ public class TileRenderer {
public BufferedImage renderImage(Layer layer, int tileX, int tileY, int zoom, boolean usecache) throws IllegalArgumentException, DataFormatException, IOException, ExecutionException {
//Check if binary cached, use cached version for rendering
if (usecache && cache.has(layer.id, tileX, tileY, zoom)) {
byte[] data = cache.get(layer.id, tileX, tileY, zoom);
if (data != null && data.length > 0)
//In case the cache disappears
return ImageIO.read(new ByteArrayInputStream(data));
}
Histogram.Timer timer = renderTime.startTimer();
ReadWriteLock rwLock = rwLockStripes.get(layer.id + "/" + tileX + "/" + tileY + "/" + zoom);
try {
rwLock.readLock().lock();
final int HALF_TILE_PIXEL_SIZE = TILE_PIXEL_SIZE / 2;
//Check if binary cached, use cached version for rendering
if (usecache && cache.has(layer.id, tileX, tileY, zoom)) {
byte[] data = cache.get(layer.id, tileX, tileY, zoom);
if (data != null && data.length > 0)
//In case the cache disappears
return ImageIO.read(new ByteArrayInputStream(data));
}
} finally {
rwLock.readLock().unlock();
}
try {
rwLock.writeLock().lock();
//Second cache check in critical section
//Check if binary cached, use cached version for rendering
if (usecache && cache.has(layer.id, tileX, tileY, zoom)) {
byte[] data = cache.get(layer.id, tileX, tileY, zoom);
if (data != null && data.length > 0)
//In case the cache disappears
return ImageIO.read(new ByteArrayInputStream(data));
}
BufferedImage tile = createTile();
//16x16 mapblocks on a tile
Graphics2D graphics = tile.createGraphics();
graphics.setColor(Color.WHITE);
graphics.fillRect(0, 0, TILE_PIXEL_SIZE, TILE_PIXEL_SIZE);
Range<MapBlockCoordinate> mapBlocksRange = CoordinateFactory.getMapBlocksInTile(new TileCoordinate(tileX, tileY, zoom));
int max = Math.max(
Math.max(mapBlocksRange.pos1.x, mapBlocksRange.pos1.z),
Math.max(mapBlocksRange.pos2.x, mapBlocksRange.pos2.x)
);
int min = Math.min(
Math.min(mapBlocksRange.pos1.x, mapBlocksRange.pos1.z),
Math.min(mapBlocksRange.pos2.x, mapBlocksRange.pos2.x)
);
if (max > 2048 || min < -2048)
//Out of range...
return tile;
if (zoom < DEFAULT_BLOCK_ZOOM) {
//Zoom out
BufferedImage tile = createTile();
//Pack 4 tiles from higher zoom into 1 tile
TileQuadrants quadrants = CoordinateFactory.getZoomedQuadrantsFromTile(new TileCoordinate(tileX, tileY, zoom));
int nextZoom = zoom + 1;
int nextZoomX = tileX * 2;
int nextZoomY = tileY * 2;
BufferedImage upperLeft = renderImage(layer, quadrants.upperLeft.x, quadrants.upperLeft.y, quadrants.upperLeft.zoom);
BufferedImage upperRightImage = renderImage(layer, quadrants.upperRight.x, quadrants.upperRight.y, quadrants.upperRight.zoom);
BufferedImage lowerLeftImage = renderImage(layer, quadrants.lowerLeft.x, quadrants.lowerLeft.y, quadrants.lowerLeft.zoom);
BufferedImage lowerRightImage = renderImage(layer, quadrants.lowerRight.x, quadrants.lowerRight.y, quadrants.lowerRight.zoom);
long start = System.currentTimeMillis();
Graphics2D graphics = tile.createGraphics();
Result<BlocksRecord> firstResult = ctx
.selectFrom(BLOCKS)
.where(
BLOCKS.POSX.ge(Math.min(mapBlocksRange.pos1.x, mapBlocksRange.pos2.x))
.and(BLOCKS.POSX.le(Math.max(mapBlocksRange.pos1.x, mapBlocksRange.pos2.x)))
.and(BLOCKS.POSZ.ge(Math.min(mapBlocksRange.pos1.z, mapBlocksRange.pos2.z)))
.and(BLOCKS.POSZ.le(Math.max(mapBlocksRange.pos1.z, mapBlocksRange.pos2.z)))
.and(yQueryBuilder.getCondition(layer))
)
.limit(1)
.fetch();
Image upperLeftScaledInstance = upperLeft.getScaledInstance(HALF_TILE_PIXEL_SIZE, HALF_TILE_PIXEL_SIZE, Image.SCALE_FAST);
graphics.drawImage(upperLeftScaledInstance, 0, 0, HALF_TILE_PIXEL_SIZE, HALF_TILE_PIXEL_SIZE, null);
if (firstResult.size() == 0){
return tile;
}
}
Image upperRightScaledInstance = upperRightImage.getScaledInstance(HALF_TILE_PIXEL_SIZE, HALF_TILE_PIXEL_SIZE, Image.SCALE_FAST);
graphics.drawImage(upperRightScaledInstance, HALF_TILE_PIXEL_SIZE, 0, HALF_TILE_PIXEL_SIZE, HALF_TILE_PIXEL_SIZE, null);
Image lowerLeftScaledInstance = lowerLeftImage.getScaledInstance(HALF_TILE_PIXEL_SIZE, HALF_TILE_PIXEL_SIZE, Image.SCALE_FAST);
graphics.drawImage(lowerLeftScaledInstance, 0, HALF_TILE_PIXEL_SIZE, HALF_TILE_PIXEL_SIZE, HALF_TILE_PIXEL_SIZE, null);
Histogram.Timer timer = renderTime.startTimer();
long start = System.currentTimeMillis();
Image lowerRightScaledInstance = lowerRightImage.getScaledInstance(HALF_TILE_PIXEL_SIZE, HALF_TILE_PIXEL_SIZE, Image.SCALE_FAST);
graphics.drawImage(lowerRightScaledInstance, HALF_TILE_PIXEL_SIZE, HALF_TILE_PIXEL_SIZE, HALF_TILE_PIXEL_SIZE, HALF_TILE_PIXEL_SIZE, null);
try {
final int HALF_TILE_PIXEL_SIZE = TILE_PIXEL_SIZE / 2;
if (zoom < DEFAULT_BLOCK_ZOOM) {
//Zoom out
logger.debug("Rendering tile: X={}, Y={} zoom={}", tileX, tileY, zoom);
//Pack 4 tiles from higher zoom into 1 tile
TileQuadrants quadrants = CoordinateFactory.getZoomedQuadrantsFromTile(new TileCoordinate(tileX, tileY, zoom));
int nextZoom = zoom + 1;
int nextZoomX = tileX * 2;
int nextZoomY = tileY * 2;
BufferedImage upperLeft = renderImage(layer, quadrants.upperLeft.x, quadrants.upperLeft.y, quadrants.upperLeft.zoom);
BufferedImage upperRightImage = renderImage(layer, quadrants.upperRight.x, quadrants.upperRight.y, quadrants.upperRight.zoom);
BufferedImage lowerLeftImage = renderImage(layer, quadrants.lowerLeft.x, quadrants.lowerLeft.y, quadrants.lowerLeft.zoom);
BufferedImage lowerRightImage = renderImage(layer, quadrants.lowerRight.x, quadrants.lowerRight.y, quadrants.lowerRight.zoom);
Image upperLeftScaledInstance = upperLeft.getScaledInstance(HALF_TILE_PIXEL_SIZE, HALF_TILE_PIXEL_SIZE, Image.SCALE_FAST);
graphics.drawImage(upperLeftScaledInstance, 0, 0, HALF_TILE_PIXEL_SIZE, HALF_TILE_PIXEL_SIZE, null);
Image upperRightScaledInstance = upperRightImage.getScaledInstance(HALF_TILE_PIXEL_SIZE, HALF_TILE_PIXEL_SIZE, Image.SCALE_FAST);
graphics.drawImage(upperRightScaledInstance, HALF_TILE_PIXEL_SIZE, 0, HALF_TILE_PIXEL_SIZE, HALF_TILE_PIXEL_SIZE, null);
Image lowerLeftScaledInstance = lowerLeftImage.getScaledInstance(HALF_TILE_PIXEL_SIZE, HALF_TILE_PIXEL_SIZE, Image.SCALE_FAST);
graphics.drawImage(lowerLeftScaledInstance, 0, HALF_TILE_PIXEL_SIZE, HALF_TILE_PIXEL_SIZE, HALF_TILE_PIXEL_SIZE, null);
Image lowerRightScaledInstance = lowerRightImage.getScaledInstance(HALF_TILE_PIXEL_SIZE, HALF_TILE_PIXEL_SIZE, Image.SCALE_FAST);
graphics.drawImage(lowerRightScaledInstance, HALF_TILE_PIXEL_SIZE, HALF_TILE_PIXEL_SIZE, HALF_TILE_PIXEL_SIZE, HALF_TILE_PIXEL_SIZE, null);
ByteArrayOutputStream output = new ByteArrayOutputStream(12000);
ImageIO.write(tile, "png", output);
byte[] data = output.toByteArray();
long diff = System.currentTimeMillis() - start;
logger.trace("Timings of cross-stitched tile X={} Y={} Zoom={}: render={} ms", tileX, tileY, zoom, diff);
cache.put(layer.id, tileX, tileY, zoom, data);
if (tile == null)
logger.error("Got a null-tile @ {}/{}/{} (data={})", tileX, tileY, zoom, data.length);
return tile;
}
Range<MapBlockCoordinate> coordinateRange = CoordinateFactory.getMapBlocksInTile(new TileCoordinate(tileX, tileY, zoom));
int mapblockX = coordinateRange.pos1.x;
int mapblockZ = coordinateRange.pos1.z;
long now = System.currentTimeMillis();
long timingImageSetup = now - start;
start = now;
Result<BlocksRecord> countList = ctx
.selectFrom(BLOCKS)
.where(
BLOCKS.POSX.eq(mapblockX)
.and(BLOCKS.POSZ.eq(mapblockZ))
.and(yQueryBuilder.getCondition(layer))
)
.limit(1)
.fetch();
now = System.currentTimeMillis();
long timingZeroCountCheck = now - start;
if (timingZeroCountCheck > 250 && cfg.logQueryPerformance()) {
logger.warn("count-zero-check took {} ms", timingZeroCountCheck);
}
start = now;
long timingRender = 0;
if (!countList.isEmpty()) {
logger.debug("Rendering tile for mapblock: X={}, Z={}", mapblockX, mapblockZ);
blockRenderer.render(
YQueryBuilder.coordinateToMapBlock(layer.from),
YQueryBuilder.coordinateToMapBlock(layer.to),
mapblockX,
mapblockZ,
graphics, 16
);
now = System.currentTimeMillis();
timingRender = now - start;
start = now;
}
ByteArrayOutputStream output = new ByteArrayOutputStream(12000);
ImageIO.write(tile, "png", output);
byte[] data = output.toByteArray();
long diff = System.currentTimeMillis() - start;
now = System.currentTimeMillis();
long timingBufferOutput = now - start;
logger.trace("Timings of cross-stitched tile X={} Y={} Zoom={}: render={} ms", tileX, tileY, zoom, diff);
logger.debug("Timings of tile X={} Y={} Zoom={}: setup={} ms, zeroCheck={} ms, render={} ms, output={} ms",
tileX, tileY, zoom,
timingImageSetup, timingZeroCountCheck, timingRender, timingBufferOutput
);
cache.put(layer.id, tileX, tileY, zoom, data);
if (tile == null)
logger.error("Got a null-tile @ {}/{}/{} (data={})", tileX, tileY, zoom, data.length);
logger.error("Got a null-tile @ {}/{}/{} (layer={},data={})", tileX, tileY, zoom, layer.id, data.length);
return tile;
} finally {
timer.observeDuration();
}
//Default zoom (13 == 1 mapblock with 16px wide blocks)
long start = System.currentTimeMillis();
Range<MapBlockCoordinate> coordinateRange = CoordinateFactory.getMapBlocksInTile(new TileCoordinate(tileX, tileY, zoom));
//16x16 mapblocks on a tile
BufferedImage image = createTile();
Graphics2D graphics = image.createGraphics();
graphics.setColor(Color.WHITE);
graphics.fillRect(0, 0, TILE_PIXEL_SIZE, TILE_PIXEL_SIZE);
int mapblockX = coordinateRange.pos1.x;
int mapblockZ = coordinateRange.pos1.z;
long now = System.currentTimeMillis();
long timingImageSetup = now - start;
start = now;
Result<BlocksRecord> countList = ctx
.selectFrom(BLOCKS)
.where(
BLOCKS.POSX.eq(mapblockX)
.and(BLOCKS.POSZ.eq(mapblockZ))
.and(yQueryBuilder.getCondition(layer))
)
.limit(1)
.fetch();
now = System.currentTimeMillis();
long timingZeroCountCheck = now - start;
if (timingZeroCountCheck > 250 && cfg.logQueryPerformance()) {
logger.warn("count-zero-check took {} ms", timingZeroCountCheck);
}
start = now;
long timingRender = 0;
if (!countList.isEmpty()) {
logger.debug("Rendering tile for mapblock: X={}, Z={}", mapblockX, mapblockZ);
blockRenderer.render(layer.from, layer.to, mapblockX, mapblockZ, graphics, 16);
now = System.currentTimeMillis();
timingRender = now - start;
start = now;
}
ByteArrayOutputStream output = new ByteArrayOutputStream(12000);
ImageIO.write(image, "png", output);
byte[] data = output.toByteArray();
now = System.currentTimeMillis();
long timingBufferOutput = now - start;
logger.trace("Timings of tile X={} Y={} Zoom={}: setup={} ms, zeroCheck={} ms, render={} ms, output={} ms",
tileX, tileY, zoom,
timingImageSetup, timingZeroCountCheck, timingRender, timingBufferOutput
);
cache.put(layer.id, tileX, tileY, zoom, data);
if (image == null)
logger.error("Got a null-tile @ {}/{}/{} (layer={},data={})", tileX, tileY, zoom, layer.id, data.length);
return image;
} finally {
timer.observeDuration();
rwLock.writeLock().unlock();
}
}

View File

@ -93,7 +93,7 @@ public class UpdateChangedTilesJob implements Runnable {
private final EventBus eventBus;
private final DSLContext ctx;
private final TileCache tileCache;
private boolean running = false;
@ -235,9 +235,28 @@ public class UpdateChangedTilesJob implements Runnable {
logger.debug("Got {} updated blocks", blocks.size());
//invalidation run
for (BlocksRecord record : blocks) {
blocksRecordService.update(record);
mapBlockAccessor.invalidate(new Coordinate(record));
if (record.getMtime() > latestTimestamp)
latestTimestamp = record.getMtime();
Integer x = record.getPosx();
Integer z = record.getPosz();
TileCoordinate tileCoordinate = CoordinateFactory.getTileCoordinateFromMapBlock(new MapBlockCoordinate(x, z));
for (int i = CoordinateFactory.MAX_ZOOM; i > 0; i--) {
tileCache.remove(layer.id, tileCoordinate.x, tileCoordinate.y, tileCoordinate.zoom);
invalidatedTiles++;
//Zoom out
if (i > 1) {
tileCoordinate = CoordinateFactory.getZoomedOutTile(tileCoordinate);
}
}
}
//assign new timestamp
@ -251,7 +270,7 @@ public class UpdateChangedTilesJob implements Runnable {
List<String> updatedTileKeys = new ArrayList<>();
//run with rendering of other zoom levels, but cached
//tile re-rendering
for (BlocksRecord record : blocks) {
Integer x = record.getPosx();
@ -259,7 +278,7 @@ public class UpdateChangedTilesJob implements Runnable {
TileCoordinate tileCoordinate = CoordinateFactory.getTileCoordinateFromMapBlock(new MapBlockCoordinate(x, z));
for (int i = CoordinateFactory.MAX_ZOOM; i >= 3; i--) {
for (int i = CoordinateFactory.MAX_ZOOM; i > 0; i--) {
String tileKey = getTileKey(tileCoordinate);
@ -269,7 +288,7 @@ public class UpdateChangedTilesJob implements Runnable {
logger.debug("Rendering tile x={} y={} zoom={}", tileCoordinate.x, tileCoordinate.y, tileCoordinate.zoom);
tileRenderer.render(layer, tileCoordinate.x, tileCoordinate.y, tileCoordinate.zoom);
logger.debug("Dispatching tile-changed-event for tile: {}/{}", tileCoordinate.x, tileCoordinate.y);
logger.debug("Dispatching tile-changed-event for tile: {}/{} zoom:{}", tileCoordinate.x, tileCoordinate.y, tileCoordinate.zoom);
EventBus.TileChangedEvent event = new EventBus.TileChangedEvent();
event.layerId = layer.id;
@ -289,7 +308,9 @@ public class UpdateChangedTilesJob implements Runnable {
}
//zom out
tileCoordinate = CoordinateFactory.getZoomedOutTile(tileCoordinate);
if (i > 1) {
tileCoordinate = CoordinateFactory.getZoomedOutTile(tileCoordinate);
}
}

View File

@ -14,7 +14,7 @@ public class CoordinateFactory {
*/
public static TileCoordinate getTileCoordinateFromMapBlock(MapBlockCoordinate mapBlock){
//Inverted and offsetet z-axis
return new TileCoordinate(mapBlock.x, (mapBlock.z * -1) + 1, MAX_ZOOM);
return new TileCoordinate(mapBlock.x, (mapBlock.z + 1) * -1, MAX_ZOOM);
}
/**

View File

@ -23,8 +23,8 @@
<logger name="org.jooq" level="WARN"/>
<logger name="io.rudin.minetest.tileserver.job.UpdateChangedTilesJob" level="INFO"/>
<logger name="io.rudin.minetest.tileserver.MapBlockRenderer" level="INFO"/>
<logger name="io.rudin.minetest.tileserver.TileRenderer" level="INFO"/>
<logger name="io.rudin.minetest.tileserver.service.impl.DefaultMapBlockRenderService" level="INFO"/>
<logger name="io.rudin.minetest.tileserver.TileRenderer" level="DEBUG"/>
<root level="info">
<appender-ref ref="logfile" />

View File

@ -12,19 +12,19 @@ public class CoordinateFactoryTest {
MapBlockCoordinate mapBlock = new MapBlockCoordinate(0,0);
TileCoordinate tileCoordinate = CoordinateFactory.getTileCoordinateFromMapBlock(mapBlock);
Assert.assertEquals(0, tileCoordinate.x);
Assert.assertEquals(1, tileCoordinate.y);
Assert.assertEquals(-1, tileCoordinate.y);
Assert.assertEquals(13, tileCoordinate.zoom);
mapBlock = new MapBlockCoordinate(1,1);
tileCoordinate = CoordinateFactory.getTileCoordinateFromMapBlock(mapBlock);
Assert.assertEquals(1, tileCoordinate.x);
Assert.assertEquals(0, tileCoordinate.y);
Assert.assertEquals(-2, tileCoordinate.y);
Assert.assertEquals(13, tileCoordinate.zoom);
mapBlock = new MapBlockCoordinate(-1,-1);
tileCoordinate = CoordinateFactory.getTileCoordinateFromMapBlock(mapBlock);
Assert.assertEquals(-1, tileCoordinate.x);
Assert.assertEquals(2, tileCoordinate.y);
Assert.assertEquals(0, tileCoordinate.y);
Assert.assertEquals(13, tileCoordinate.zoom);
}

View File

@ -44,7 +44,7 @@ public class UpdateChangedTilesTest extends TileServerTest {
byte[] tile = tileCache.get(0, tileCoordinate.x, tileCoordinate.y, tileCoordinate.zoom);
Assert.assertNull(tile);
logger.debug("First result: {}", changedTilesJob.updateChangedTiles());
logger.debug("First result (init): {}", changedTilesJob.updateChangedTiles());
tile = tileCache.get(0, tileCoordinate.x, tileCoordinate.y, tileCoordinate.zoom);
Assert.assertNotNull(tile);
@ -56,7 +56,12 @@ public class UpdateChangedTilesTest extends TileServerTest {
block.setMtime(System.currentTimeMillis());
block.update();
logger.debug("Second result: {}", changedTilesJob.updateChangedTiles());
logger.debug("Second result (changed): {}", changedTilesJob.updateChangedTiles());
tile = tileCache.get(0, tileCoordinate.x, tileCoordinate.y, tileCoordinate.zoom);
Assert.assertNotNull(tile);
logger.debug("Third result (unchanged): {}", changedTilesJob.updateChangedTiles());
tile = tileCache.get(0, tileCoordinate.x, tileCoordinate.y, tileCoordinate.zoom);
Assert.assertNotNull(tile);