Skip to content

Commit 2419af7

Browse files
committed
perf(http): garbage-free HTTP header parser
This change reduces the allocation rate when parsing HTTP headers. Previously, header parser would carefully pool HTTP headers, only to store them in a map which implicitly created a defensive copy of all keys. This change extends the map with a method to store keys without creating a defensive copy.
1 parent 813dc07 commit 2419af7

File tree

5 files changed

+78
-34
lines changed

5 files changed

+78
-34
lines changed

core/src/main/java/io/questdb/cutlass/http/HttpHeaderParser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ public long parse(long ptr, long hi, boolean _method, boolean _protocol) {
329329
if (HttpKeywords.isHeaderSetCookie(headerName)) {
330330
cookieParse(_lo, _wptr - 1);
331331
} else {
332-
headers.put(headerName, csPool.next().of(_lo, _wptr - 1));
332+
headers.putImmutable(headerName, csPool.next().of(_lo, _wptr - 1));
333333
}
334334
headerName = null;
335335
_lo = _wptr;

core/src/main/java/io/questdb/cutlass/http/processors/LineHttpTudCache.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public LineHttpTudCache(
8383
}
8484

8585
public void clear() {
86-
ObjList<Utf8String> keys = tableUpdateDetails.keys();
86+
ObjList<Utf8Sequence> keys = tableUpdateDetails.keys();
8787
for (int i = 0, n = keys.size(); i < n; i++) {
8888
Utf8Sequence tableName = tableUpdateDetails.keys().get(i);
8989
WalTableUpdateDetails tud = tableUpdateDetails.get(tableName);
@@ -101,7 +101,7 @@ public void clear() {
101101
@Override
102102
public void close() {
103103
// Close happens when HTTP connection is closed
104-
ObjList<Utf8String> keys = tableUpdateDetails.keys();
104+
ObjList<Utf8Sequence> keys = tableUpdateDetails.keys();
105105
for (int i = 0, n = keys.size(); i < n; i++) {
106106
Utf8Sequence tableName = tableUpdateDetails.keys().get(i);
107107
WalTableUpdateDetails tud = tableUpdateDetails.get(tableName);
@@ -116,7 +116,7 @@ public void commitAll() throws Throwable {
116116
boolean droppedTableFound;
117117
do {
118118
droppedTableFound = false;
119-
ObjList<Utf8String> keys = tableUpdateDetails.keys();
119+
ObjList<Utf8Sequence> keys = tableUpdateDetails.keys();
120120
for (int i = 0, n = keys.size(); i < n; i++) {
121121
Utf8Sequence tableName = tableUpdateDetails.keys().get(i);
122122
WalTableUpdateDetails tud = tableUpdateDetails.get(tableName);
@@ -184,7 +184,7 @@ public WalTableUpdateDetails getTableUpdateDetails(
184184
}
185185

186186
public void reset() {
187-
ObjList<Utf8String> keys = tableUpdateDetails.keys();
187+
ObjList<Utf8Sequence> keys = tableUpdateDetails.keys();
188188
for (int i = 0, n = keys.size(); i < n; i++) {
189189
Utf8Sequence tableName = tableUpdateDetails.keys().get(i);
190190
WalTableUpdateDetails tud = tableUpdateDetails.get(tableName);

core/src/main/java/io/questdb/std/AbstractLowerCaseUtf8SequenceHashSet.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public AbstractLowerCaseUtf8SequenceHashSet(int initialCapacity, double loadFact
4848
free = this.capacity = initialCapacity < MIN_INITIAL_CAPACITY ? MIN_INITIAL_CAPACITY : Numbers.ceilPow2(initialCapacity);
4949
this.loadFactor = loadFactor;
5050
int len = Numbers.ceilPow2((int) (this.capacity / loadFactor));
51-
keys = new Utf8String[len];
51+
keys = new Utf8Sequence[len];
5252
hashCodes = new int[len];
5353
mask = len - 1;
5454
}

core/src/main/java/io/questdb/std/LowerCaseUtf8SequenceObjHashMap.java

Lines changed: 43 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
* Note: this class is case-insensitive only for ASCII chars.
3737
*/
3838
public class LowerCaseUtf8SequenceObjHashMap<V> extends AbstractLowerCaseUtf8SequenceHashSet {
39-
private final ObjList<Utf8String> list;
39+
private final ObjList<Utf8Sequence> list;
4040
private V[] values;
4141

4242
public LowerCaseUtf8SequenceObjHashMap() {
@@ -64,7 +64,7 @@ public V get(Utf8Sequence key) {
6464
return valueAt(keyIndex(key));
6565
}
6666

67-
public ObjList<Utf8String> keys() {
67+
public ObjList<Utf8Sequence> keys() {
6868
return list;
6969
}
7070

@@ -77,36 +77,35 @@ public boolean put(Utf8Sequence key, V value) {
7777
}
7878

7979
public boolean putAt(int index, Utf8Sequence key, V value) {
80-
assert value != null;
81-
if (index < 0) {
82-
values[-index - 1] = value;
83-
return false;
84-
}
8580
Utf8String onHeapKey = Utf8String.newInstance(key);
86-
keys[index] = onHeapKey;
87-
hashCodes[index] = Utf8s.lowerCaseAsciiHashCode(key);
88-
values[index] = value;
89-
if (--free == 0) {
90-
rehash();
91-
}
92-
list.add(onHeapKey);
93-
return true;
81+
return putImmutableAt(index, onHeapKey, value);
9482
}
9583

9684
public boolean putAt(int index, Utf8String key, V value) {
97-
assert value != null;
98-
if (index < 0) {
99-
values[-index - 1] = value;
100-
return false;
101-
}
102-
keys[index] = key;
103-
hashCodes[index] = Utf8s.lowerCaseAsciiHashCode(key);
104-
values[index] = value;
105-
if (--free == 0) {
106-
rehash();
107-
}
108-
list.add(key);
109-
return true;
85+
return putImmutableAt(index, key, value);
86+
}
87+
88+
/**
89+
* Stores a key-value pair in the map without creating a defensive copy of the key.
90+
* <p>
91+
* Unlike {@link #put(Utf8Sequence, Object)}, this method stores the exact key instance provided,
92+
* preserving its identity.
93+
* <p>
94+
* <b>Important lifecycle requirement:</b> The caller must guarantee that:
95+
* <ul>
96+
* <li>The key instance will not be modified after insertion</li>
97+
* <li>The key instance will remain valid (e.g., backing memory not freed) while stored in the map</li>
98+
* <li>The key instance will not be reused or returned to a pool until removed from the map</li>
99+
* </ul>
100+
* <p>
101+
* Use {@link #put(Utf8Sequence, Object)} instead when the key lifecycle cannot be guaranteed.
102+
*
103+
* @param key the immutable key whose exact instance will be stored (must not be null)
104+
* @param value the value to associate with the key (must not be null)
105+
* @return true if this is a new entry, false if an existing entry was updated
106+
*/
107+
public boolean putImmutable(Utf8Sequence key, V value) {
108+
return putImmutableAt(keyIndex(key), key, value);
110109
}
111110

112111
public void removeAt(int index) {
@@ -129,6 +128,22 @@ public V valueQuick(int index) {
129128
return get(list.getQuick(index));
130129
}
131130

131+
private boolean putImmutableAt(int index, Utf8Sequence key, V value) {
132+
assert value != null;
133+
if (index < 0) {
134+
values[-index - 1] = value;
135+
return false;
136+
}
137+
keys[index] = key;
138+
hashCodes[index] = Utf8s.lowerCaseAsciiHashCode(key);
139+
values[index] = value;
140+
if (--free == 0) {
141+
rehash();
142+
}
143+
list.add(key);
144+
return true;
145+
}
146+
132147
@SuppressWarnings({"unchecked"})
133148
private void rehash() {
134149
int size = size();

core/src/test/java/io/questdb/test/std/LowerCaseUtf8SequenceObjHashMapTest.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@
2828
import io.questdb.std.MemoryTag;
2929
import io.questdb.std.Rnd;
3030
import io.questdb.std.Unsafe;
31+
import io.questdb.std.str.DirectUtf8Sink;
3132
import io.questdb.std.str.DirectUtf8String;
3233
import io.questdb.std.str.Utf8String;
3334
import io.questdb.std.str.Utf8s;
35+
import io.questdb.test.tools.TestUtils;
3436
import org.junit.Assert;
3537
import org.junit.Test;
3638

@@ -200,4 +202,31 @@ public void testLowerCaseKeys() {
200202
Assert.assertEquals(i, (int) map.get(key));
201203
}
202204
}
205+
206+
@Test
207+
public void testPutImmutableRetainsKeyIdentity() throws Exception {
208+
TestUtils.assertMemoryLeak(() -> {
209+
try (DirectUtf8Sink directUtf8Sink = new DirectUtf8Sink(8)) {
210+
LowerCaseUtf8SequenceObjHashMap<Integer> map = new LowerCaseUtf8SequenceObjHashMap<>();
211+
212+
directUtf8Sink.put("foo");
213+
Utf8String utf8String = new Utf8String("foo");
214+
215+
Assert.assertTrue(map.putImmutable(directUtf8Sink, 1));
216+
Assert.assertEquals(1, (int) map.get(utf8String));
217+
int idx = map.keyIndex(utf8String);
218+
219+
Assert.assertSame(directUtf8Sink, map.keyAt(idx));
220+
Assert.assertSame(directUtf8Sink, map.keys().get(0));
221+
222+
Assert.assertFalse(map.putImmutable(utf8String, 2));
223+
Assert.assertEquals(2, (int) map.get(directUtf8Sink));
224+
225+
map.remove(directUtf8Sink);
226+
Assert.assertEquals(0, map.size());
227+
}
228+
});
229+
230+
231+
}
203232
}

0 commit comments

Comments
 (0)