perf(mainhttp): micro-optimize internal data structures
All checks were successful
ci/woodpecker/push/vitepress Pipeline was successful
ci/woodpecker/push/jfmod Pipeline was successful

This commit is contained in:
Johannes Frohnmeyer 2024-10-14 23:02:27 +02:00
parent 243dfcaed0
commit 9e59851a9f
Signed by: Johannes
GPG Key ID: E76429612C2929F4
3 changed files with 53 additions and 20 deletions

View File

@ -31,7 +31,7 @@ public class HttpDecoder extends ChannelInboundHandlerAdapter {
while (buf.isReadable() while (buf.isReadable()
&& current != null && current != null
&& current.content == null) && current.content == null)
current = current.next.get((char) buf.readByte()); current = current.next.get(buf.readByte());
if (current == null || current.content == null) return; if (current == null || current.content == null) return;
// Method identified, this is HTTP! // Method identified, this is HTTP!
passOn = false; passOn = false;

View File

@ -1,37 +1,68 @@
package io.gitlab.jfronny.libjf.mainhttp.impl.util; package io.gitlab.jfronny.libjf.mainhttp.impl.util;
import java.util.LinkedList; import java.util.Objects;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
public class ClaimPool<T> { public class ClaimPool<T> {
private final List<Claim> content = new LinkedList<>(); private Claim top = null;
public Claim claim(T value) { public Claim claim(T value) {
return new Claim(value); synchronized (this) {
return top = new Claim(value, top);
}
} }
public T getTopmost() { public T getTopmost() {
return content.isEmpty() ? null : content.get(content.size() - 1).value; Claim tp = top;
return tp == null ? null : tp.value;
} }
public boolean isEmpty() { public boolean isEmpty() {
return content.isEmpty(); return top == null;
} }
public class Claim { public class Claim {
private Claim prev, next;
private final T value; private final T value;
private final AtomicBoolean active = new AtomicBoolean(true); private boolean active = true;
private Claim(T value) { private Claim(T value, Claim prev) {
// This is synchronized on the pool, so we can safely read top
this.value = value; this.value = value;
content.add(this); if (prev != null) {
this.prev = prev;
prev.next = this;
}
} }
public void release() { public void release() {
if (!active.getAndSet(false)) // By synchronizing from left to right, we ensure that only claims to our left can be blocked by this claim,
throw new UnsupportedOperationException("Cannot release claim that is already released"); // meaning that the rightmost claim can always be released, preventing deadlocks
content.remove(this);
// If there is no previous object, prev cannot change, so we can safely read it without synchronization
// (which is effectively what is caused by synchronizing on this twice)
synchronized (Objects.requireNonNullElse(prev, this)) {
synchronized (this) {
// If there is no next object, ClaimPool might add a new one, so we need to synchronize on it
synchronized (Objects.requireNonNullElse(next, ClaimPool.this)) {
if (!active) throw new UnsupportedOperationException("Cannot release claim that is already released");
active = false;
if (prev != null) prev.next = next;
if (next != null) next.prev = prev;
if (top == this) top = prev;
}
}
}
} }
} }
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("ClaimPool{");
Claim current = top;
while (current != null) {
sb.append(current.value).append(" -> ");
current = current.prev;
}
return sb.append('}').toString();
}
}

View File

@ -1,7 +1,7 @@
package io.gitlab.jfronny.libjf.mainhttp.impl.util; package io.gitlab.jfronny.libjf.mainhttp.impl.util;
import it.unimi.dsi.fastutil.chars.Char2ObjectArrayMap; import it.unimi.dsi.fastutil.bytes.Byte2ObjectArrayMap;
import it.unimi.dsi.fastutil.chars.Char2ObjectMap; import it.unimi.dsi.fastutil.bytes.Byte2ObjectMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -9,11 +9,11 @@ import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class Trie<T> { public class Trie<T> {
public final Char2ObjectMap<Trie<T>> next; public final Byte2ObjectMap<Trie<T>> next;
public T content; public T content;
public Trie() { public Trie() {
this.next = new Char2ObjectArrayMap<>(); this.next = new Byte2ObjectArrayMap<>();
} }
public void add(Map<String, T> next) { public void add(Map<String, T> next) {
@ -22,7 +22,9 @@ public class Trie<T> {
public void add(String key, T value) { public void add(String key, T value) {
if (key.isEmpty()) this.content = value; if (key.isEmpty()) this.content = value;
else this.next.computeIfAbsent(key.charAt(0), k -> new Trie<>()) char c = key.charAt(0);
if (c > 255) throw new IllegalArgumentException("Invalid character: " + c);
else this.next.computeIfAbsent((byte) c, k -> new Trie<>())
.add(key.substring(1), value); .add(key.substring(1), value);
} }