Skip to content

Commit b330197

Browse files
committed
perf(render): encode primitive fingerprints without strings
1 parent 13d2fcc commit b330197

2 files changed

Lines changed: 149 additions & 6 deletions

File tree

src/main/java/top/ellan/mahjong/table/render/TableRegionFingerprintService.java

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -264,19 +264,81 @@ private static final class FingerprintBuilder {
264264
private boolean needsSeparator;
265265

266266
private FingerprintBuilder field(Object value) {
267+
this.startField();
268+
this.mixText(Objects.toString(value, ""));
269+
return this.finishField();
270+
}
271+
272+
private FingerprintBuilder field(boolean value) {
273+
this.startField();
274+
this.mixText(value ? "true" : "false");
275+
return this.finishField();
276+
}
277+
278+
private FingerprintBuilder field(char value) {
279+
this.startField();
280+
this.mix(value);
281+
return this.finishField();
282+
}
283+
284+
private FingerprintBuilder field(int value) {
285+
this.startField();
286+
this.mixDecimal(value);
287+
return this.finishField();
288+
}
289+
290+
private FingerprintBuilder field(long value) {
291+
this.startField();
292+
this.mixDecimal(value);
293+
return this.finishField();
294+
}
295+
296+
private long value() {
297+
return this.hash;
298+
}
299+
300+
private void startField() {
267301
if (this.needsSeparator) {
268302
this.mix(':');
269303
}
270-
String text = Objects.toString(value, "");
271-
for (int index = 0; index < text.length(); index++) {
272-
this.mix(text.charAt(index));
273-
}
304+
}
305+
306+
private FingerprintBuilder finishField() {
274307
this.needsSeparator = true;
275308
return this;
276309
}
277310

278-
private long value() {
279-
return this.hash;
311+
private void mixText(String value) {
312+
for (int index = 0; index < value.length(); index++) {
313+
this.mix(value.charAt(index));
314+
}
315+
}
316+
317+
private void mixDecimal(long value) {
318+
long remaining = value > 0L ? -value : value;
319+
long lowDigits = 0L;
320+
long highDigits = 0L;
321+
int digitCount = 0;
322+
do {
323+
int digit = (int) -(remaining % 10L);
324+
if (digitCount < 16) {
325+
lowDigits |= (long) digit << (digitCount * 4);
326+
} else {
327+
highDigits |= (long) digit << ((digitCount - 16) * 4);
328+
}
329+
digitCount++;
330+
remaining /= 10L;
331+
} while (remaining != 0L);
332+
333+
if (value < 0L) {
334+
this.mix('-');
335+
}
336+
for (int index = digitCount - 1; index >= 0; index--) {
337+
int digit = index < 16
338+
? (int) (lowDigits >>> (index * 4)) & 0x0f
339+
: (int) (highDigits >>> ((index - 16) * 4)) & 0x0f;
340+
this.mix((char) ('0' + digit));
341+
}
280342
}
281343

282344
private void mix(char value) {
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package top.ellan.mahjong.table.render;
2+
3+
import java.lang.reflect.Constructor;
4+
import java.lang.reflect.Method;
5+
import java.util.Objects;
6+
import org.junit.jupiter.api.Test;
7+
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
10+
class TableRegionFingerprintServiceTest {
11+
@Test
12+
void primitiveFieldsPreserveTheDelimitedTextFingerprint() throws ReflectiveOperationException {
13+
Class<?> builderType = Class.forName(TableRegionFingerprintService.class.getName() + "$FingerprintBuilder");
14+
Constructor<?> constructor = builderType.getDeclaredConstructor();
15+
constructor.setAccessible(true);
16+
Object builder = constructor.newInstance();
17+
18+
Method objectField = accessible(builderType.getDeclaredMethod("field", Object.class));
19+
Method booleanField = accessible(builderType.getDeclaredMethod("field", boolean.class));
20+
Method charField = accessible(builderType.getDeclaredMethod("field", char.class));
21+
Method intField = accessible(builderType.getDeclaredMethod("field", int.class));
22+
Method longField = accessible(builderType.getDeclaredMethod("field", long.class));
23+
Method value = accessible(builderType.getDeclaredMethod("value"));
24+
25+
objectField.invoke(builder, "prefix");
26+
booleanField.invoke(builder, true);
27+
booleanField.invoke(builder, false);
28+
charField.invoke(builder, 'Z');
29+
intField.invoke(builder, 0);
30+
intField.invoke(builder, 1);
31+
intField.invoke(builder, -1);
32+
intField.invoke(builder, Integer.MIN_VALUE);
33+
intField.invoke(builder, Integer.MAX_VALUE);
34+
longField.invoke(builder, Long.MIN_VALUE);
35+
longField.invoke(builder, Long.MAX_VALUE);
36+
objectField.invoke(builder, new Object[] {null});
37+
38+
assertEquals(
39+
referenceFingerprint(
40+
"prefix",
41+
true,
42+
false,
43+
'Z',
44+
0,
45+
1,
46+
-1,
47+
Integer.MIN_VALUE,
48+
Integer.MAX_VALUE,
49+
Long.MIN_VALUE,
50+
Long.MAX_VALUE,
51+
null
52+
),
53+
value.invoke(builder)
54+
);
55+
}
56+
57+
private static Method accessible(Method method) {
58+
method.setAccessible(true);
59+
return method;
60+
}
61+
62+
private static long referenceFingerprint(Object... fields) {
63+
long hash = 0xcbf29ce484222325L;
64+
boolean needsSeparator = false;
65+
for (Object field : fields) {
66+
if (needsSeparator) {
67+
hash = mix(hash, ':');
68+
}
69+
String text = Objects.toString(field, "");
70+
for (int index = 0; index < text.length(); index++) {
71+
hash = mix(hash, text.charAt(index));
72+
}
73+
needsSeparator = true;
74+
}
75+
return hash;
76+
}
77+
78+
private static long mix(long hash, char value) {
79+
return (hash ^ value) * 0x100000001b3L;
80+
}
81+
}

0 commit comments

Comments
 (0)