Update + added benchmark tool

This commit is contained in:
Felix Cravic 2020-04-13 17:17:21 +02:00
parent 126d778221
commit c2580789b9
16 changed files with 262 additions and 43 deletions

View File

@ -13,6 +13,8 @@ import fr.themode.minestom.item.Material;
import fr.themode.minestom.net.packet.server.play.DeclareRecipesPacket; import fr.themode.minestom.net.packet.server.play.DeclareRecipesPacket;
import fr.themode.minestom.recipe.RecipeManager; import fr.themode.minestom.recipe.RecipeManager;
import fr.themode.minestom.recipe.ShapelessRecipe; import fr.themode.minestom.recipe.ShapelessRecipe;
import fr.themode.minestom.utils.time.TimeUnit;
import fr.themode.minestom.utils.time.UpdateOption;
public class Main { public class Main {
@ -38,6 +40,7 @@ public class Main {
shapelessRecipe.addIngredient(ingredient); shapelessRecipe.addIngredient(ingredient);
recipeManager.addRecipe(shapelessRecipe); recipeManager.addRecipe(shapelessRecipe);
MinecraftServer.getBenchmarkManager().enable(new UpdateOption(2500, TimeUnit.MILLISECOND));
PlayerInit.init(); PlayerInit.init();

View File

@ -1,7 +1,10 @@
package fr.themode.demo; package fr.themode.demo;
import fr.themode.demo.entity.ChickenCreature;
import fr.themode.demo.generator.ChunkGeneratorDemo; import fr.themode.demo.generator.ChunkGeneratorDemo;
import fr.themode.minestom.MinecraftServer; import fr.themode.minestom.MinecraftServer;
import fr.themode.minestom.benchmark.BenchmarkManager;
import fr.themode.minestom.benchmark.ThreadResult;
import fr.themode.minestom.entity.Entity; import fr.themode.minestom.entity.Entity;
import fr.themode.minestom.entity.EntityCreature; import fr.themode.minestom.entity.EntityCreature;
import fr.themode.minestom.entity.GameMode; import fr.themode.minestom.entity.GameMode;
@ -12,8 +15,14 @@ import fr.themode.minestom.inventory.Inventory;
import fr.themode.minestom.inventory.InventoryType; import fr.themode.minestom.inventory.InventoryType;
import fr.themode.minestom.item.ItemStack; import fr.themode.minestom.item.ItemStack;
import fr.themode.minestom.net.ConnectionManager; import fr.themode.minestom.net.ConnectionManager;
import fr.themode.minestom.timer.TaskRunnable;
import fr.themode.minestom.utils.MathUtils;
import fr.themode.minestom.utils.Position; import fr.themode.minestom.utils.Position;
import fr.themode.minestom.utils.Vector; import fr.themode.minestom.utils.Vector;
import fr.themode.minestom.utils.time.TimeUnit;
import fr.themode.minestom.utils.time.UpdateOption;
import java.util.Map;
public class PlayerInit { public class PlayerInit {
@ -37,6 +46,33 @@ public class PlayerInit {
public static void init() { public static void init() {
ConnectionManager connectionManager = MinecraftServer.getConnectionManager(); ConnectionManager connectionManager = MinecraftServer.getConnectionManager();
BenchmarkManager benchmarkManager = MinecraftServer.getBenchmarkManager();
MinecraftServer.getSchedulerManager().addRepeatingTask(new TaskRunnable() {
@Override
public void run() {
long ramUsage = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
ramUsage /= 1e6; // To MB
String benchmarkMessage = "";
for (Map.Entry<String, ThreadResult> resultEntry : benchmarkManager.getResultMap().entrySet()) {
String name = resultEntry.getKey();
ThreadResult result = resultEntry.getValue();
benchmarkMessage += name;
benchmarkMessage += ": ";
benchmarkMessage += MathUtils.round(result.getCpuPercentage(), 2) + "% CPU ";
benchmarkMessage += MathUtils.round(result.getUserPercentage(), 2) + "% USER ";
benchmarkMessage += MathUtils.round(result.getBlockedPercentage(), 2) + "% BLOCKED ";
benchmarkMessage += "\n";
}
if (benchmarkMessage.length() > 0)
System.out.println(benchmarkMessage);
for (Player player : connectionManager.getOnlinePlayers()) {
player.sendHeaderFooter("RAM USAGE: " + ramUsage + " MB", "", '&');
}
}
}, new UpdateOption(5, TimeUnit.TICK));
connectionManager.setResponseDataConsumer(responseData -> { connectionManager.setResponseDataConsumer(responseData -> {
responseData.setName("1.15.2"); responseData.setName("1.15.2");
@ -78,8 +114,8 @@ public class PlayerInit {
p.teleport(player.getPosition()); p.teleport(player.getPosition());
} }
//ChickenCreature chickenCreature = new ChickenCreature(player.getPosition()); ChickenCreature chickenCreature = new ChickenCreature(player.getPosition());
//chickenCreature.setInstance(player.getInstance()); chickenCreature.setInstance(player.getInstance());
}); });

View File

@ -1,6 +1,7 @@
package fr.themode.minestom; package fr.themode.minestom;
import com.github.simplenet.Server; import com.github.simplenet.Server;
import fr.themode.minestom.benchmark.BenchmarkManager;
import fr.themode.minestom.command.CommandManager; import fr.themode.minestom.command.CommandManager;
import fr.themode.minestom.data.DataManager; import fr.themode.minestom.data.DataManager;
import fr.themode.minestom.entity.EntityManager; import fr.themode.minestom.entity.EntityManager;
@ -23,15 +24,33 @@ import fr.themode.minestom.utils.Utils;
public class MinecraftServer { public class MinecraftServer {
// Thread pools // Threads
public static final String THREAD_NAME_BENCHMARK = "Ms-Benchmark";
public static final String THREAD_NAME_PACKET_WRITER = "Ms-PacketWriterPool";
public static final int THREAD_COUNT_PACKET_WRITER = 2; public static final int THREAD_COUNT_PACKET_WRITER = 2;
public static final String THREAD_NAME_IO = "Ms-IOPool";
public static final int THREAD_COUNT_IO = 2; public static final int THREAD_COUNT_IO = 2;
public static final String THREAD_NAME_BLOCK_BATCH = "Ms-BlockBatchPool";
public static final int THREAD_COUNT_BLOCK_BATCH = 2; public static final int THREAD_COUNT_BLOCK_BATCH = 2;
public static final String THREAD_NAME_BLOCK_UPDATE = "Ms-BlockUpdatePool";
public static final int THREAD_COUNT_BLOCK_UPDATE = 2; public static final int THREAD_COUNT_BLOCK_UPDATE = 2;
public static final String THREAD_NAME_ENTITIES = "Ms-EntitiesPool";
public static final int THREAD_COUNT_ENTITIES = 2; public static final int THREAD_COUNT_ENTITIES = 2;
public static final String THREAD_NAME_ENTITIES_PATHFINDING = "Ms-EntitiesPathFinding";
public static final int THREAD_COUNT_ENTITIES_PATHFINDING = 2; public static final int THREAD_COUNT_ENTITIES_PATHFINDING = 2;
public static final String THREAD_NAME_PLAYERS_ENTITIES = "Ms-PlayersPool";
public static final int THREAD_COUNT_PLAYERS_ENTITIES = 2; public static final int THREAD_COUNT_PLAYERS_ENTITIES = 2;
public static final int THREAD_COUNT_SCHEDULER = 2;
public static final String THREAD_NAME_SCHEDULER = "Ms-SchedulerPool";
public static final int THREAD_COUNT_SCHEDULER = 1;
// Config // Config
public static final int CHUNK_VIEW_DISTANCE = 5; public static final int CHUNK_VIEW_DISTANCE = 5;
public static final int ENTITY_VIEW_DISTANCE = 2; public static final int ENTITY_VIEW_DISTANCE = 2;
@ -55,6 +74,7 @@ public class MinecraftServer {
private static DataManager dataManager; private static DataManager dataManager;
private static TeamManager teamManager; private static TeamManager teamManager;
private static SchedulerManager schedulerManager; private static SchedulerManager schedulerManager;
private static BenchmarkManager benchmarkManager;
private static MinecraftServer minecraftServer; private static MinecraftServer minecraftServer;
@ -71,6 +91,7 @@ public class MinecraftServer {
dataManager = new DataManager(); dataManager = new DataManager();
teamManager = new TeamManager(); teamManager = new TeamManager();
schedulerManager = new SchedulerManager(); schedulerManager = new SchedulerManager();
benchmarkManager = new BenchmarkManager();
server = new Server(); server = new Server();
@ -125,6 +146,10 @@ public class MinecraftServer {
return schedulerManager; return schedulerManager;
} }
public static BenchmarkManager getBenchmarkManager() {
return benchmarkManager;
}
public static ConnectionManager getConnectionManager() { public static ConnectionManager getConnectionManager() {
return connectionManager; return connectionManager;
} }

View File

@ -0,0 +1,122 @@
package fr.themode.minestom.benchmark;
import fr.themode.minestom.MinecraftServer;
import fr.themode.minestom.utils.time.UpdateOption;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.*;
import static fr.themode.minestom.MinecraftServer.*;
import static org.junit.Assert.assertTrue;
public class BenchmarkManager {
public static ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
private static List<String> threads = Arrays.asList(THREAD_NAME_PACKET_WRITER, THREAD_NAME_IO,
THREAD_NAME_BLOCK_BATCH, THREAD_NAME_BLOCK_UPDATE, THREAD_NAME_ENTITIES, THREAD_NAME_ENTITIES_PATHFINDING,
THREAD_NAME_PLAYERS_ENTITIES, THREAD_NAME_SCHEDULER);
static {
assertTrue(threadMXBean.isThreadCpuTimeSupported());
assertTrue(threadMXBean.isCurrentThreadCpuTimeSupported());
threadMXBean.setThreadContentionMonitoringEnabled(true);
threadMXBean.setThreadCpuTimeEnabled(true);
assertTrue(threadMXBean.isThreadCpuTimeEnabled());
}
private Map<Long, Long> lastCpuTimeMap = new HashMap<>();
private Map<Long, Long> lastUserTimeMap = new HashMap<>();
private Map<Long, Long> lastBlockedMap = new HashMap<>();
private Map<String, ThreadResult> resultMap = new HashMap<>();
private boolean enabled = false;
private volatile boolean stop = false;
private UpdateOption updateOption;
private Thread thread;
private long time;
public void enable(UpdateOption updateOption) {
if (enabled)
throw new IllegalStateException("A benchmark is already running, please disable it first.");
this.updateOption = updateOption;
time = updateOption.getTimeUnit().toMilliseconds(updateOption.getValue());
this.thread = new Thread(null, () -> {
while (!stop) {
refreshData();
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
stop = false;
}, MinecraftServer.THREAD_NAME_BENCHMARK, 0L);
this.thread.start();
this.enabled = true;
}
public void disable() {
this.stop = true;
this.enabled = false;
}
public Map<String, ThreadResult> getResultMap() {
return Collections.unmodifiableMap(resultMap);
}
private void refreshData() {
ThreadInfo[] threadInfo = threadMXBean.getThreadInfo(threadMXBean.getAllThreadIds());
for (ThreadInfo threadInfo2 : threadInfo) {
String name = threadInfo2.getThreadName();
boolean shouldBenchmark = false;
for (String thread : threads) {
if (name.startsWith(thread)) {
shouldBenchmark = true;
break;
}
}
if (!shouldBenchmark)
continue;
long id = threadInfo2.getThreadId();
long lastCpuTime = lastCpuTimeMap.getOrDefault(id, 0L);
long lastUserTime = lastUserTimeMap.getOrDefault(id, 0L);
long lastBlockedTime = lastBlockedMap.getOrDefault(id, 0L);
long blockedTime = threadInfo2.getBlockedTime();
//long waitedTime = threadInfo2.getWaitedTime();
long cpuTime = threadMXBean.getThreadCpuTime(id);
long userTime = threadMXBean.getThreadUserTime(id);
lastCpuTimeMap.put(id, cpuTime);
lastUserTimeMap.put(id, userTime);
lastBlockedMap.put(id, blockedTime);
double totalCpuTime = (double) (cpuTime - lastCpuTime) / 1000000D;
double totalUserTime = (double) (userTime - lastUserTime) / 1000000D;
long totalBlocked = blockedTime - lastBlockedTime;
double cpuPercentage = totalCpuTime / (double) time * 100L;
double userPercentage = totalUserTime / (double) time * 100L;
double blockedPercentage = totalBlocked / (double) time * 100L;
ThreadResult threadResult = new ThreadResult(cpuPercentage, userPercentage, blockedPercentage);
resultMap.put(name, threadResult);
}
}
}

View File

@ -0,0 +1,24 @@
package fr.themode.minestom.benchmark;
public class ThreadResult {
private double cpuPercentage, userPercentage, blockedPercentage;
protected ThreadResult(double cpuPercentage, double userPercentage, double blockedPercentage) {
this.cpuPercentage = cpuPercentage;
this.userPercentage = userPercentage;
this.blockedPercentage = blockedPercentage;
}
public double getCpuPercentage() {
return cpuPercentage;
}
public double getUserPercentage() {
return userPercentage;
}
public double getBlockedPercentage() {
return blockedPercentage;
}
}

View File

@ -18,8 +18,8 @@ public class EntityManager {
private UpdateType updateType = UpdateType.PER_INSTANCE; private UpdateType updateType = UpdateType.PER_INSTANCE;
private Set<Instance> instances = instanceManager.getInstances(); private Set<Instance> instances = instanceManager.getInstances();
private ExecutorService entitiesPool = new MinestomThread(MinecraftServer.THREAD_COUNT_ENTITIES, "Ms-EntitiesPool"); private ExecutorService entitiesPool = new MinestomThread(MinecraftServer.THREAD_COUNT_ENTITIES, MinecraftServer.THREAD_NAME_ENTITIES);
private ExecutorService playersPool = new MinestomThread(MinecraftServer.THREAD_COUNT_PLAYERS_ENTITIES, "Ms-PlayersPool"); private ExecutorService playersPool = new MinestomThread(MinecraftServer.THREAD_COUNT_PLAYERS_ENTITIES, MinecraftServer.THREAD_NAME_PLAYERS_ENTITIES);
private ConcurrentLinkedQueue<Player> waitingPlayers = new ConcurrentLinkedQueue<>(); private ConcurrentLinkedQueue<Player> waitingPlayers = new ConcurrentLinkedQueue<>();
@ -187,6 +187,6 @@ public class EntityManager {
PER_CHUNK, PER_CHUNK,
PER_ENTITY_TYPE, PER_ENTITY_TYPE,
PER_INSTANCE, PER_INSTANCE,
SINGLE_THREADED; SINGLE_THREADED
} }
} }

View File

@ -12,7 +12,7 @@ import java.util.function.Consumer;
public class EntityPathFinder { public class EntityPathFinder {
private ExecutorService pathfindingPool = new MinestomThread(MinecraftServer.THREAD_COUNT_ENTITIES_PATHFINDING, "Ms-EntitiesPathFinding"); private ExecutorService pathfindingPool = new MinestomThread(MinecraftServer.THREAD_COUNT_ENTITIES_PATHFINDING, MinecraftServer.THREAD_NAME_ENTITIES_PATHFINDING);
private Entity entity; private Entity entity;

View File

@ -59,9 +59,8 @@ public class InstanceContainer extends Instance {
chunk.UNSAFE_setBlock(index, blockId, data); chunk.UNSAFE_setBlock(index, blockId, data);
executeNeighboursBlockPlacementRule(blockId, blockPosition); executeNeighboursBlockPlacementRule(blockPosition);
// TODO instead of sending a block change packet each time, cache changed blocks and flush them every tick with a MultiBlockChangePacket
sendBlockChange(chunk, x, y, z, blockId); sendBlockChange(chunk, x, y, z, blockId);
} }
} }
@ -83,11 +82,10 @@ public class InstanceContainer extends Instance {
chunk.UNSAFE_setCustomBlock(index, blockId, data); chunk.UNSAFE_setCustomBlock(index, blockId, data);
executeNeighboursBlockPlacementRule(blockId, blockPosition); executeNeighboursBlockPlacementRule(blockPosition);
callBlockPlace(chunk, index, x, y, z); callBlockPlace(chunk, index, x, y, z);
// TODO instead of sending a block change packet each time, cache changed blocks and flush them every tick with a MultiBlockChangePacket
short id = BLOCK_MANAGER.getBlock(blockId).getType(); short id = BLOCK_MANAGER.getBlock(blockId).getType();
sendBlockChange(chunk, x, y, z, id); sendBlockChange(chunk, x, y, z, id);
} }
@ -132,7 +130,7 @@ public class InstanceContainer extends Instance {
return blockId; return blockId;
} }
private void executeNeighboursBlockPlacementRule(short blockId, BlockPosition blockPosition) { private void executeNeighboursBlockPlacementRule(BlockPosition blockPosition) {
for (int offsetX = -1; offsetX < 2; offsetX++) { for (int offsetX = -1; offsetX < 2; offsetX++) {
for (int offsetY = -1; offsetY < 2; offsetY++) { for (int offsetY = -1; offsetY < 2; offsetY++) {
for (int offsetZ = -1; offsetZ < 2; offsetZ++) { for (int offsetZ = -1; offsetZ < 2; offsetZ++) {

View File

@ -12,7 +12,7 @@ import java.util.concurrent.ExecutorService;
public class InstanceManager { public class InstanceManager {
private ExecutorService blocksPool = new MinestomThread(MinecraftServer.THREAD_COUNT_BLOCK_UPDATE, "Ms-BlockUpdatePool"); private ExecutorService blocksPool = new MinestomThread(MinecraftServer.THREAD_COUNT_BLOCK_UPDATE, MinecraftServer.THREAD_NAME_BLOCK_UPDATE);
private Set<Instance> instances = Collections.synchronizedSet(new HashSet<>()); private Set<Instance> instances = Collections.synchronizedSet(new HashSet<>());

View File

@ -8,6 +8,6 @@ import java.util.concurrent.ExecutorService;
public interface InstanceBatch extends BlockModifier { public interface InstanceBatch extends BlockModifier {
ExecutorService batchesPool = new MinestomThread(MinecraftServer.THREAD_COUNT_BLOCK_BATCH, "Ms-BlockBatchPool"); ExecutorService batchesPool = new MinestomThread(MinecraftServer.THREAD_COUNT_BLOCK_BATCH, MinecraftServer.THREAD_NAME_BLOCK_BATCH);
} }

View File

@ -56,6 +56,8 @@ public class RedstonePlacementRule extends BlockPlacementRule {
north = "side"; north = "side";
} }
// TODO power
return Block.REDSTONE_WIRE.withProperties(east, north, power, south, west); return Block.REDSTONE_WIRE.withProperties(east, north, power, south, west);
} }

View File

@ -97,7 +97,7 @@ public class Inventory implements InventoryModifier, InventoryClickHandler, View
public void update() { public void update() {
WindowItemsPacket windowItemsPacket = getWindowItemsPacket(); WindowItemsPacket windowItemsPacket = getWindowItemsPacket();
getViewers().forEach(p -> p.getPlayerConnection().sendPacket(windowItemsPacket)); sendPacketToViewers(windowItemsPacket);
} }
@Override @Override
@ -129,7 +129,7 @@ public class Inventory implements InventoryModifier, InventoryClickHandler, View
setSlotPacket.windowId = 1; setSlotPacket.windowId = 1;
setSlotPacket.slot = (short) slot; setSlotPacket.slot = (short) slot;
setSlotPacket.itemStack = itemStack; setSlotPacket.itemStack = itemStack;
getViewers().forEach(player -> player.getPlayerConnection().sendPacket(setSlotPacket)); sendPacketToViewers(setSlotPacket);
} }
} }

View File

@ -7,7 +7,7 @@ import java.util.concurrent.ExecutorService;
public class IOManager { public class IOManager {
private static final ExecutorService IO_POOL = new MinestomThread(MinecraftServer.THREAD_COUNT_IO, "Ms-IOPool"); private static final ExecutorService IO_POOL = new MinestomThread(MinecraftServer.THREAD_COUNT_IO, MinecraftServer.THREAD_NAME_IO);
public static void submit(Runnable runnable) { public static void submit(Runnable runnable) {
IO_POOL.execute(runnable); IO_POOL.execute(runnable);

View File

@ -14,7 +14,7 @@ import java.util.function.Consumer;
public class PacketWriterUtils { public class PacketWriterUtils {
private static ExecutorService batchesPool = new MinestomThread(MinecraftServer.THREAD_COUNT_PACKET_WRITER, "Ms-PacketWriterPool"); private static ExecutorService batchesPool = new MinestomThread(MinecraftServer.THREAD_COUNT_PACKET_WRITER, MinecraftServer.THREAD_NAME_PACKET_WRITER);
public static void writeCallbackPacket(ServerPacket serverPacket, Consumer<Packet> consumer) { public static void writeCallbackPacket(ServerPacket serverPacket, Consumer<Packet> consumer) {
batchesPool.execute(() -> { batchesPool.execute(() -> {

View File

@ -5,7 +5,6 @@ import fr.themode.minestom.utils.thread.MinestomThread;
import fr.themode.minestom.utils.time.CooldownUtils; import fr.themode.minestom.utils.time.CooldownUtils;
import fr.themode.minestom.utils.time.UpdateOption; import fr.themode.minestom.utils.time.UpdateOption;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@ -14,7 +13,7 @@ import java.util.concurrent.atomic.AtomicInteger;
public class SchedulerManager { public class SchedulerManager {
private static final AtomicInteger COUNTER = new AtomicInteger(); private static final AtomicInteger COUNTER = new AtomicInteger();
private static ExecutorService batchesPool = new MinestomThread(MinecraftServer.THREAD_COUNT_SCHEDULER, "Ms-SchedulerPool"); private static ExecutorService batchesPool = new MinestomThread(MinecraftServer.THREAD_COUNT_SCHEDULER, MinecraftServer.THREAD_NAME_SCHEDULER);
private List<Task> tasks = new CopyOnWriteArrayList<>(); private List<Task> tasks = new CopyOnWriteArrayList<>();
public int addTask(TaskRunnable runnable, UpdateOption updateOption, int maxCallCount) { public int addTask(TaskRunnable runnable, UpdateOption updateOption, int maxCallCount) {
@ -36,36 +35,28 @@ public class SchedulerManager {
} }
public void removeTask(int taskId) { public void removeTask(int taskId) {
synchronized (tasks) { this.tasks.removeIf(task -> task.getId() == taskId);
this.tasks.removeIf(task -> task.getId() == taskId);
}
} }
public void update() { public void update() {
long time = System.currentTimeMillis(); long time = System.currentTimeMillis();
batchesPool.execute(() -> { batchesPool.execute(() -> {
for (Task task : tasks) {
UpdateOption updateOption = task.getUpdateOption();
long lastUpdate = task.getLastUpdateTime();
boolean hasCooldown = CooldownUtils.hasCooldown(time, lastUpdate, updateOption.getTimeUnit(), updateOption.getValue());
if (!hasCooldown) {
TaskRunnable runnable = task.getRunnable();
int maxCallCount = task.getMaxCallCount();
int callCount = runnable.getCallCount() + 1;
runnable.setCallCount(callCount);
synchronized (tasks) { runnable.run();
Iterator<Task> iterator = tasks.iterator();
while (iterator.hasNext()) {
Task task = iterator.next();
UpdateOption updateOption = task.getUpdateOption(); task.refreshLastUpdateTime(time);
long lastUpdate = task.getLastUpdateTime();
boolean hasCooldown = CooldownUtils.hasCooldown(time, lastUpdate, updateOption.getTimeUnit(), updateOption.getValue());
if (!hasCooldown) {
TaskRunnable runnable = task.getRunnable();
int maxCallCount = task.getMaxCallCount();
int callCount = runnable.getCallCount() + 1;
runnable.setCallCount(callCount);
runnable.run(); if (callCount == maxCallCount) {
tasks.remove(task);
task.refreshLastUpdateTime(time);
if (callCount == maxCallCount) {
iterator.remove();
}
} }
} }
} }

View File

@ -10,4 +10,22 @@ public class MathUtils {
return num * num; return num * num;
} }
public static double round(double value, int places) {
if (places < 0) throw new IllegalArgumentException();
long factor = (long) Math.pow(10, places);
value = value * factor;
long tmp = Math.round(value);
return (double) tmp / factor;
}
public static float round(float value, int places) {
if (places < 0) throw new IllegalArgumentException();
long factor = (long) Math.pow(10, places);
value = value * factor;
long tmp = Math.round(value);
return (float) tmp / factor;
}
} }