Added DynamicStrings to minimize String.format invocations in GUI

This commit is contained in:
OLEGSHA 2021-01-06 21:26:19 +03:00
parent 755a6932cf
commit 890dd16ec6
6 changed files with 596 additions and 11 deletions

View File

@ -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;
}
}

View File

@ -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<CharSequence> 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);
}
}
}

View File

@ -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<DynamicString.Part> parts = new ArrayList<>();
public DynamicString build() {
return new DynamicString(parts.toArray(new DynamicString.Part[parts.size()]));
}
public Supplier<CharSequence> 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() {}
}

View File

@ -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;
}
}

View File

@ -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(' ');
}
}
}

View File

@ -19,7 +19,7 @@ package ru.windcorp.progressia.test;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Locale; import java.util.function.Supplier;
import glm.vec._3.Vec3; import glm.vec._3.Vec3;
import ru.windcorp.progressia.client.Client; 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.LayoutAlign;
import ru.windcorp.progressia.client.graphics.gui.layout.LayoutVertical; import ru.windcorp.progressia.client.graphics.gui.layout.LayoutVertical;
import ru.windcorp.progressia.common.Units; 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.Server;
import ru.windcorp.progressia.server.ServerState; import ru.windcorp.progressia.server.ServerState;
@ -67,7 +68,7 @@ public class LayerTestGUI extends GUILayer {
panel.addChild(new DynamicLabel( panel.addChild(new DynamicLabel(
"FPSDisplay", new Font().withColor(0xFF37A3E6).deriveShadow(), "FPSDisplay", new Font().withColor(0xFF37A3E6).deriveShadow(),
LayerTestGUI::getFPS, DynamicStrings.builder().add("FPS: ").addDyn(() -> FPS_RECORD.update(GraphicsInterface.getFPS()), 5, 1).buildSupplier(),
128 128
)); ));
@ -79,7 +80,7 @@ public class LayerTestGUI extends GUILayer {
panel.addChild(new DynamicLabel( panel.addChild(new DynamicLabel(
"ChunkUpdatesDisplay", new Font().withColor(0xFF37A3E6).deriveShadow(), "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 128
)); ));
@ -148,26 +149,34 @@ public class LayerTestGUI extends GUILayer {
private static final Averager FPS_RECORD = new Averager(); private static final Averager FPS_RECORD = new Averager();
private static final Averager TPS_RECORD = new Averager(); private static final Averager TPS_RECORD = new Averager();
private static String getFPS() { private static final Supplier<CharSequence> TPS_STRING = DynamicStrings.builder()
return String.format(Locale.US, "FPS: %5.1f", FPS_RECORD.update(GraphicsInterface.getFPS())); .add("TPS: ")
} .addDyn(() -> TPS_RECORD.update(ServerState.getInstance().getTPS()), 5, 1)
.buildSupplier();
private static String getTPS() { private static final Supplier<CharSequence> 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(); Server server = ServerState.getInstance();
if (server == null) return "TPS: n/a"; 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(); Client client = ClientState.getInstance();
if (client == null) return "Pos: n/a"; if (client == null) return "Pos: client n/a";
Vec3 pos = client.getCamera().getLastAnchorPosition(); Vec3 pos = client.getCamera().getLastAnchorPosition();
if (Float.isNaN(pos.x)) { if (Float.isNaN(pos.x)) {
return "Pos: entity n/a"; return "Pos: entity n/a";
} else { } else {
return String.format(Locale.US, "Pos: %+7.1f %+7.1f %+7.1f", pos.x, pos.y, pos.z); return POS_STRING.get();
} }
} }