Stop saving Air blocks to the database

War now skips over saving Air blocks. Instead, the plugin loads all solid blocks back into the world and then sets each block to air that was not changed.

Benchmarks:
Warzone molecule
Block count 2,487,555
Speed 50,000 blocks per tick
Intel Core i7 @ 3.6 GHz * 8

Before:
File size 70 MB
Reset 23 seconds
Save 16 seconds

After:
File size 54 MB
Save 5 seconds
(no changes)
Reset 4.9 seconds
(set to air)
Reset 31.17 seconds
(set to stone)
Reset 18.23 seconds
This commit is contained in:
cmastudios 2013-12-07 16:22:50 -06:00
parent 7ef61af277
commit 67bb509735
3 changed files with 148 additions and 58 deletions

View File

@ -4,11 +4,11 @@ import java.sql.SQLException;
import java.text.DecimalFormat;
import java.text.MessageFormat;
import java.text.NumberFormat;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.*;
import java.util.logging.Level;
import org.bukkit.Material;
import org.bukkit.block.BlockState;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
@ -31,57 +31,77 @@ public class PartialZoneResetJob extends BukkitRunnable implements Cloneable {
private int completed = 0;
private final long startTime = System.currentTimeMillis();
private long messageCounter = System.currentTimeMillis();
boolean[][][] changes;
public static final long MESSAGE_INTERVAL = 7500;
// Ticks between job runs
public static final int JOB_INTERVAL = 1;
private int totalChanges = 0;
private NumberFormat formatter = new DecimalFormat("#0.00");
/**
* Reset a warzone's blocks at a certain speed.
*
* @param volume
* @param zone
* Warzone to reset.
* @param speed
* Blocks to modify per #INTERVAL.
*/
public PartialZoneResetJob(Warzone zone, int speed) {
public PartialZoneResetJob(Warzone zone, int speed) throws SQLException {
this.zone = zone;
this.volume = zone.getVolume();
this.speed = speed;
this.total = volume.size();
this.total = volume.getTotalSavedBlocks();
this.changes = new boolean[volume.getSizeX()][volume.getSizeY()][volume.getSizeZ()];
}
@Override
public void run() {
try {
volume.resetSection(completed, speed);
completed += speed;
NumberFormat formatter = new DecimalFormat("#0.00");
String secondsAsText = formatter.format(((double)(System.currentTimeMillis() - startTime)) / 1000);
if (completed < total) {
if (System.currentTimeMillis() - messageCounter > MESSAGE_INTERVAL) {
messageCounter = System.currentTimeMillis();
int percent = (int) (((double) completed / (double) total) * 100);
float seconds = (System.currentTimeMillis() - startTime) / 1000;
String message = MessageFormat.format(
War.war.getString("zone.battle.resetprogress"),
percent, secondsAsText);
this.sendMessageToAllWarzonePlayers(message);
if (completed >= total) {
int airChanges = 0;
int minX = volume.getMinX(), minY = volume.getMinY(), minZ = volume.getMinZ();
air: for (int x = volume.getMinX(); x <= volume.getMaxX(); x++) {
for (int y = volume.getMinY(); y <= volume.getMaxY(); y++) {
for (int z = volume.getMinZ(); z <= volume.getMaxZ(); z++) {
int xi = x - minX, yi = y - minY, zi = z - minZ;
if (!changes[xi][yi][zi]) {
changes[xi][yi][zi] = true;
airChanges++;
BlockState state = volume.getWorld().getBlockAt(x, y, z).getState();
if (state.getType() != Material.AIR) {
state.setType(Material.AIR);
state.update(true, false);
}
if (airChanges >= speed) {
this.displayStatusMessage();
break air;
}
}
}
}
}
totalChanges += airChanges;
if (this.doneAir()) {
String secondsAsText = formatter.format(((double)(System.currentTimeMillis() - startTime)) / 1000);
String message = MessageFormat.format(
War.war.getString("zone.battle.resetcomplete"), secondsAsText);
this.sendMessageToAllWarzonePlayers(message);
PartialZoneResetJob.setSenderToNotify(zone, null); // stop notifying for this zone
zone.initializeZone();
War.war.getLogger().log(Level.INFO, "Finished reset cycle for warzone {0} (took {1} seconds)",
new Object[]{volume.getName(), secondsAsText});
} else {
War.war.getServer().getScheduler().runTaskLater(War.war, this.clone(), JOB_INTERVAL);
}
War.war.getServer().getScheduler()
.runTaskLater(War.war, this.clone(), JOB_INTERVAL);
} else {
float seconds = (System.currentTimeMillis() - startTime) / 1000;
String message = MessageFormat.format(
War.war.getString("zone.battle.resetcomplete"), secondsAsText);
this.sendMessageToAllWarzonePlayers(message);
PartialZoneResetJob.setSenderToNotify(zone, null); // stop notifying for this zone
zone.initializeZone();
War.war.getLogger().info(
"Finished reset cycle for warzone " + volume.getName() + " (took " + secondsAsText + " seconds)");
int solidChanges = volume.resetSection(completed, speed, changes);
completed += solidChanges;
totalChanges += solidChanges;
this.displayStatusMessage();
War.war.getServer().getScheduler().runTaskLater(War.war, this.clone(), JOB_INTERVAL);
}
} catch (SQLException e) {
War.war.getLogger().log(Level.WARNING,
"Failed to load zone during reset loop", e);
War.war.getLogger().log(Level.WARNING, "Failed to load zone during reset loop", e);
}
}
@ -100,6 +120,18 @@ public class PartialZoneResetJob extends BukkitRunnable implements Cloneable {
}
}
private void displayStatusMessage() {
if (System.currentTimeMillis() - messageCounter > MESSAGE_INTERVAL) {
String secondsAsText = formatter.format(((double)(System.currentTimeMillis() - startTime)) / 1000);
messageCounter = System.currentTimeMillis();
int percent = (int) (((double) totalChanges / (double) volume.size()) * 100);
String message = MessageFormat.format(
War.war.getString("zone.battle.resetprogress"),
percent, secondsAsText);
this.sendMessageToAllWarzonePlayers(message);
}
}
@Override
protected PartialZoneResetJob clone() {
try {
@ -112,4 +144,19 @@ public class PartialZoneResetJob extends BukkitRunnable implements Cloneable {
public static void setSenderToNotify(Warzone warzone, CommandSender sender) {
PartialZoneResetJob.sendersToNotify.put(warzone, sender);
}
private boolean doneAir() {
int minX = volume.getMinX(), minY = volume.getMinY(), minZ = volume.getMinZ();
for (int x = volume.getMinX(); x <= volume.getMaxX(); x++) {
for (int y = volume.getMinY(); y <= volume.getMaxY(); y++) {
for (int z = volume.getMinZ(); z <= volume.getMaxZ(); z++) {
int xi = x - minX, yi = y - minY, zi = z - minZ;
if (!changes[xi][yi][zi]) {
return false;
}
}
}
}
return true;
}
}

View File

@ -7,6 +7,7 @@ import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
@ -57,16 +58,15 @@ public class ZoneVolumeMapper {
* @param total Amount of blocks to read
* @return Changed blocks
* @throws SQLException Error communicating with SQLite3 database
*/
public static int load(ZoneVolume volume, String zoneName, World world, boolean onlyLoadCorners, int start, int total) throws SQLException {
int changed = 0;
*/
public static int load(ZoneVolume volume, String zoneName, World world, boolean onlyLoadCorners, int start, int total, boolean[][][] changes) throws SQLException {
File databaseFile = new File(War.war.getDataFolder(), String.format("/dat/warzone-%s/volume-%s.sl3", zoneName, volume.getName()));
if (!databaseFile.exists()) {
// Convert warzone to nimitz file format.
changed = PreNimitzZoneVolumeMapper.load(volume, zoneName, world, onlyLoadCorners);
PreNimitzZoneVolumeMapper.load(volume, zoneName, world, onlyLoadCorners);
ZoneVolumeMapper.save(volume, zoneName);
War.war.log("Warzone " + zoneName + " converted to nimitz format!", Level.INFO);
return changed;
return volume.size();
}
Connection databaseConnection = DriverManager.getConnection("jdbc:sqlite:" + databaseFile.getPath());
Statement stmt = databaseConnection.createStatement();
@ -114,10 +114,18 @@ public class ZoneVolumeMapper {
databaseConnection.close();
return 0;
}
int minX = volume.getMinX(), minY = volume.getMinY(), minZ = volume.getMinZ();
int changed = 0;
ResultSet query = stmt.executeQuery("SELECT * FROM blocks ORDER BY rowid LIMIT " + start + ", " + total);
while (query.next()) {
int x = query.getInt("x"), y = query.getInt("y"), z = query.getInt("z");
BlockState modify = corner1.getRelative(x, y, z).getState();
changed++;
Block relative = corner1.getRelative(x, y, z);
int xi = relative.getX() - minX, yi = relative.getY() - minY, zi = relative.getZ() - minZ;
if (changes != null) {
changes[xi][yi][zi] = true;
}
BlockState modify = relative.getState();
ItemStack data = new ItemStack(Material.valueOf(query.getString("type")), 0, query.getShort("data"));
if (modify.getType() != data.getType() || !modify.getData().equals(data.getData())) {
// Update the type & data if it has changed
@ -126,7 +134,6 @@ public class ZoneVolumeMapper {
modify.update(true, false); // No-physics update, preventing the need for deferring blocks
modify = corner1.getRelative(x, y, z).getState(); // Grab a new instance
}
changed++;
if (query.getString("metadata") == null || query.getString("metadata").isEmpty()) {
continue;
}
@ -140,7 +147,7 @@ public class ZoneVolumeMapper {
}
// Containers
if (modify instanceof InventoryHolder && query.getString("metadata") != null) {
if (modify instanceof InventoryHolder) {
YamlConfiguration config = new YamlConfiguration();
config.loadFromString(query.getString("metadata"));
((InventoryHolder) modify).getInventory().clear();
@ -167,7 +174,7 @@ public class ZoneVolumeMapper {
}
// Skulls
if (modify instanceof Skull && query.getString("metadata") != null) {
if (modify instanceof Skull) {
String[] opts = query.getString("metadata").split("\n");
((Skull) modify).setOwner(opts[0]);
((Skull) modify).setSkullType(SkullType.valueOf(opts[1]));
@ -176,7 +183,7 @@ public class ZoneVolumeMapper {
}
// Command blocks
if (modify instanceof CommandBlock && query.getString("metadata") != null) {
if (modify instanceof CommandBlock) {
final String[] commandArray = query.getString("metadata").split("\n");
((CommandBlock) modify).setName(commandArray[0]);
((CommandBlock) modify).setCommand(commandArray[1]);
@ -198,6 +205,25 @@ public class ZoneVolumeMapper {
return changed;
}
/**
* Get total saved blocks for a warzone. This should only be called on nimitz-format warzones.
* @param volume Warzone volume
* @param zoneName Name of zone file
* @return Total saved blocks
* @throws SQLException
*/
public static int getTotalSavedBlocks(ZoneVolume volume, String zoneName) throws SQLException {
File databaseFile = new File(War.war.getDataFolder(), String.format("/dat/warzone-%s/volume-%s.sl3", zoneName, volume.getName()));
Connection databaseConnection = DriverManager.getConnection("jdbc:sqlite:" + databaseFile.getPath());
Statement stmt = databaseConnection.createStatement();
ResultSet sizeQuery = stmt.executeQuery("SELECT COUNT(*) AS total FROM blocks");
int size = sizeQuery.getInt("total");
sizeQuery.close();
stmt.close();
databaseConnection.close();
return size;
}
/**
* Save all war zone blocks to a SQLite3 database file.
*
@ -207,9 +233,10 @@ public class ZoneVolumeMapper {
* @throws SQLException
*/
public static int save(Volume volume, String zoneName) throws SQLException {
long startTime = System.currentTimeMillis();
int changed = 0;
File warzoneDir = new File(War.war.getDataFolder().getPath() + "/dat/warzone-" + zoneName);
if (!warzoneDir.mkdirs()) {
if (!warzoneDir.exists() && !warzoneDir.mkdirs()) {
throw new RuntimeException("Failed to create warzone data directory");
}
File databaseFile = new File(War.war.getDataFolder(), String.format("/dat/warzone-%s/volume-%s.sl3", zoneName, volume.getName()));
@ -230,9 +257,7 @@ public class ZoneVolumeMapper {
cornerStmt.setInt(6, volume.getCornerTwo().getBlockZ());
cornerStmt.executeUpdate();
cornerStmt.close();
//x, y, z, type, data, sign, container, note, record, skull, command, mobid
//x, y, z, type, data, metadata
PreparedStatement dataStmt = databaseConnection.prepareStatement("INSERT INTO blocks VALUES (?, ?, ?, ?, ?, ?)");
PreparedStatement dataStmt = databaseConnection.prepareStatement("INSERT INTO blocks (x, y, z, type, data, metadata) VALUES (?, ?, ?, ?, ?, ?)");
databaseConnection.setAutoCommit(false);
final int batchSize = 10000;
for (int i = 0, x = volume.getMinX(); i < volume.getSizeX(); i++, x++) {
@ -240,6 +265,9 @@ public class ZoneVolumeMapper {
for (int k = 0, z = volume.getMinZ(); k < volume.getSizeZ(); k++, z++) {
// Make sure we are using zone volume-relative coords
final Block block = volume.getWorld().getBlockAt(x, y, z);
if (block.getType() == Material.AIR) {
continue; // Do not save air blocks to the file anymore.
}
final BlockState state = block.getState();
dataStmt.setInt(1, block.getX() - volume.getCornerOne().getBlockX());
dataStmt.setInt(2, block.getY() - volume.getCornerOne().getBlockY());
@ -276,6 +304,10 @@ public class ZoneVolumeMapper {
if (++changed % batchSize == 0) {
dataStmt.executeBatch();
if ((System.currentTimeMillis() - startTime) >= 5000L) {
String seconds = new DecimalFormat("#0.00").format((double) (System.currentTimeMillis() - startTime) / 1000.0D);
War.war.getLogger().log(Level.FINE, "Still saving warzone {0} , {1} seconds elapsed.", new Object[] {zoneName, seconds});
}
}
}
}
@ -284,6 +316,8 @@ public class ZoneVolumeMapper {
databaseConnection.commit();
dataStmt.close();
databaseConnection.close();
String seconds = new DecimalFormat("#0.00").format((double) (System.currentTimeMillis() - startTime) / 1000.0D);
War.war.getLogger().log(Level.INFO, "Saved warzone {0} in {1} seconds.", new Object[] {zoneName, seconds});
return changed;
}

View File

@ -50,21 +50,20 @@ public class ZoneVolume extends Volume {
}
public void loadCorners() throws SQLException {
ZoneVolumeMapper.load(this, this.zone.getName(), this.getWorld(), true, 0, 0);
ZoneVolumeMapper.load(this, this.zone.getName(), this.getWorld(), true, 0, 0, null);
this.isSaved = true;
}
@Override
public void resetBlocks() {
// Load blocks directly from disk and onto the map (i.e. no more in-memory warzone blocks)
int reset = 0;
try {
reset = ZoneVolumeMapper.load(this, this.zone.getName(), this.getWorld(), false, 0, Integer.MAX_VALUE);
ZoneVolumeMapper.load(this, this.zone.getName(), this.getWorld(), false, 0, Integer.MAX_VALUE, null);
} catch (SQLException ex) {
War.war.log("Failed to load warzone " + zone.getName() + ": " + ex.getMessage(), Level.WARNING);
ex.printStackTrace();
}
War.war.log("Reset " + reset + " blocks in warzone " + this.zone.getName() + ".", java.util.logging.Level.INFO);
War.war.log("Reset warzone " + this.zone.getName() + ".", java.util.logging.Level.INFO);
this.isSaved = true;
}
@ -75,10 +74,20 @@ public class ZoneVolume extends Volume {
* Starting position for reset.
* @param total
* Amount of blocks to reset.
* @return Changed block count.
* @throws SQLException
*/
public void resetSection(int start, int total) throws SQLException {
ZoneVolumeMapper.load(this, this.zone.getName(), this.getWorld(), false, start, total);
public int resetSection(int start, int total, boolean[][][] changes) throws SQLException {
return ZoneVolumeMapper.load(this, this.zone.getName(), this.getWorld(), false, start, total, changes);
}
/**
* Get total saved blocks for this warzone. This should only be called on nimitz-format warzones.
* @return Total saved blocks
* @throws SQLException
*/
public int getTotalSavedBlocks() throws SQLException {
return ZoneVolumeMapper.getTotalSavedBlocks(this, this.zone.getName());
}
@Override
@ -87,9 +96,12 @@ public class ZoneVolume extends Volume {
* The job will automatically spawn new instances of itself to run every tick until it is done resetting all blocks.
*/
public void resetBlocksAsJob() {
PartialZoneResetJob job = new PartialZoneResetJob(zone, War.war
.getWarConfig().getInt(WarConfig.RESETSPEED));
War.war.getServer().getScheduler().runTask(War.war, job);
try {
PartialZoneResetJob job = new PartialZoneResetJob(zone, War.war.getWarConfig().getInt(WarConfig.RESETSPEED));
War.war.getServer().getScheduler().runTask(War.war, job);
} catch (SQLException e) {
War.war.getLogger().log(Level.WARNING, "Failed to reset warzone - cannot get count of saved blocks", e);
}
}
public void setNorthwest(Location block) throws NotNorthwestException, TooSmallException, TooBigException {
@ -239,10 +251,7 @@ public class ZoneVolume extends Volume {
private static final int MIN_SIZE = 10;
public boolean tooSmall() {
if (!this.hasTwoCorners()) {
return false;
}
return this.getSizeX() < MIN_SIZE || this.getSizeY() < MIN_SIZE || this.getSizeZ() < MIN_SIZE;
return this.hasTwoCorners() && (this.getSizeX() < MIN_SIZE || this.getSizeY() < MIN_SIZE || this.getSizeZ() < MIN_SIZE);
}
private static final int MAX_SIZE_DEFAULT = 750;