fix: Performance fixes
This commit is contained in:
29
.project
29
.project
@@ -1,23 +1,22 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<projectDescription>
|
<projectDescription>
|
||||||
<name>Radixor</name>
|
<name>Radixor</name>
|
||||||
<comment>Project Radixor created by Buildship.</comment>
|
<comment></comment>
|
||||||
<projects>
|
<projects/>
|
||||||
</projects>
|
|
||||||
<buildSpec>
|
|
||||||
<buildCommand>
|
|
||||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
|
||||||
<arguments>
|
|
||||||
</arguments>
|
|
||||||
</buildCommand>
|
|
||||||
<buildCommand>
|
|
||||||
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
|
|
||||||
<arguments>
|
|
||||||
</arguments>
|
|
||||||
</buildCommand>
|
|
||||||
</buildSpec>
|
|
||||||
<natures>
|
<natures>
|
||||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||||
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
|
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
|
||||||
</natures>
|
</natures>
|
||||||
|
<buildSpec>
|
||||||
|
<buildCommand>
|
||||||
|
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||||
|
<arguments/>
|
||||||
|
</buildCommand>
|
||||||
|
<buildCommand>
|
||||||
|
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
|
||||||
|
<arguments/>
|
||||||
|
</buildCommand>
|
||||||
|
</buildSpec>
|
||||||
|
<linkedResources/>
|
||||||
|
<filteredResources/>
|
||||||
</projectDescription>
|
</projectDescription>
|
||||||
|
|||||||
11
build.gradle
11
build.gradle
@@ -33,6 +33,9 @@ configurations {
|
|||||||
java {
|
java {
|
||||||
withSourcesJar()
|
withSourcesJar()
|
||||||
withJavadocJar()
|
withJavadocJar()
|
||||||
|
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
|
targetCompatibility = JavaVersion.VERSION_21
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(AbstractArchiveTask).configureEach {
|
tasks.withType(AbstractArchiveTask).configureEach {
|
||||||
@@ -51,18 +54,14 @@ pmd {
|
|||||||
ruleSetFiles = files(rootProject.file(".ruleset"))
|
ruleSetFiles = files(rootProject.file(".ruleset"))
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(JavaCompile).configureEach {
|
|
||||||
options.release = 21
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencyLocking {
|
dependencyLocking {
|
||||||
lockAllConfigurations()
|
lockAllConfigurations()
|
||||||
|
|
||||||
lockMode = LockMode.STRICT
|
lockMode = LockMode.STRICT
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
jmhImplementation sourceSets.main.output
|
jmhImplementation sourceSets.main.output
|
||||||
|
|
||||||
testImplementation platform(libs.junit.bom)
|
testImplementation platform(libs.junit.bom)
|
||||||
testImplementation libs.junit.jupiter
|
testImplementation libs.junit.jupiter
|
||||||
|
|||||||
@@ -95,11 +95,6 @@ public final class FrequencyTrie<V> {
|
|||||||
*/
|
*/
|
||||||
private static final Logger LOGGER = Logger.getLogger(FrequencyTrie.class.getName());
|
private static final Logger LOGGER = Logger.getLogger(FrequencyTrie.class.getName());
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory used to create correctly typed arrays for {@link #getAll(String)}.
|
|
||||||
*/
|
|
||||||
private final IntFunction<V[]> arrayFactory;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Root node of the compiled read-only trie.
|
* Root node of the compiled read-only trie.
|
||||||
*/
|
*/
|
||||||
@@ -110,6 +105,26 @@ public final class FrequencyTrie<V> {
|
|||||||
*/
|
*/
|
||||||
private final TrieMetadata metadata;
|
private final TrieMetadata metadata;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cached traversal direction used for key lookup.
|
||||||
|
*/
|
||||||
|
private final WordTraversalDirection lookupTraversalDirection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether lookups require lowercase normalization.
|
||||||
|
*/
|
||||||
|
private final boolean lowercasesLookupKeys;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether lookups require diacritic stripping.
|
||||||
|
*/
|
||||||
|
private final boolean removeDiacritics;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared empty array instance for empty lookup results from {@link #getAll(String)}.
|
||||||
|
*/
|
||||||
|
private final V[] emptyValues;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Binary format magic header.
|
* Binary format magic header.
|
||||||
*/
|
*/
|
||||||
@@ -145,9 +160,12 @@ public final class FrequencyTrie<V> {
|
|||||||
*/
|
*/
|
||||||
private FrequencyTrie(final IntFunction<V[]> arrayFactory, final CompiledNode<V> root,
|
private FrequencyTrie(final IntFunction<V[]> arrayFactory, final CompiledNode<V> root,
|
||||||
final TrieMetadata metadata) {
|
final TrieMetadata metadata) {
|
||||||
this.arrayFactory = Objects.requireNonNull(arrayFactory, "arrayFactory");
|
|
||||||
this.root = Objects.requireNonNull(root, "root");
|
this.root = Objects.requireNonNull(root, "root");
|
||||||
this.metadata = Objects.requireNonNull(metadata, "metadata");
|
this.metadata = Objects.requireNonNull(metadata, "metadata");
|
||||||
|
this.lookupTraversalDirection = metadata.traversalDirection();
|
||||||
|
this.lowercasesLookupKeys = metadata.caseProcessingMode() == CaseProcessingMode.LOWERCASE_WITH_LOCALE_ROOT;
|
||||||
|
this.removeDiacritics = metadata.diacriticProcessingMode() == DiacriticProcessingMode.REMOVE;
|
||||||
|
this.emptyValues = arrayFactory.apply(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -172,10 +190,14 @@ public final class FrequencyTrie<V> {
|
|||||||
public V get(final String key) {
|
public V get(final String key) {
|
||||||
Objects.requireNonNull(key, "key");
|
Objects.requireNonNull(key, "key");
|
||||||
final CompiledNode<V> node = findNode(normalizeLookupKey(key));
|
final CompiledNode<V> node = findNode(normalizeLookupKey(key));
|
||||||
if (node == null || node.orderedValues().length == 0) {
|
if (node == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return node.orderedValues()[0];
|
final V[] orderedValues = node.orderedValues();
|
||||||
|
if (orderedValues.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return orderedValues[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -201,13 +223,18 @@ public final class FrequencyTrie<V> {
|
|||||||
* value is stored at the addressed node
|
* value is stored at the addressed node
|
||||||
* @throws NullPointerException if {@code key} is {@code null}
|
* @throws NullPointerException if {@code key} is {@code null}
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("PMD.MethodReturnsInternalArray")
|
||||||
public V[] getAll(final String key) {
|
public V[] getAll(final String key) {
|
||||||
Objects.requireNonNull(key, "key");
|
Objects.requireNonNull(key, "key");
|
||||||
final CompiledNode<V> node = findNode(normalizeLookupKey(key));
|
final CompiledNode<V> node = findNode(normalizeLookupKey(key));
|
||||||
if (node == null || node.orderedValues().length == 0) {
|
if (node == null) {
|
||||||
return this.arrayFactory.apply(0);
|
return this.emptyValues;
|
||||||
}
|
}
|
||||||
return Arrays.copyOf(node.orderedValues(), node.orderedValues().length);
|
final V[] orderedValues = node.orderedValues();
|
||||||
|
if (orderedValues.length == 0) {
|
||||||
|
return this.emptyValues;
|
||||||
|
}
|
||||||
|
return Arrays.copyOf(orderedValues, orderedValues.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -232,16 +259,28 @@ public final class FrequencyTrie<V> {
|
|||||||
* if the key does not exist or no value is stored at the addressed node
|
* if the key does not exist or no value is stored at the addressed node
|
||||||
* @throws NullPointerException if {@code key} is {@code null}
|
* @throws NullPointerException if {@code key} is {@code null}
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
|
||||||
public List<ValueCount<V>> getEntries(final String key) {
|
public List<ValueCount<V>> getEntries(final String key) {
|
||||||
Objects.requireNonNull(key, "key");
|
Objects.requireNonNull(key, "key");
|
||||||
final CompiledNode<V> node = findNode(normalizeLookupKey(key));
|
final CompiledNode<V> node = findNode(normalizeLookupKey(key));
|
||||||
if (node == null || node.orderedValues().length == 0) {
|
if (node == null) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<ValueCount<V>> entries = new ArrayList<>(node.orderedValues().length);
|
final V[] orderedValues = node.orderedValues();
|
||||||
for (int index = 0; index < node.orderedValues().length; index++) {
|
final int valueCount = orderedValues.length;
|
||||||
entries.add(new ValueCount<>(node.orderedValues()[index], node.orderedCounts()[index]));
|
if (valueCount == 0) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueCount == 1) {
|
||||||
|
return List.of(new ValueCount<>(orderedValues[0], node.orderedCounts()[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
final int[] orderedCounts = node.orderedCounts();
|
||||||
|
final List<ValueCount<V>> entries = new ArrayList<>(valueCount);
|
||||||
|
for (int index = 0; index < valueCount; index++) {
|
||||||
|
entries.add(new ValueCount<>(orderedValues[index], orderedCounts[index]));
|
||||||
}
|
}
|
||||||
return Collections.unmodifiableList(entries);
|
return Collections.unmodifiableList(entries);
|
||||||
}
|
}
|
||||||
@@ -644,9 +683,18 @@ public final class FrequencyTrie<V> {
|
|||||||
*/
|
*/
|
||||||
private CompiledNode<V> findNode(final String key) {
|
private CompiledNode<V> findNode(final String key) {
|
||||||
CompiledNode<V> current = this.root;
|
CompiledNode<V> current = this.root;
|
||||||
|
if (this.lookupTraversalDirection == WordTraversalDirection.BACKWARD) {
|
||||||
|
for (int traversalOffset = key.length() - 1; traversalOffset >= 0; traversalOffset--) {
|
||||||
|
current = current.findChild(key.charAt(traversalOffset));
|
||||||
|
if (current == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
for (int traversalOffset = 0; traversalOffset < key.length(); traversalOffset++) {
|
for (int traversalOffset = 0; traversalOffset < key.length(); traversalOffset++) {
|
||||||
current = current.findChild(
|
current = current.findChild(key.charAt(traversalOffset));
|
||||||
key.charAt(this.metadata.traversalDirection().logicalIndex(key.length(), traversalOffset)));
|
|
||||||
if (current == null) {
|
if (current == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -661,13 +709,15 @@ public final class FrequencyTrie<V> {
|
|||||||
* @return normalized key for trie traversal
|
* @return normalized key for trie traversal
|
||||||
*/
|
*/
|
||||||
private String normalizeLookupKey(final String key) {
|
private String normalizeLookupKey(final String key) {
|
||||||
String normalized = key;
|
if (!this.lowercasesLookupKeys && !this.removeDiacritics) {
|
||||||
|
return key;
|
||||||
if (this.metadata.caseProcessingMode() == CaseProcessingMode.LOWERCASE_WITH_LOCALE_ROOT) {
|
|
||||||
normalized = normalized.toLowerCase(Locale.ROOT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.metadata.diacriticProcessingMode() == DiacriticProcessingMode.REMOVE) {
|
String normalized = key;
|
||||||
|
if (this.lowercasesLookupKeys) {
|
||||||
|
normalized = normalized.toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
if (this.removeDiacritics) {
|
||||||
normalized = DiacriticStripper.strip(normalized);
|
normalized = DiacriticStripper.strip(normalized);
|
||||||
} else if (this.metadata.diacriticProcessingMode() == DiacriticProcessingMode.AS_IS_AND_STRIPPED_FALLBACK) {
|
} else if (this.metadata.diacriticProcessingMode() == DiacriticProcessingMode.AS_IS_AND_STRIPPED_FALLBACK) {
|
||||||
throw new UnsupportedOperationException(
|
throw new UnsupportedOperationException(
|
||||||
|
|||||||
@@ -121,6 +121,16 @@ public final class PatchCommandEncoder {
|
|||||||
*/
|
*/
|
||||||
/* default */ static final String NOOP_PATCH = String.valueOf(new char[] { NOOP_OPCODE, NOOP_ARGUMENT });
|
/* default */ static final String NOOP_PATCH = String.valueOf(new char[] { NOOP_OPCODE, NOOP_ARGUMENT });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefix used in unsupported NOOP patch argument exceptions.
|
||||||
|
*/
|
||||||
|
private static final String MSG_NOOP = "Unsupported NOOP patch argument: ";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefix used in unsupported patch opcode exceptions.
|
||||||
|
*/
|
||||||
|
private static final String MSG_OPCODE = "Unsupported patch opcode: ";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Safety penalty used to prevent a mismatch from being selected as a match.
|
* Safety penalty used to prevent a mismatch from being selected as a match.
|
||||||
*/
|
*/
|
||||||
@@ -413,6 +423,9 @@ public final class PatchCommandEncoder {
|
|||||||
if ((patchCommand.length() & 1) != 0) {
|
if ((patchCommand.length() & 1) != 0) {
|
||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
|
if (patchCommand.length() == 2) {
|
||||||
|
return applySingleBackwardInstruction(source, patchCommand.charAt(0), patchCommand.charAt(1));
|
||||||
|
}
|
||||||
|
|
||||||
final StringBuilder result = new StringBuilder(source);
|
final StringBuilder result = new StringBuilder(source);
|
||||||
if (result.isEmpty()) {
|
if (result.isEmpty()) {
|
||||||
@@ -494,6 +507,9 @@ public final class PatchCommandEncoder {
|
|||||||
if ((patchCommand.length() & 1) != 0) {
|
if ((patchCommand.length() & 1) != 0) {
|
||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
|
if (patchCommand.length() == 2) {
|
||||||
|
return applySingleForwardInstruction(source, patchCommand.charAt(0), patchCommand.charAt(1));
|
||||||
|
}
|
||||||
|
|
||||||
final StringBuilder result = new StringBuilder(source);
|
final StringBuilder result = new StringBuilder(source);
|
||||||
if (result.isEmpty()) {
|
if (result.isEmpty()) {
|
||||||
@@ -552,6 +568,102 @@ public final class PatchCommandEncoder {
|
|||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a single backward-direction patch instruction.
|
||||||
|
*
|
||||||
|
* @param source original source word
|
||||||
|
* @param opcode patch opcode
|
||||||
|
* @param argument encoded patch argument
|
||||||
|
* @return transformed source after one instruction
|
||||||
|
*/
|
||||||
|
private static String applySingleBackwardInstruction(final String source, final char opcode, final char argument) {
|
||||||
|
final int sourceLength = source.length();
|
||||||
|
final int encodedValue;
|
||||||
|
|
||||||
|
switch (opcode) {
|
||||||
|
case DELETE_OPCODE:
|
||||||
|
encodedValue = decodeEncodedCount(argument);
|
||||||
|
if (encodedValue < 1 || encodedValue > sourceLength) {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
return source.substring(0, sourceLength - encodedValue);
|
||||||
|
|
||||||
|
case INSERT_OPCODE:
|
||||||
|
final char[] insertTarget = new char[sourceLength + 1];
|
||||||
|
source.getChars(0, sourceLength, insertTarget, 0);
|
||||||
|
insertTarget[sourceLength] = argument;
|
||||||
|
return new String(insertTarget);
|
||||||
|
|
||||||
|
case REPLACE_OPCODE:
|
||||||
|
if (sourceLength == 0) {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
final char[] replaceTarget = source.toCharArray();
|
||||||
|
replaceTarget[sourceLength - 1] = argument;
|
||||||
|
return new String(replaceTarget);
|
||||||
|
|
||||||
|
case SKIP_OPCODE:
|
||||||
|
return source;
|
||||||
|
|
||||||
|
case NOOP_OPCODE:
|
||||||
|
if (argument != NOOP_ARGUMENT) {
|
||||||
|
throw new IllegalArgumentException(MSG_NOOP + argument);
|
||||||
|
}
|
||||||
|
return source;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException(MSG_OPCODE + opcode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a single forward-direction patch instruction.
|
||||||
|
*
|
||||||
|
* @param source original source word
|
||||||
|
* @param opcode patch opcode
|
||||||
|
* @param argument encoded patch argument
|
||||||
|
* @return transformed source after one instruction
|
||||||
|
*/
|
||||||
|
private static String applySingleForwardInstruction(final String source, final char opcode, final char argument) {
|
||||||
|
final int sourceLength = source.length();
|
||||||
|
final int encodedValue;
|
||||||
|
|
||||||
|
switch (opcode) {
|
||||||
|
case DELETE_OPCODE:
|
||||||
|
encodedValue = decodeEncodedCount(argument);
|
||||||
|
if (encodedValue < 1 || encodedValue > sourceLength) {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
return source.substring(encodedValue);
|
||||||
|
|
||||||
|
case INSERT_OPCODE:
|
||||||
|
final char[] insertTarget = new char[sourceLength + 1];
|
||||||
|
insertTarget[0] = argument;
|
||||||
|
source.getChars(0, sourceLength, insertTarget, 1);
|
||||||
|
return new String(insertTarget);
|
||||||
|
|
||||||
|
case REPLACE_OPCODE:
|
||||||
|
if (sourceLength == 0) {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
final char[] replaceTarget = source.toCharArray();
|
||||||
|
replaceTarget[0] = argument;
|
||||||
|
return new String(replaceTarget);
|
||||||
|
|
||||||
|
case SKIP_OPCODE:
|
||||||
|
return source;
|
||||||
|
|
||||||
|
case NOOP_OPCODE:
|
||||||
|
if (argument != NOOP_ARGUMENT) {
|
||||||
|
throw new IllegalArgumentException(MSG_NOOP + argument);
|
||||||
|
}
|
||||||
|
return source;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException(MSG_OPCODE + opcode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies a backward patch command to an empty source word.
|
* Applies a backward patch command to an empty source word.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ import java.util.Objects;
|
|||||||
@SuppressWarnings("PMD.DataClass")
|
@SuppressWarnings("PMD.DataClass")
|
||||||
public record CompiledNode<V>(char[] edgeLabels, CompiledNode<V>[] children, V[] orderedValues, int... orderedCounts) {
|
public record CompiledNode<V>(char[] edgeLabels, CompiledNode<V>[] children, V[] orderedValues, int... orderedCounts) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of child edges where linear scan is cheaper than binary search.
|
||||||
|
*/
|
||||||
|
private static final int LINEAR_CHILD_COUNT_THRESHOLD = 4;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates one validated compiled node.
|
* Creates one validated compiled node.
|
||||||
*
|
*
|
||||||
@@ -140,6 +145,19 @@ public record CompiledNode<V>(char[] edgeLabels, CompiledNode<V>[] children, V[]
|
|||||||
* @return child node, or {@code null} if absent
|
* @return child node, or {@code null} if absent
|
||||||
*/
|
*/
|
||||||
public CompiledNode<V> findChild(final char edge) {
|
public CompiledNode<V> findChild(final char edge) {
|
||||||
|
final int childCount = this.edgeLabels.length;
|
||||||
|
if (childCount == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (childCount <= LINEAR_CHILD_COUNT_THRESHOLD) {
|
||||||
|
for (int index = 0; index < childCount; index++) {
|
||||||
|
if (this.edgeLabels[index] == edge) {
|
||||||
|
return this.children[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
final int index = Arrays.binarySearch(this.edgeLabels, edge);
|
final int index = Arrays.binarySearch(this.edgeLabels, edge);
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user