Skip to content

Commit b976eab

Browse files
committed
Merge remote-tracking branch 'origin/master' into claude/fix-partition-address-limit
# Conflicts: # CHANGELOG.md
2 parents 1194255 + 4f8ec02 commit b976eab

4 files changed

Lines changed: 63 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,24 @@ LOAD erpl;
2626

2727
### Fixed
2828

29+
- **[rfc]** **`fetch_size` and `partitions` did nothing to the batch size of a
30+
single-column scan.** `MaxBatchSizeForColumnCount` returned the full 32768-row cap
31+
whenever a scan projected one column, discarding the budget that
32+
`ResolveEffectiveMaxBatchSize` had just divided across the partition workers — so every
33+
worker asked SAP for `ROWCOUNT=32768` however `erpl_rfc_fetch_size` and `partitions`
34+
were set. The exemption was deliberate, on the reasoning that a narrow scan is already
35+
under any sane budget; that reasoning ignored the division, and a narrow scan is
36+
precisely what partitioning exists for.
37+
38+
Measured on a 300,000-row single-column read at `partitions = 8`: peak RSS **405 MB →
39+
307 MB (−24%)** with no loss of throughput (2.17s → 2.07s). The budget now binds for
40+
every column count; only a zero budget or a scan with no projected column bypasses it.
41+
42+
- **[rfc]** A partitioned scan never returned its allocator arenas to the OS. The serial
43+
path calls `malloc_trim(0)` when the scan finishes, but each partition worker retires on
44+
a different code path that did not, so every worker thread's glibc arena kept its
45+
high-water mark — which is what `getrusage` reports.
46+
2947
- **[rfc]** **A partitioned scan past the `ROWSKIPS` ceiling returned a truncated result
3048
instead of refusing.** The row-window scheduler signalled the ABAP `INT4` limit by
3149
returning "no more windows", which is the same answer it gives at the end of a table —

rfc/src/sap_rfc.cpp

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1246,8 +1246,8 @@ namespace duckdb
12461246
// so without dividing here the budget would be per worker and peak memory would
12471247
// scale with the partition count -- the opposite of what a budget is for.
12481248
//
1249-
// Narrow scans, which is what partitioning is for, are unaffected: the batch is
1250-
// already capped at MAX_BATCH_SIZE long before the divided budget binds.
1249+
// Narrow scans are the point of partitioning, so they must feel this division
1250+
// too -- see MaxBatchSizeForColumnCount, which no longer exempts a single column.
12511251
auto budget = EffectiveFetchSize();
12521252
auto workers = GetPartitionCount();
12531253
if (workers > 1 && budget > 0) {
@@ -1592,7 +1592,15 @@ namespace duckdb
15921592
// concurrent-row budget, floored to a power of two in
15931593
// [STANDARD_VECTOR_SIZE, MAX_BATCH_SIZE] so the doubling warm-up still
15941594
// reaches the cap cleanly and ROWSKIPS % ROWCOUNT stays valid.
1595-
if (num_columns <= 1 || concurrent_row_budget == 0) {
1595+
// A zero budget disables the cap, and a scan with no projected column has
1596+
// nothing to bound. Note that num_columns == 1 is NOT exempt: it used to be,
1597+
// on the reasoning that a narrow scan is already under any sane budget -- but
1598+
// ResolveEffectiveMaxBatchSize divides the budget by the partition count, and
1599+
// a narrow scan is precisely what partitioning is for. Exempting it meant
1600+
// erpl_rfc_fetch_size and partitions silently did nothing to the batch for the
1601+
// one scan shape that most needed them (heaptrack showed 32768-row batches on
1602+
// every worker at partitions=8).
1603+
if (num_columns == 0 || concurrent_row_budget == 0) {
15961604
return MAX_BATCH_SIZE;
15971605
}
15981606
unsigned int per_col = concurrent_row_budget / num_columns;

rfc/src/scanner_read_table.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,13 @@ namespace duckdb
172172
}
173173
if (claim != RfcRowWindowScheduler::ClaimResult::CLAIMED) {
174174
// Nothing left to claim; an empty chunk retires this worker.
175+
#ifdef __GLIBC__
176+
// Mirror the serial path: this worker is done, its per-column SDK
177+
// handles are released, and each worker thread has its own glibc
178+
// arena. Without this the arenas keep their high-water mark and
179+
// getrusage still reports the peak long after the rows are gone.
180+
malloc_trim(0);
181+
#endif
175182
return;
176183
}
177184
for (auto &sm : lstate.machines) {

rfc/test/cpp/test_read_table_batching.cpp

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,39 @@ TEST_CASE("NextDesiredBatchSize doubles geometrically while keeping ROWSKIPS val
7878
REQUIRE(size == SM::MAX_BATCH_SIZE);
7979
}
8080

81+
TEST_CASE("A single-column scan honours a small budget instead of ignoring it",
82+
"[erpl_rfc][batching]") {
83+
using SM = RfcReadColumnStateMachine;
84+
85+
// ResolveEffectiveMaxBatchSize divides the budget across partition workers so that
86+
// peak memory does not scale with the worker count. A single-column scan used to
87+
// short-circuit to MAX_BATCH_SIZE before the divided budget was ever consulted, so
88+
// every worker asked SAP for ROWCOUNT=32768 no matter what erpl_rfc_fetch_size or
89+
// partitions said -- confirmed on a4h by heaptrack, which showed 32768-row batches
90+
// under partitions=8. Narrow scans are exactly what partitioning is for, so this is
91+
// the case where the budget matters most.
92+
REQUIRE(SM::MaxBatchSizeForColumnCount(1, 2048) == (unsigned int)STANDARD_VECTOR_SIZE);
93+
REQUIRE(SM::MaxBatchSizeForColumnCount(1, 8192) == 8192u);
94+
95+
// The default fetch size (16384 concurrent rows) split across 8 workers.
96+
REQUIRE(SM::MaxBatchSizeForColumnCount(1, 16384u / 8u) == (unsigned int)STANDARD_VECTOR_SIZE);
97+
98+
// A budget large enough for the full batch still yields it -- the cap binds, it does
99+
// not shrink for its own sake.
100+
REQUIRE(SM::MaxBatchSizeForColumnCount(1, 256u * 1024u) == SM::MAX_BATCH_SIZE);
101+
102+
// Zero columns still means "no projection to bound"; a zero budget still disables it.
103+
REQUIRE(SM::MaxBatchSizeForColumnCount(0, 2048) == SM::MAX_BATCH_SIZE);
104+
REQUIRE(SM::MaxBatchSizeForColumnCount(1, 0) == SM::MAX_BATCH_SIZE);
105+
}
106+
81107
TEST_CASE("MaxBatchSizeForColumnCount bounds the SDK buffer on wide scans",
82108
"[erpl_rfc][batching]") {
83109
using SM = RfcReadColumnStateMachine;
84110
// Use an explicit budget so the test is independent of the runtime default.
85111
constexpr unsigned int BUDGET = 256u * 1024u;
86112

87-
// Narrow scans keep the full batch for throughput.
113+
// Narrow scans keep the full batch when the budget is large enough to allow it.
88114
REQUIRE(SM::MaxBatchSizeForColumnCount(0, BUDGET) == SM::MAX_BATCH_SIZE);
89115
REQUIRE(SM::MaxBatchSizeForColumnCount(1, BUDGET) == SM::MAX_BATCH_SIZE);
90116
REQUIRE(SM::MaxBatchSizeForColumnCount(2, BUDGET) == SM::MAX_BATCH_SIZE);

0 commit comments

Comments
 (0)