Skip to content

Commit efdb110

Browse files
committed
Move save-time state off TrieBuilder.Node (#15970)
TrieBuilder.Node objects are constructed during field merges, and last as long as the merge for a given field. They can use significant heap memory, particularly for fields with many long terms (eg fields that store uuids or URLs). Two of the member fields on Node are only used at the end of a merge when the trie is being written, and so are effectively wasted memory during trie construction. This commit moves these two member fields out of Node onto a separate SaveFrame class, which is only built when the trie is saved. This should reduce the amount of memory held by Nodes by around a quarter.
1 parent ff46d15 commit efdb110

3 files changed

Lines changed: 71 additions & 39 deletions

File tree

lucene/CHANGES.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ Optimizations
8181
* GITHUB#15938: Optimize ReaderUtil#partitionByLeaf to use binary search on leaf
8282
boundaries instead of linear scan. (Greg Miller)
8383

84+
* GITHUB#15970: Reduce memory usage of fields with long terms during segment merges. (Alan Woodward)
85+
8486
Bug Fixes
8587
---------------------
8688
* GITHUB#15754: Fix HTMLStripCharFilter to prevent tags from incorrectly consuming subsequent

lucene/core/src/java/org/apache/lucene/codecs/lucene103/blocktree/Lucene103BlockTreeTermsWriter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ public String toString() {
424424
}
425425
}
426426

427-
private final class PendingBlock extends PendingEntry {
427+
private static final class PendingBlock extends PendingEntry {
428428
public final BytesRef prefix;
429429
public final long fp;
430430
public TrieBuilder index;

lucene/core/src/java/org/apache/lucene/codecs/lucene103/blocktree/TrieBuilder.java

Lines changed: 68 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -73,19 +73,38 @@ private static class Node {
7373
private Node firstChild;
7474
private Node lastChild;
7575

76-
// Vars used during saving:
77-
78-
// The file pointer point to where the node saved. -1 means the node has not been saved.
79-
private long fp = -1;
80-
// The latest child that have been saved. null means no child has been saved.
81-
private Node savedTo;
82-
8376
Node(int label, Output output) {
8477
this.label = label;
8578
this.output = output;
8679
}
8780
}
8881

82+
/**
83+
* Transient state used only during {@link #saveNodes}. Keeps save-time bookkeeping off of {@link
84+
* Node} so that Nodes are smaller during the (much longer) building phase.
85+
*/
86+
private static class SaveFrame {
87+
final Node node;
88+
// Iteration cursor: the latest child that has been pushed for saving.
89+
Node savedTo;
90+
// File pointer assigned when this node is written. -1 until then.
91+
long fp = -1;
92+
// File pointers of children, collected as they are popped. Null for leaf nodes.
93+
long[] childFps;
94+
int numChildFps;
95+
96+
SaveFrame(Node node) {
97+
this.node = node;
98+
if (node.childrenNum > 0) {
99+
this.childFps = new long[node.childrenNum];
100+
}
101+
}
102+
103+
void addChildFp(long childFp) {
104+
childFps[numChildFps++] = childFp;
105+
}
106+
}
107+
89108
private final Node root = new Node(0, null);
90109
private final BytesRef minKey;
91110
private BytesRef maxKey;
@@ -202,31 +221,35 @@ void save(DataOutput meta, IndexOutput index) throws IOException {
202221
throw new IllegalStateException("only unsaved trie can be saved, got: " + status);
203222
}
204223
meta.writeVLong(index.getFilePointer());
205-
saveNodes(index);
206-
meta.writeVLong(root.fp);
224+
meta.writeVLong(saveNodes(index));
207225
index.writeLong(0L); // additional 8 bytes for over-reading
208226
meta.writeVLong(index.getFilePointer());
209227
status = Status.SAVED;
210228
}
211229

212-
void saveNodes(IndexOutput index) throws IOException {
230+
long saveNodes(IndexOutput index) throws IOException {
213231
final long startFP = index.getFilePointer();
214-
Deque<Node> stack = new ArrayDeque<>();
215-
stack.push(root);
232+
Deque<SaveFrame> stack = new ArrayDeque<>();
233+
SaveFrame rootFrame = new SaveFrame(root);
234+
stack.push(rootFrame);
216235

217236
// Visit and save nodes of this trie in a post-order depth-first traversal.
218237
while (stack.isEmpty() == false) {
219-
Node node = stack.peek();
220-
assert node.fp == -1;
238+
SaveFrame frame = stack.peek();
239+
Node node = frame.node;
240+
assert frame.fp == -1;
221241
assert assertChildrenLabelInOrder(node);
222242

223243
final int childrenNum = node.childrenNum;
224244

225245
if (childrenNum == 0) { // leaf node
226246
assert node.output != null : "leaf nodes should have output.";
227247

228-
node.fp = index.getFilePointer() - startFP;
248+
frame.fp = index.getFilePointer() - startFP;
229249
stack.pop();
250+
if (stack.isEmpty() == false) {
251+
stack.peek().addChildFp(frame.fp);
252+
}
230253

231254
// [n bytes] floor data
232255
// [n bytes] output fp
@@ -251,31 +274,34 @@ void saveNodes(IndexOutput index) throws IOException {
251274
// If there are any children have not been saved, push the first one into stack and continue.
252275
// We want to ensure saving children before parent.
253276

254-
if (node.savedTo == null) {
255-
node.savedTo = node.firstChild;
256-
stack.push(node.savedTo);
277+
if (frame.savedTo == null) {
278+
frame.savedTo = node.firstChild;
279+
stack.push(new SaveFrame(frame.savedTo));
257280
continue;
258281
}
259-
if (node.savedTo.next != null) {
260-
assert node.savedTo.fp >= 0;
261-
node.savedTo = node.savedTo.next;
262-
stack.push(node.savedTo);
282+
if (frame.savedTo.next != null) {
283+
assert frame.numChildFps > 0 && frame.childFps[frame.numChildFps - 1] >= 0;
284+
frame.savedTo = frame.savedTo.next;
285+
stack.push(new SaveFrame(frame.savedTo));
263286
continue;
264287
}
265288

266289
// All children have been written, now it's time to write the parent!
267290

268-
assert assertNonLeafNodePreparingSaving(node);
269-
node.fp = index.getFilePointer() - startFP;
291+
assert assertNonLeafFramePreparingSaving(frame);
292+
frame.fp = index.getFilePointer() - startFP;
270293
stack.pop();
294+
if (stack.isEmpty() == false) {
295+
stack.peek().addChildFp(frame.fp);
296+
}
271297

272298
if (childrenNum == 1) {
273299

274300
// [n bytes] floor data
275301
// [n bytes] encoded output fp | [n bytes] child fp | [1 byte] label
276302
// [3bit] encoded output fp bytes | [3bit] child fp bytes | [2bit] sign
277303

278-
long childDeltaFp = node.fp - node.firstChild.fp;
304+
long childDeltaFp = frame.fp - frame.childFps[0];
279305
assert childDeltaFp > 0 : "parent node is always written after children: " + childDeltaFp;
280306
int childFpBytes = bytesRequiredVLong(childDeltaFp);
281307
int encodedOutputFpBytes =
@@ -317,7 +343,7 @@ void saveNodes(IndexOutput index) throws IOException {
317343
assert strategyBytes > 0 && strategyBytes <= 32;
318344

319345
// children fps are in order, so the first child's fp is min, then delta is max.
320-
long maxChildDeltaFp = node.fp - node.firstChild.fp;
346+
long maxChildDeltaFp = frame.fp - frame.childFps[0];
321347
assert maxChildDeltaFp > 0 : "parent always written after all children";
322348

323349
int childrenFpBytes = bytesRequiredVLong(maxChildDeltaFp);
@@ -353,9 +379,9 @@ void saveNodes(IndexOutput index) throws IOException {
353379
+ " actual: "
354380
+ (index.getFilePointer() - strategyStartFp);
355381

356-
for (Node child = node.firstChild; child != null; child = child.next) {
357-
assert node.fp > child.fp : "parent always written after all children";
358-
writeLongNBytes(node.fp - child.fp, childrenFpBytes, index);
382+
for (int i = 0; i < frame.numChildFps; i++) {
383+
assert frame.fp > frame.childFps[i] : "parent always written after all children";
384+
writeLongNBytes(frame.fp - frame.childFps[i], childrenFpBytes, index);
359385
}
360386

361387
if (node.output != null && node.output.floorData != null) {
@@ -364,6 +390,7 @@ void saveNodes(IndexOutput index) throws IOException {
364390
}
365391
}
366392
}
393+
return rootFrame.fp;
367394
}
368395

369396
private long encodeFP(Output output) {
@@ -412,25 +439,28 @@ private static boolean assertChildrenLabelInOrder(Node node) {
412439
return true;
413440
}
414441

415-
private static boolean assertNonLeafNodePreparingSaving(Node node) {
442+
private static boolean assertNonLeafFramePreparingSaving(SaveFrame frame) {
443+
Node node = frame.node;
416444
assert assertChildrenLabelInOrder(node);
417445
assert node.childrenNum != 0;
446+
assert frame.numChildFps == node.childrenNum
447+
: "expected " + node.childrenNum + " child fps, got " + frame.numChildFps;
418448
if (node.childrenNum == 1) {
419449
assert node.firstChild == node.lastChild;
420450
assert node.firstChild.next == null;
421-
assert node.savedTo == node.firstChild;
422-
assert node.firstChild.fp >= 0;
451+
assert frame.savedTo == node.firstChild;
452+
assert frame.childFps[0] >= 0;
423453
} else {
424454
int n = 0;
425-
for (Node child = node.firstChild; child != null; child = child.next) {
455+
for (int i = 0; i < frame.numChildFps; i++) {
426456
n++;
427-
assert child.fp >= 0;
428-
assert child.next == null || child.fp < child.next.fp
429-
: " the fp or children nodes should always be in order.";
457+
assert frame.childFps[i] >= 0;
458+
assert i == frame.numChildFps - 1 || frame.childFps[i] < frame.childFps[i + 1]
459+
: " the fp of children nodes should always be in order.";
430460
}
431461
assert node.childrenNum == n;
432-
assert node.lastChild == node.savedTo;
433-
assert node.savedTo.next == null;
462+
assert node.lastChild == frame.savedTo;
463+
assert frame.savedTo.next == null;
434464
}
435465
return true;
436466
}

0 commit comments

Comments
 (0)