From 890dd16ec66ac3989a7ab716fd05c045a732cee1 Mon Sep 17 00:00:00 2001 From: OLEGSHA Date: Wed, 6 Jan 2021 21:26:19 +0300 Subject: [PATCH] Added DynamicStrings to minimize String.format invocations in GUI --- .../common/util/dynstr/DoubleFlusher.java | 84 ++++++++++ .../common/util/dynstr/DynamicString.java | 138 ++++++++++++++++ .../common/util/dynstr/DynamicStrings.java | 155 ++++++++++++++++++ .../common/util/dynstr/FloatFlusher.java | 84 ++++++++++ .../common/util/dynstr/IntFlusher.java | 115 +++++++++++++ .../progressia/test/LayerTestGUI.java | 31 ++-- 6 files changed, 596 insertions(+), 11 deletions(-) create mode 100644 src/main/java/ru/windcorp/progressia/common/util/dynstr/DoubleFlusher.java create mode 100644 src/main/java/ru/windcorp/progressia/common/util/dynstr/DynamicString.java create mode 100644 src/main/java/ru/windcorp/progressia/common/util/dynstr/DynamicStrings.java create mode 100644 src/main/java/ru/windcorp/progressia/common/util/dynstr/FloatFlusher.java create mode 100644 src/main/java/ru/windcorp/progressia/common/util/dynstr/IntFlusher.java diff --git a/src/main/java/ru/windcorp/progressia/common/util/dynstr/DoubleFlusher.java b/src/main/java/ru/windcorp/progressia/common/util/dynstr/DoubleFlusher.java new file mode 100644 index 0000000..7ab5d54 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/common/util/dynstr/DoubleFlusher.java @@ -0,0 +1,84 @@ +package ru.windcorp.progressia.common.util.dynstr; + +import gnu.trove.list.TCharList; + +class DoubleFlusher { + + public static void flushDouble(TCharList sink, double number, int width, int precision, boolean alwaysUseSign) { + boolean isSignNeeded = !Double.isNaN(number) && !isZero(number, precision) && (number < 0 || alwaysUseSign); + int size = getSize(number, precision, isSignNeeded); + + int needChars = Math.max(width, size); + reserve(sink, needChars); + + int charPos = flushDigits(number, precision, sink); + + if (isSignNeeded) { + sink.set(--charPos, number > 0 ? '+' : '-'); + } + } + + private static boolean isZero(double number, int precision) { + int digits = (int) Math.floor(number * pow10(precision)); + return digits == 0; + } + + private static final char[] NaN_CHARS = "NaN".toCharArray(); + private static final char[] INFINITY_CHARS = "Infinity".toCharArray(); + + private static int getSize(double number, int precision, boolean isSignNeeded) { + if (Double.isNaN(number)) return NaN_CHARS.length; + if (number == Double.POSITIVE_INFINITY) return (isSignNeeded ? 1 : 0) + INFINITY_CHARS.length; + + int integer = (int) Math.floor(Math.abs(number)); + return (isSignNeeded ? 1 : 0) + IntFlusher.stringSize(integer) + 1 + precision; + } + + private static void reserve(TCharList sink, int needChars) { + for (int i = 0; i < needChars; ++i) { + sink.add(' '); + } + } + + private static int flushDigits(double number, int precision, TCharList sink) { + if (Double.isFinite(number)) { + return flushFiniteDigits(number, precision, sink); + } else { + return flushNonFiniteDigits(number, sink); + } + } + + private static int flushFiniteDigits(double number, int precision, TCharList sink) { + number = Math.abs(number); + + int integer = (int) Math.floor(number); + int fraction = (int) Math.floor((number - Math.floor(number)) * pow10(precision)); + + int charPos = IntFlusher.flushDigits(fraction, sink, sink.size()); + sink.set(--charPos, '.'); + charPos = IntFlusher.flushDigits(integer, sink, charPos); + + return charPos; + } + + private static double pow10(int precision) { + double result = 1; + for (int i = 0; i < precision; ++i) result *= 10; + return result; + } + + private static int flushNonFiniteDigits(double number, TCharList sink) { + final char[] chars; + + if (Double.isNaN(number)) { + chars = NaN_CHARS; + } else { + chars = INFINITY_CHARS; + } + + int offset = sink.size() - chars.length; + sink.set(offset, chars); + return offset; + } + +} diff --git a/src/main/java/ru/windcorp/progressia/common/util/dynstr/DynamicString.java b/src/main/java/ru/windcorp/progressia/common/util/dynstr/DynamicString.java new file mode 100644 index 0000000..5c57a14 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/common/util/dynstr/DynamicString.java @@ -0,0 +1,138 @@ +package ru.windcorp.progressia.common.util.dynstr; + +import java.util.function.Supplier; + +import gnu.trove.list.TCharList; +import gnu.trove.list.array.TCharArrayList; +import ru.windcorp.jputil.chars.CharConsumer; + +public final class DynamicString implements CharSequence { + + interface Part { + void flush(TCharList sink); + } + + @FunctionalInterface + public interface CharFlusherPart { + void flush(CharConsumer sink); + } + + final TCharList chars = new TCharArrayList(); + final Part[] parts; + + private int hashCode = 0; + + DynamicString(Part[] parts) { + this.parts = parts; + } + + /** + * Causes the contents of this string to be reevaluated. + * This is not currently thread-safe, take caution. + */ + public void update() { + chars.clear(); + hashCode = 0; + + for (Part part : parts) { + part.flush(chars); + } + } + + public Supplier asSupplier() { + return () -> { + update(); + return this; + }; + } + + @Override + public int length() { + return chars.size(); + } + + @Override + public char charAt(int index) { + return chars.get(index); + } + + @Override + public CharSequence subSequence(int start, int end) { + return new SubString(start, end); + } + + @Override + public String toString() { + int length = length(); + + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; ++i) { + sb.append(chars.get(i)); + } + + return sb.toString(); + } + + @Override + public int hashCode() { + int h = hashCode; + int length = length(); + + if (h != 0 || length == 0) return h; + + for (int i = 0; i < length; i++) { + h = 31 * h + this.chars.get(i); + } + + hashCode = h; + return h; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (obj.getClass() != getClass()) return false; + + DynamicString other = (DynamicString) obj; + + if (hashCode() != this.hashCode()) return false; + + return other.chars.equals(this.chars); + } + + private class SubString implements CharSequence { + + private final int start; + private final int end; + + public SubString(int start, int end) { + this.start = start; + this.end = end; + } + + @Override + public int length() { + return Math.min(end, DynamicString.this.length()) - start; + } + @Override + public char charAt(int index) { + if (index < 0 || index > length()) { + throw new IndexOutOfBoundsException(Integer.toString(index) + " is out of bounds"); + } + + return DynamicString.this.charAt(index); + } + @Override + public CharSequence subSequence(int start, int end) { + if (start < 0) throw new IllegalArgumentException("start (" + start + ") is negative"); + if (end < start) throw new IllegalArgumentException("end (" + end + ") < start (" + start + ")"); + + int absoluteStart = this.start + start; + int absoluteEnd = this.start + end; + + return DynamicString.this.subSequence(absoluteStart, absoluteEnd); + } + + } + +} diff --git a/src/main/java/ru/windcorp/progressia/common/util/dynstr/DynamicStrings.java b/src/main/java/ru/windcorp/progressia/common/util/dynstr/DynamicStrings.java new file mode 100644 index 0000000..f92ca17 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/common/util/dynstr/DynamicStrings.java @@ -0,0 +1,155 @@ +package ru.windcorp.progressia.common.util.dynstr; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.DoubleSupplier; +import java.util.function.IntSupplier; +import java.util.function.Supplier; + +import ru.windcorp.jputil.chars.CharConsumer; +import ru.windcorp.jputil.functions.FloatSupplier; + +public class DynamicStrings { + + @FunctionalInterface + public interface CharSource { + void flush(CharConsumer sink); + } + + @FunctionalInterface + public interface StringSupplier { + String get(); + } + + public static class Builder { + + private final List parts = new ArrayList<>(); + + public DynamicString build() { + return new DynamicString(parts.toArray(new DynamicString.Part[parts.size()])); + } + + public Supplier buildSupplier() { + return build().asSupplier(); + } + + public Builder addConst(Object constant) { + return add(constant.toString()); + } + + public Builder add(String string) { + return add(string.toCharArray()); + } + + public Builder add(final char[] chars) { + parts.add(sink -> sink.add(chars)); + return this; + } + + public Builder add(char c) { + parts.add(sink -> sink.add(c)); + return this; + } + + public Builder addDyn(Object obj) { + if (obj == null) return add("null"); + return addDyn(obj::toString); + } + + public Builder embed(DynamicString str) { + if (str == null) return add("null"); + + for (DynamicString.Part p : str.parts) { + parts.add(p); + } + + return this; + } + + public Builder addDyn(Supplier supplier) { + Objects.requireNonNull(supplier, "supplier"); + return addDyn(() -> Objects.toString(supplier.get())); + } + + public Builder addDyn(StringSupplier supplier) { + Objects.requireNonNull(supplier, "supplier"); + + parts.add(sink -> { + String str = supplier.get(); + int length = str.length(); + + for (int i = 0; i < length; ++i) { + sink.add(str.charAt(i)); + } + }); + + return this; + } + + public Builder addDyn(IntSupplier supplier, int width, boolean alwaysUseSign) { + Objects.requireNonNull(supplier, "supplier"); + + parts.add(sink -> IntFlusher.flushInt(sink, supplier.getAsInt(), width, alwaysUseSign)); + return this; + } + + public Builder addDyn(IntSupplier supplier, int width) { + return addDyn(supplier, width, false); + } + + public Builder addDyn(IntSupplier supplier, boolean alwaysUseSign) { + return addDyn(supplier, 0, alwaysUseSign); + } + + public Builder addDyn(IntSupplier supplier) { + return addDyn(supplier, 0, false); + } + + public Builder addDyn(DoubleSupplier supplier, int width, int precision, boolean alwaysUseSign) { + Objects.requireNonNull(supplier, "supplier"); + + parts.add(sink -> DoubleFlusher.flushDouble(sink, supplier.getAsDouble(), width, precision, alwaysUseSign)); + return this; + } + + public Builder addDyn(DoubleSupplier supplier, int width, int precision) { + return addDyn(supplier, width, precision, false); + } + + public Builder addDyn(DoubleSupplier supplier, boolean alwaysUseSign, int precision) { + return addDyn(supplier, 0, precision, alwaysUseSign); + } + + public Builder addDyn(DoubleSupplier supplier, int precision) { + return addDyn(supplier, 0, precision, false); + } + + public Builder addDyn(FloatSupplier supplier, int width, int precision, boolean alwaysUseSign) { + Objects.requireNonNull(supplier, "supplier"); + + parts.add(sink -> FloatFlusher.flushFloat(sink, supplier.getAsFloat(), width, precision, alwaysUseSign)); + return this; + } + + public Builder addDyn(FloatSupplier supplier, int width, int precision) { + return addDyn(supplier, width, precision, false); + } + + public Builder addDyn(FloatSupplier supplier, boolean alwaysUseSign, int precision) { + return addDyn(supplier, 0, precision, alwaysUseSign); + } + + public Builder addDyn(FloatSupplier supplier, int precision) { + return addDyn(supplier, 0, precision, false); + } + + } + + public static Builder builder() { + return new Builder(); + } + + private DynamicStrings() {} + +} diff --git a/src/main/java/ru/windcorp/progressia/common/util/dynstr/FloatFlusher.java b/src/main/java/ru/windcorp/progressia/common/util/dynstr/FloatFlusher.java new file mode 100644 index 0000000..c1128da --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/common/util/dynstr/FloatFlusher.java @@ -0,0 +1,84 @@ +package ru.windcorp.progressia.common.util.dynstr; + +import gnu.trove.list.TCharList; + +class FloatFlusher { + + public static void flushFloat(TCharList sink, float number, int width, int precision, boolean alwaysUseSign) { + boolean isSignNeeded = !Float.isNaN(number) && !isZero(number, precision) && (number < 0 || alwaysUseSign); + int size = getSize(number, precision, isSignNeeded); + + int needChars = Math.max(width, size); + reserve(sink, needChars); + + int charPos = flushDigits(number, precision, sink); + + if (isSignNeeded) { + sink.set(--charPos, number > 0 ? '+' : '-'); + } + } + + private static boolean isZero(float number, int precision) { + int digits = (int) Math.floor(number * pow10(precision)); + return digits == 0; + } + + private static final char[] NaN_CHARS = "NaN".toCharArray(); + private static final char[] INFINITY_CHARS = "Infinity".toCharArray(); + + private static int getSize(float number, int precision, boolean isSignNeeded) { + if (Float.isNaN(number)) return NaN_CHARS.length; + if (number == Float.POSITIVE_INFINITY) return (isSignNeeded ? 1 : 0) + INFINITY_CHARS.length; + + int integer = (int) Math.floor(Math.abs(number)); + return (isSignNeeded ? 1 : 0) + IntFlusher.stringSize(integer) + 1 + precision; + } + + private static void reserve(TCharList sink, int needChars) { + for (int i = 0; i < needChars; ++i) { + sink.add(' '); + } + } + + private static int flushDigits(float number, int precision, TCharList sink) { + if (Float.isFinite(number)) { + return flushFiniteDigits(number, precision, sink); + } else { + return flushNonFiniteDigits(number, sink); + } + } + + private static int flushFiniteDigits(float number, int precision, TCharList sink) { + number = Math.abs(number); + + int integer = (int) Math.floor(number); + int fraction = (int) Math.floor((number - Math.floor(number)) * pow10(precision)); + + int charPos = IntFlusher.flushDigits(fraction, sink, sink.size()); + sink.set(--charPos, '.'); + charPos = IntFlusher.flushDigits(integer, sink, charPos); + + return charPos; + } + + private static float pow10(int precision) { + float result = 1; + for (int i = 0; i < precision; ++i) result *= 10; + return result; + } + + private static int flushNonFiniteDigits(float number, TCharList sink) { + final char[] chars; + + if (Float.isNaN(number)) { + chars = NaN_CHARS; + } else { + chars = INFINITY_CHARS; + } + + int offset = sink.size() - chars.length; + sink.set(offset, chars); + return offset; + } + +} diff --git a/src/main/java/ru/windcorp/progressia/common/util/dynstr/IntFlusher.java b/src/main/java/ru/windcorp/progressia/common/util/dynstr/IntFlusher.java new file mode 100644 index 0000000..d838ca4 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/common/util/dynstr/IntFlusher.java @@ -0,0 +1,115 @@ +/* + * The algorithm implemented in this class is adapted from OpenJDK's Integer.toString(int) implementation. + * This class therefore falls under the GNU GPL v2 only license. + */ +package ru.windcorp.progressia.common.util.dynstr; + +import gnu.trove.list.TCharList; + +class IntFlusher { + + public static void flushInt(TCharList sink, int number, int width, boolean alwaysUseSign) { + int size = stringSize(number); + + boolean isSignNeeded = number != 0 && (number < 0 || alwaysUseSign); + if (isSignNeeded) { + size++; + } + + int needChars = Math.max(size, width); + reserve(sink, needChars); + + int charPos = flushDigits(number, sink, sink.size()); + + if (isSignNeeded) { + sink.set(--charPos, number > 0 ? '+' : '-'); + } + } + + /* + * Copied from OpenJDK's Integer.stringSize(int) + */ + public static int stringSize(int x) { + int d = 1; + if (x >= 0) { + d = 0; + x = -x; + } + int p = -10; + for (int i = 1; i < 10; i++) { + if (x > p) + return i + d; + p = 10 * p; + } + return 10 + d; + } + + /* + * Copied from OpenJDK's Integer.DigitTens and Integer.DigitOnes + */ + private static final char[] DIGIT_TENS = { + '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', + '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', + '2', '2', '2', '2', '2', '2', '2', '2', '2', '2', + '3', '3', '3', '3', '3', '3', '3', '3', '3', '3', + '4', '4', '4', '4', '4', '4', '4', '4', '4', '4', + '5', '5', '5', '5', '5', '5', '5', '5', '5', '5', + '6', '6', '6', '6', '6', '6', '6', '6', '6', '6', + '7', '7', '7', '7', '7', '7', '7', '7', '7', '7', + '8', '8', '8', '8', '8', '8', '8', '8', '8', '8', + '9', '9', '9', '9', '9', '9', '9', '9', '9', '9', + }; + + private static final char[] DIGIT_ONES = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + }; + + /* + * Adapted from OpenJDK's Integer.getChars(int, int, byte[]) + */ + public static int flushDigits(int number, TCharList output, int endIndex) { + int q, r; + int charPos = endIndex; + + if (number >= 0) { + number = -number; + } + + // Generate two digits per iteration + while (number <= -100) { + q = number / 100; + r = (q * 100) - number; + number = q; + output.set(--charPos, DIGIT_ONES[r]); + output.set(--charPos, DIGIT_TENS[r]); + } + + // We know there are at most two digits left at this point. + q = number / 10; + r = (q * 10) - number; + output.set(--charPos, (char) ('0' + r)); + + // Whatever left is the remaining digit. + if (q < 0) { + output.set(--charPos, (char) ('0' - q)); + } + + return charPos; + } + + private static void reserve(TCharList sink, int needChars) { + for (int i = 0; i < needChars; ++i) { + sink.add(' '); + } + } + +} diff --git a/src/main/java/ru/windcorp/progressia/test/LayerTestGUI.java b/src/main/java/ru/windcorp/progressia/test/LayerTestGUI.java index b087001..d2bf0ed 100755 --- a/src/main/java/ru/windcorp/progressia/test/LayerTestGUI.java +++ b/src/main/java/ru/windcorp/progressia/test/LayerTestGUI.java @@ -19,7 +19,7 @@ package ru.windcorp.progressia.test; import java.util.ArrayList; import java.util.Collection; -import java.util.Locale; +import java.util.function.Supplier; import glm.vec._3.Vec3; import ru.windcorp.progressia.client.Client; @@ -33,6 +33,7 @@ import ru.windcorp.progressia.client.graphics.gui.Panel; import ru.windcorp.progressia.client.graphics.gui.layout.LayoutAlign; import ru.windcorp.progressia.client.graphics.gui.layout.LayoutVertical; import ru.windcorp.progressia.common.Units; +import ru.windcorp.progressia.common.util.dynstr.DynamicStrings; import ru.windcorp.progressia.server.Server; import ru.windcorp.progressia.server.ServerState; @@ -67,7 +68,7 @@ public class LayerTestGUI extends GUILayer { panel.addChild(new DynamicLabel( "FPSDisplay", new Font().withColor(0xFF37A3E6).deriveShadow(), - LayerTestGUI::getFPS, + DynamicStrings.builder().add("FPS: ").addDyn(() -> FPS_RECORD.update(GraphicsInterface.getFPS()), 5, 1).buildSupplier(), 128 )); @@ -79,7 +80,7 @@ public class LayerTestGUI extends GUILayer { panel.addChild(new DynamicLabel( "ChunkUpdatesDisplay", new Font().withColor(0xFF37A3E6).deriveShadow(), - () -> "Pending updates: " + Integer.toString(ClientState.getInstance().getWorld().getPendingChunkUpdates()), + DynamicStrings.builder().addConst("Pending updates: ").addDyn(ClientState.getInstance().getWorld()::getPendingChunkUpdates).buildSupplier(), 128 )); @@ -148,26 +149,34 @@ public class LayerTestGUI extends GUILayer { private static final Averager FPS_RECORD = new Averager(); private static final Averager TPS_RECORD = new Averager(); - private static String getFPS() { - return String.format(Locale.US, "FPS: %5.1f", FPS_RECORD.update(GraphicsInterface.getFPS())); - } + private static final Supplier TPS_STRING = DynamicStrings.builder() + .add("TPS: ") + .addDyn(() -> TPS_RECORD.update(ServerState.getInstance().getTPS()), 5, 1) + .buildSupplier(); - private static String getTPS() { + private static final Supplier POS_STRING = DynamicStrings.builder() + .add("Pos: ") + .addDyn(() -> ClientState.getInstance().getCamera().getLastAnchorPosition().x, 7, 1) + .addDyn(() -> ClientState.getInstance().getCamera().getLastAnchorPosition().y, 7, 1) + .addDyn(() -> ClientState.getInstance().getCamera().getLastAnchorPosition().z, 7, 1) + .buildSupplier(); + + private static CharSequence getTPS() { Server server = ServerState.getInstance(); if (server == null) return "TPS: n/a"; - return String.format(Locale.US, "TPS: %5.1f", TPS_RECORD.update(server.getTPS())); + return TPS_STRING.get(); } - private static String getPos() { + private static CharSequence getPos() { Client client = ClientState.getInstance(); - if (client == null) return "Pos: n/a"; + if (client == null) return "Pos: client n/a"; Vec3 pos = client.getCamera().getLastAnchorPosition(); if (Float.isNaN(pos.x)) { return "Pos: entity n/a"; } else { - return String.format(Locale.US, "Pos: %+7.1f %+7.1f %+7.1f", pos.x, pos.y, pos.z); + return POS_STRING.get(); } }