Skip to content

Commit bc81f68

Browse files
authored
Fix memory tracking in TopN.Row (#102831) (#102887)
This commit addresses the issue of missing memory tracking for the BitSet in TopN.Row. Instead of introducing BreakingBitSet, we replace the BitSet with a smaller array of offsets in this PR. Nik suggested to remove that BitSet, but I haven't looked into that option yet. Closes #100640 Closes #102683 Closes #102790 Closes #102784
1 parent b152d50 commit bc81f68

File tree

4 files changed

+84
-27
lines changed

4 files changed

+84
-27
lines changed

docs/changelog/102831.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
pr: 102831
2+
summary: Fix memory tracking in TopN.Row
3+
area: ES|QL
4+
type: bug
5+
issues:
6+
- 100640
7+
- 102784
8+
- 102790
9+
- 102683

x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/topn/TopNOperator.java

Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525

2626
import java.util.ArrayList;
2727
import java.util.Arrays;
28-
import java.util.BitSet;
2928
import java.util.Collections;
3029
import java.util.Iterator;
3130
import java.util.List;
@@ -51,8 +50,7 @@ public class TopNOperator implements Operator, Accountable {
5150
* multivalues) to reference each position in each block of the Page.
5251
*/
5352
static final class Row implements Accountable, Releasable {
54-
private static final long SHALLOW_SIZE = RamUsageEstimator.shallowSizeOfInstance(Row.class) + RamUsageEstimator
55-
.shallowSizeOfInstance(BitSet.class);
53+
private static final long SHALLOW_SIZE = RamUsageEstimator.shallowSizeOfInstance(Row.class);
5654

5755
/**
5856
* The sort key.
@@ -64,7 +62,7 @@ static final class Row implements Accountable, Releasable {
6462
* For ex, if a Long is represented as 8 bytes, each of these bytes will have the same value (set/unset) if the respective Long
6563
* value is used for sorting ascending/descending.
6664
*/
67-
final BitSet orderByCompositeKeyAscending = new BitSet();
65+
final BytesOrder bytesOrder;
6866

6967
/**
7068
* Values to reconstruct the row. Sort of. When we reconstruct the row we read
@@ -73,11 +71,12 @@ static final class Row implements Accountable, Releasable {
7371
*/
7472
final BreakingBytesRefBuilder values;
7573

76-
Row(CircuitBreaker breaker) {
74+
Row(CircuitBreaker breaker, List<SortOrder> sortOrders) {
7775
boolean success = false;
7876
try {
7977
keys = new BreakingBytesRefBuilder(breaker, "topn");
8078
values = new BreakingBytesRefBuilder(breaker, "topn");
79+
bytesOrder = new BytesOrder(sortOrders, breaker, "topn");
8180
success = true;
8281
} finally {
8382
if (success == false) {
@@ -88,12 +87,54 @@ static final class Row implements Accountable, Releasable {
8887

8988
@Override
9089
public long ramBytesUsed() {
91-
return SHALLOW_SIZE + keys.ramBytesUsed() + orderByCompositeKeyAscending.size() / Byte.SIZE + values.ramBytesUsed();
90+
return SHALLOW_SIZE + keys.ramBytesUsed() + bytesOrder.ramBytesUsed() + values.ramBytesUsed();
9291
}
9392

9493
@Override
9594
public void close() {
96-
Releasables.closeExpectNoException(keys, values);
95+
Releasables.closeExpectNoException(keys, values, bytesOrder);
96+
}
97+
}
98+
99+
static final class BytesOrder implements Releasable, Accountable {
100+
private static final long BASE_RAM_USAGE = RamUsageEstimator.shallowSizeOfInstance(BytesOrder.class);
101+
private final CircuitBreaker breaker;
102+
final List<SortOrder> sortOrders;
103+
final int[] endOffsets;
104+
105+
BytesOrder(List<SortOrder> sortOrders, CircuitBreaker breaker, String label) {
106+
this.breaker = breaker;
107+
this.sortOrders = sortOrders;
108+
breaker.addEstimateBytesAndMaybeBreak(memoryUsed(sortOrders.size()), label);
109+
this.endOffsets = new int[sortOrders.size()];
110+
}
111+
112+
/**
113+
* Returns true if the byte at the given position is ordered ascending; otherwise, return false
114+
*/
115+
boolean isByteOrderAscending(int bytePosition) {
116+
int index = Arrays.binarySearch(endOffsets, bytePosition);
117+
if (index < 0) {
118+
index = -1 - index;
119+
}
120+
return sortOrders.get(index).asc();
121+
}
122+
123+
private long memoryUsed(int numKeys) {
124+
// sortOrders is global and its memory is accounted at the top level TopNOperator
125+
return BASE_RAM_USAGE + RamUsageEstimator.alignObjectSize(
126+
(long) RamUsageEstimator.NUM_BYTES_ARRAY_HEADER + (long) Integer.BYTES * numKeys
127+
);
128+
}
129+
130+
@Override
131+
public long ramBytesUsed() {
132+
return memoryUsed(sortOrders.size());
133+
}
134+
135+
@Override
136+
public void close() {
137+
breaker.addWithoutBreaking(-ramBytesUsed());
97138
}
98139
}
99140

@@ -138,14 +179,11 @@ void row(int position, Row destination) {
138179

139180
private void writeKey(int position, Row row) {
140181
int orderByCompositeKeyCurrentPosition = 0;
141-
for (KeyFactory factory : keyFactories) {
142-
int valueAsBytesSize = factory.extractor.writeKey(row.keys, position);
143-
row.orderByCompositeKeyAscending.set(
144-
orderByCompositeKeyCurrentPosition,
145-
valueAsBytesSize + orderByCompositeKeyCurrentPosition,
146-
factory.ascending
147-
);
182+
for (int i = 0; i < keyFactories.length; i++) {
183+
int valueAsBytesSize = keyFactories[i].extractor.writeKey(row.keys, position);
184+
assert valueAsBytesSize > 0 : valueAsBytesSize;
148185
orderByCompositeKeyCurrentPosition += valueAsBytesSize;
186+
row.bytesOrder.endOffsets[i] = orderByCompositeKeyCurrentPosition - 1;
149187
}
150188
}
151189

@@ -189,9 +227,7 @@ public record TopNOperatorFactory(
189227
List<SortOrder> sortOrders,
190228
int maxPageSize
191229
) implements OperatorFactory {
192-
public TopNOperatorFactory
193-
194-
{
230+
public TopNOperatorFactory {
195231
for (ElementType e : elementTypes) {
196232
if (e == null) {
197233
throw new IllegalArgumentException("ElementType not known");
@@ -274,19 +310,20 @@ static int compareRows(Row r1, Row r2) {
274310
// the two rows are equal
275311
return 0;
276312
}
313+
277314
int length = Math.min(br1.length, br2.length);
278315
// one value is the prefix of the other
279316
if (mismatchedByteIndex == length) {
280317
// the value with the greater length is considered greater than the other
281318
if (length == br1.length) {// first row is less than the second row
282-
return r2.orderByCompositeKeyAscending.get(length) ? 1 : -1;
319+
return r2.bytesOrder.isByteOrderAscending(length) ? 1 : -1;
283320
} else {// second row is less than the first row
284-
return r1.orderByCompositeKeyAscending.get(length) ? -1 : 1;
321+
return r1.bytesOrder.isByteOrderAscending(length) ? -1 : 1;
285322
}
286323
} else {
287324
// compare the byte that mismatched accounting for that respective byte asc/desc ordering
288325
int c = Byte.compareUnsigned(br1.bytes[br1.offset + mismatchedByteIndex], br2.bytes[br2.offset + mismatchedByteIndex]);
289-
return r1.orderByCompositeKeyAscending.get(mismatchedByteIndex) ? -c : c;
326+
return r1.bytesOrder.isByteOrderAscending(mismatchedByteIndex) ? -c : c;
290327
}
291328
}
292329

@@ -312,10 +349,9 @@ public void addInput(Page page) {
312349
try {
313350
for (int i = 0; i < page.getPositionCount(); i++) {
314351
if (spare == null) {
315-
spare = new Row(breaker);
352+
spare = new Row(breaker, sortOrders);
316353
} else {
317354
spare.keys.clear();
318-
spare.orderByCompositeKeyAscending.clear();
319355
spare.values.clear();
320356
}
321357
rowFiller.row(i, spare);

x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/topn/TopNOperatorTests.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -434,13 +434,14 @@ private TopNOperator.Row row(
434434
Page page,
435435
int position
436436
) {
437+
final var sortOrders = List.of(new TopNOperator.SortOrder(channel, asc, nullsFirst));
437438
TopNOperator.RowFiller rf = new TopNOperator.RowFiller(
438439
IntStream.range(0, page.getBlockCount()).mapToObj(i -> elementType).toList(),
439440
IntStream.range(0, page.getBlockCount()).mapToObj(i -> encoder).toList(),
440-
List.of(new TopNOperator.SortOrder(channel, asc, nullsFirst)),
441+
sortOrders,
441442
page
442443
);
443-
TopNOperator.Row row = new TopNOperator.Row(nonBreakingBigArrays().breakerService().getBreaker("request"));
444+
TopNOperator.Row row = new TopNOperator.Row(nonBreakingBigArrays().breakerService().getBreaker("request"), sortOrders);
444445
rf.row(position, row);
445446
return row;
446447
}

x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/topn/TopNRowTests.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,32 +12,41 @@
1212
import org.elasticsearch.common.breaker.NoopCircuitBreaker;
1313
import org.elasticsearch.test.ESTestCase;
1414

15+
import java.util.List;
16+
1517
import static org.hamcrest.Matchers.equalTo;
1618

1719
public class TopNRowTests extends ESTestCase {
1820
private final CircuitBreaker breaker = new NoopCircuitBreaker(CircuitBreaker.REQUEST);
1921

2022
public void testRamBytesUsedEmpty() {
21-
TopNOperator.Row row = new TopNOperator.Row(breaker);
23+
TopNOperator.Row row = new TopNOperator.Row(breaker, sortOrders());
2224
assertThat(row.ramBytesUsed(), equalTo(expectedRamBytesUsed(row)));
2325
}
2426

2527
public void testRamBytesUsedSmall() {
26-
TopNOperator.Row row = new TopNOperator.Row(new NoopCircuitBreaker(CircuitBreaker.REQUEST));
28+
TopNOperator.Row row = new TopNOperator.Row(new NoopCircuitBreaker(CircuitBreaker.REQUEST), sortOrders());
2729
row.keys.append(randomByte());
2830
row.values.append(randomByte());
2931
assertThat(row.ramBytesUsed(), equalTo(expectedRamBytesUsed(row)));
3032
}
3133

3234
public void testRamBytesUsedBig() {
33-
TopNOperator.Row row = new TopNOperator.Row(new NoopCircuitBreaker(CircuitBreaker.REQUEST));
35+
TopNOperator.Row row = new TopNOperator.Row(new NoopCircuitBreaker(CircuitBreaker.REQUEST), sortOrders());
3436
for (int i = 0; i < 10000; i++) {
3537
row.keys.append(randomByte());
3638
row.values.append(randomByte());
3739
}
3840
assertThat(row.ramBytesUsed(), equalTo(expectedRamBytesUsed(row)));
3941
}
4042

43+
private static List<TopNOperator.SortOrder> sortOrders() {
44+
return List.of(
45+
new TopNOperator.SortOrder(randomNonNegativeInt(), randomBoolean(), randomBoolean()),
46+
new TopNOperator.SortOrder(randomNonNegativeInt(), randomBoolean(), randomBoolean())
47+
);
48+
}
49+
4150
private long expectedRamBytesUsed(TopNOperator.Row row) {
4251
long expected = RamUsageTester.ramUsed(row);
4352
if (row.values.bytes().length == 0) {
@@ -47,6 +56,8 @@ private long expectedRamBytesUsed(TopNOperator.Row row) {
4756
// The breaker is shared infrastructure so we don't count it but RamUsageTester does
4857
expected -= RamUsageTester.ramUsed(breaker);
4958
expected -= RamUsageTester.ramUsed("topn");
59+
// the sort orders are shared
60+
expected -= RamUsageTester.ramUsed(sortOrders());
5061
return expected;
5162
}
5263
}

0 commit comments

Comments
 (0)