Skip to content

[improvement](be) Let each SNII index spend a size budget on faster .prx windows - #69769

Merged
airborne12 merged 2 commits into
apache:masterfrom
airborne12:snii-prx-zstd-budget
Oct 29, 2026
Merged

airborne12 merged 2 commits into
apache:masterfrom
airborne12:snii-prx-zstd-budget

Conversation

@airborne12

@airborne12 airborne12 commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

What problem does this PR solve?

Issue Number: None

Related PR: #69649, #69770

Problem Summary: #69649 stores a .prx window as PFOR instead of zstd when zstd saves less than snii_prx_zstd_min_saving_percent (30) of the PFOR frame, because every read of a zstd window pays a decompression. What that costs and buys depends on how much zstd saves on the data. Replaying the 1.6M windows of 10M AgentLogs rows, 30 spends 8% more bytes on the windows zstd compresses and cuts their decode time by 42%; on the windows of TextBench service logs it spends 5% and cuts 6%; on data whose windows zstd shrinks by just under 30% it stores every window as PFOR. No single percentage fits all data.

Each index of a segment now spends a size budget instead: PrxZstdBudget, owned by its LogicalIndexWriter and reached through PrxWindowLimits, so the resident and the spilled encoder, and the ordinary and the compaction build, all use it. A window whose smallest frame is zstd is stored in its faster frame (PFOR, or raw when smaller) when

  • its saving is below the share whose trades, over the windows the index has seen, fit 8% of their smallest frames, which puts the budget on the windows zstd shrinks least, and
  • the bytes the index has actually traded, this window included, still fit 8% of the smallest frames seen. This makes 8% a bound for any order of windows: fed the TextBench windows from the largest saving to the smallest, the share alone spends 36%.

Until it has seen 64 windows the share is 30, the previous default. The windows counted are those that get a zstd frame (a plain payload of at least 512 bytes), the only ones whose codec a share decides; singleton and short windows are written as before and counted neither in the allowance nor in the warmup. The choice uses the frames the writer builds anyway: nothing is compressed twice or scanned ahead, and the same input gives the same bytes. snii_prx_zstd_min_saving_percent now defaults to -1 (the budget); 0-100 keeps a fixed share, 0 the smallest frame.

The budget is 8% rather than less: with it the AgentLogs phrase queries below read nearly the same frames as with 30, while in the replay a 5% budget cuts the decode time by 31% instead of 39%.

Same binary, one stream load per table, size against the smallest frames (0):

data index, 30 index, budget table, 30 table, budget
AgentLogs 99M rows +4.13% +3.65% +1.92% +1.69%
AgentLogs 10M rows +4.59% +3.37% +2.13% +1.57%
TextBench 12M rows +0.89% +0.79% +0.51% +0.45%
  • The phrase queries read the same frames with 30 and with the budget: at 99M rows all PFOR for DS1, DS6, DS7, A6 and A9 (for example 107,307 frames for DS7), and about 11,170 zstd frames of about 259,000 for A10 with both; on TextBench most windows stay zstd either way (582 against 583 zstd frames of 599 for 'connection reset').
  • Hot queries in one process, budget data against 30 data with the same read path (paired cycles: 6 for the 25 AgentLogs queries at 10M rows, 10 for the 15 TextBench queries): 0.96-1.06 and 0.97-1.05; every query returns the same result.
  • Against the data written before [improvement](be) Let SNII skip zstd for .prx windows where it saves little over PFOR #69649 (smallest frames), the 11 AgentLogs phrase queries at 99M rows take 0.83x in geometric mean with the budget data (DS7 0.63x, A6 0.67x, A9 0.70x, DS4 0.74x, A10 0.77x), as with 30 (0.83x). This run also had the read-path change of [improvement](be) Decode SNII zstd and raw .prx windows faster #69770, which does not touch the PFOR windows these queries read.
  • Data bytes are identical, and the index written with 0 and with 30 has exactly the size the previous binary writes.
  • Write cost: the choice adds about 48 ns per window, 79 ms over the 1.6M AgentLogs windows.

Release note

SNII now chooses per index how many position windows to store in the faster but larger PFOR form, spending at most 8% of the size of the windows zstd compresses; snii_prx_zstd_min_saving_percent defaults to -1 for this, and 0-100 still sets a fixed percentage.

Check List (For Author)

  • Test: Unit Test
    • SniiPrxZstdBudget: the prior share, the windows zstd shrinks least are traded first, the bound under a descending order, windows with nothing to trade.
    • SniiPrxPodTest: the faster frame (PFOR or raw) and when the budget is asked.
    • SniiStreamedWriterSessionTest: windows that save 1-29% through the ordinary and the streamed (compaction) build; 30 trades all of them, the budget at most 8% of the smallest frames.
    • SniiWriterPrxZstdSaving: the default and the config mapping.
  • Manual test: index size of 10M and 99M AgentLogs and 12M TextBench loads with 0, 30 and the budget; frames read per codec from query profiles; interleaved hot A/B in one process.
  • Behavior changed: Yes (the default .prx codec choice of newly written segments; existing segments are read as before)
  • Does this need documentation: No

🤖 Generated with Claude Code

…prx windows

### What problem does this PR solve?

Issue Number: None

Related PR: apache#69649

Problem Summary: apache#69649 stores a .prx window as PFOR instead of zstd when zstd saves less than
snii_prx_zstd_min_saving_percent (30) of the PFOR frame, because every read of a zstd window pays a
decompression. What that costs and buys depends on how much zstd saves on the data. Replaying the
1.6M windows of 10M AgentLogs rows, 30 spends 8% more bytes on the windows zstd compresses and
cuts their decode time by 42%; on the windows of TextBench service logs it spends 5% and cuts 6%;
on data whose windows zstd shrinks by just under 30% it stores every window as PFOR. No single
percentage fits all data.

Each index of a segment now spends a size budget instead: PrxZstdBudget, owned by its
LogicalIndexWriter and reached through PrxWindowLimits, so the resident and the spilled encoder,
and the ordinary and the compaction build, all use it. A window whose smallest frame is zstd is
stored in its faster frame (PFOR, or raw when smaller) when
- its saving is below the share whose trades, over the windows the index has seen, fit 8% of their
  smallest frames, which puts the budget on the windows zstd shrinks least, and
- the bytes the index has actually traded, this window included, still fit 8% of the smallest
  frames seen. This makes 8% a bound for any order of windows: fed the TextBench windows from the
  largest saving to the smallest, the share alone spends 36%.
Until it has seen 64 windows the share is 30, the previous default. The choice uses the frames the
writer builds anyway: nothing is compressed twice or scanned ahead, and the same input gives the
same bytes. snii_prx_zstd_min_saving_percent now defaults to -1 (the budget); 0-100 keeps a fixed
share, 0 the smallest frame.

The budget is 8% rather than less: with it the AgentLogs phrase queries below read nearly the same
frames as with 30, while in the replay a 5% budget cuts the decode time by 31% instead of 39%.

10M rows each, same binary, one stream load per table, size against the smallest frames (0):
- AgentLogs: index +4.59% with 30, +3.37% with the budget (table +2.13%, +1.57%). The phrase
  queries read the same frames: all PFOR for DS1, DS2, DS6, DS7, A6 and A9 with both, and 326
  against 324 zstd frames of 5011 for DS4, 1049 against 1041 of 25930 for A10.
- TextBench: index +0.89% with 30, +0.79% with the budget (table +0.51%, +0.45%). Most windows
  stay zstd either way (P1 'connection reset': 582 against 583 zstd frames of 599).
- Data bytes are identical; the index written with 0 and with 30 is byte-for-byte the size the
  previous binary writes.
- Hot queries, budget data against 30 data in one process (6-10 paired cycles, ratio of medians per
  cycle): all 25 AgentLogs and 15 TextBench queries 0.96-1.04, the same results.
- Write cost: the choice adds about 48 ns per window, 79 ms over the 1.6M AgentLogs windows.

### Release note

SNII now chooses per index how many position windows to store in the faster but larger PFOR form,
spending at most 8% of their size; snii_prx_zstd_min_saving_percent defaults to -1 for this, and
0-100 still sets a fixed percentage.

### Check List (For Author)

- Test: Unit Test (SniiPrxZstdBudget: the prior share, the windows zstd shrinks least traded first,
  the bound under a descending order, windows with nothing to trade; SniiPrxPodTest: the faster
  frame and when the budget is asked; SniiStreamedWriterSessionTest: windows that save 1-29% through
  the ordinary and the streamed build, where 30 trades all of them and the budget at most 8%;
  SniiWriterPrxZstdSaving: the default and the config mapping). Manual test: index size of 10M-row
  AgentLogs and TextBench loads with 0, 30 and the budget, frames read per codec from query profiles.
- Behavior changed: Yes (the default .prx codec choice of newly written segments; existing segments
  are read as before)
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@airborne12

Copy link
Copy Markdown
Member Author

run buildall

@airborne12

Copy link
Copy Markdown
Member Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed all 11 changed files at head 170822d, including ordinary/streamed builds and resident/replayable encoding. One P2 finding: successful fast-path windows are omitted from the per-index budget, so later windows can retain zstd despite available space for faster frames. No existing inline findings or additional user-provided focus were supplied.

Critical checkpoints:

  • Goal and proof: the per-index adaptive policy and fixed-percentage override are implemented, with one incomplete accounting path described inline. Added tests exercise the budget and both writer modes, but omit mixed short-window/compressible-window accounting.
  • Scope and clarity: changes are focused on codec policy, ownership/config propagation, and unit coverage; existing codec selection and serialization are reused.
  • Concurrency and locks: each index owns its mutable budget. Production flush and merge paths feed one writer serially; no new threads, shared global budget, lock acquisitions, or lock-order changes were introduced.
  • Lifecycle and initialization: heap-owned LogicalIndexWriter instances remain at stable addresses; synchronous encoders borrow their budget through copied limits. Moving owning unique_ptrs does not move the writer. Constants are constexpr, with no new cross-translation-unit initialization dependency.
  • Configuration: the existing mutable setting now defaults to -1; negative values enable the budget, and 0-100 preserve fixed mode. Both direct-load and compaction resolve it for each new index; already-started writers retain their resolved policy.
  • Compatibility: codec IDs, frame layouts, lengths, CRCs, and reader allocation guards are unchanged. Budget state is writer-only; existing readers support every selected codec. No new libc APIs or build/link dependencies were introduced; no ELF/runtime validation was performed.
  • Thrift/protobuf: no gensrc changes.
  • Parallel paths: ordinary and streamed writes share process_term and the same budget owner; resident and replayable encoders reach the new chooser. Both also contain the fast-path accounting omission covered by the single inline finding.
  • Conditional checks: warmup, histogram eligibility, spend limits, and reader-safe frame alternatives have concrete comments and defined states. Split validation occurs before budget mutation.
  • Test coverage: inspected the added warmup, least-saving preference, descending-order cap, no-trade credit, raw/PFOR/readability, config mapping, and ordinary/streamed size tests, plus adjacent round-trip, spill and failure coverage. The no-trade test calls add() directly and cannot detect the production omission. No builds, tests, or benchmarks were run under this static-only contract.
  • Test results: added expected values and size inequalities are consistent with the code by inspection; no generated regression-output files changed. Runtime results remain unverified.
  • Observability: existing per-codec decode counters/timing and framing errors remain available; no separate observability defect was established.
  • Transactions and persistence: transaction versions, delete bitmaps, EditLog/replay, and FE failover protocols are unchanged. Existing index serialization is reused.
  • Writes and failure atomicity: unsuccessful split attempts do not credit or spend budget. Errors after a choice poison the enclosing logical/compound writer, preventing reuse of abandoned accounting or sealing of partial output.
  • FE/BE propagation: no new transmitted variables or protocol fields.
  • Performance and memory: one fixed 101-bucket histogram per index; bounded scans and arithmetic use already-built frame sizes, with no second compression or added payload allocation. The inline finding concerns missed opportunities for faster reads; no claim of size-cap violation or wrong query results is made.
  • Other concerns: no additional verified correctness, error-propagation, include-hygiene, or ownership issue. Every initial risk was resolved to the accepted omission or dismissed with call-chain evidence.

Coverage limits: static source and test inspection only; compression timings, benchmark claims, final ELF ABI, and runtime spill equivalence were not executed or measured.

Review converged after 2 round(s).

const AutoPrxCodecChoice choice = select_auto_prx_codec(
payload.size(), plain.size(), compressed.size(), has_compressed,
limits.max_uncomp_bytes, limits.zstd_min_saving_percent);
const AutoPrxCodecChoice choice = choose_auto_prx_codec(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Credit fast-path windows to the per-index budget

The new budget only sees windows that reach choose_auto_prx_codec, but this builder emits singleton RAW and sub-512-byte PFOR windows without reaching that call; choose_position_codec has the same bypasses in the replayable path. Their committed frames therefore supply neither budget bytes nor warmup observations, so indexes with many short posting lists unnecessarily keep later windows as zstd even when their faster frames fit the 8% index budget. For example, 64 singleton {7} frames contribute 576 bytes; for the 900-byte zstd / 1000-byte PFOR choice used in the new selector test, those bytes raise the allowance from 72 to 118, enough for the 100-byte trade, but the current writer ignores them. Record each successfully emitted fast-path frame with PrxZstdBudget::add in both encoders, and cover a short-window prefix followed by compressible windows through the writer rather than only calling add directly.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is deliberate; c360054 makes it explicit in the class comment and the config comment.

The budget is a share of the windows that get a zstd frame (a plain payload of at least 512 bytes), the only ones whose codec a share decides. Singleton and short windows are never traded, so they are left out of both the allowance and the warmup:

  • Counting their bytes would make the allowance for the large windows grow with the number of short posting lists an index has: in an index of many rare terms, its large windows could grow by well over 8% of their own size. The sizes in the description (the budget below the fixed 30 on every dataset measured) use this definition.
  • Counting them toward the warmup, as add() does, would end the prior share before any tradable window is seen. The share computed from an empty histogram is 100, so the first large windows would then be traded whatever their saving until the cap stops it.

So the 8% is a share of the windows the policy acts on, the stricter of the two readings.

…share of

The budget is a share of the smallest frames of the windows that get a zstd
frame, the only ones whose codec a share decides; windows written without one
(a singleton, or too short to compress) are not counted. Comments only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@airborne12

Copy link
Copy Markdown
Member Author

run buildall

@airborne12

Copy link
Copy Markdown
Member Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No new actionable findings at c360054eeea27241056d8ab256e1d487b7d611dc. Reviewed all 11 changed files and the production load/rebuild and streamed-compaction chains, including resident and replayable position encoding. The review converged in 1 round: both complete-review scopes and the separate risk-focused investigation returned NO_NEW_VALUABLE_FINDINGS, and every candidate and initial risk has an evidence-backed disposition.

The existing short-window thread was revalidated against the author reply and current class/config comments. Excluding singleton and short windows from both allowance and warmup matches the explicitly defined eligible-window budget, so that concern is not repeated. There are no existing P0/P1 findings to carry forward. No additional user focus was supplied.

Required checkpoint Conclusion
Goal and supporting tests The shared chooser implements the per-index size budget using complete frames and an independent cumulative-byte admission check. Added unit tests express warmup, histogram selection, byte-bound, readability and configuration expectations; the session test covers ordinary and streamed output.
Scope and reuse The change is focused on write-policy selection, configuration capture and tests, reusing the existing codec selector and framing.
Concurrency and locks Budget state belongs to one sequential logical-index writer. Ordinary iteration and compaction pushes consume terms synchronously; no new shared global state, thread, lock or lock-order edge was found.
Lifecycle and static initialization Limits borrow a writer-owned inline budget. Production moves heap ownership through unique_ptr without moving the writer object, preserving its address. No ownership cycle or cross-translation-unit initialization dependency is introduced.
Configuration The existing mutable setting now defaults to -1. Negative selects the budget; nonnegative values retain the fixed percentage and existing clamping. New index creation captures current policy without requiring restart.
Storage/API compatibility RAW/PFOR/ZSTD codec IDs, framing, CRCs and reader guards are unchanged. The budget is writer-only; existing readers consume the selected existing frame formats.
Thrift/protobuf IDs No generated protocol definitions or numeric IDs change.
Parallel paths Production ordinary/direct-load/rebuild and streamed compaction use the shared resolver. Resident and replayable encoders both forward the owned budget to the shared chooser.
Conditional branches Readability gates and strict minimum-frame comparisons make the size differences nonnegative; unreadable PFOR is not selected as a faster alternative. The documented short-window bypasses match across encoders.
Test coverage Inspected all changed tests and their setup/cleanup. The new session test exercises resident encoding; no new test explicitly forces spilling with the budget enabled. The replayable branch and its propagation were reviewed directly.
Expected test results Added frame-size arithmetic and inequalities match the implementation by inspection. Existing golden digests are preserved; no regression output files change. No test execution is claimed.
Observability Codec identity remains persisted in frame headers and existing status/reader checks remain available. No new distributed operation or error mode requiring separate tracing was found.
Transactions and persistence Only codec choice changes. Selected codecs persist in existing frames; EditLog, failover, visible-version and delete-bitmap behavior are untouched.
Writes, errors and crash handling Split requests occur before budget mutation. Successfully built inline/windowed frames are copied or streamed unchanged. Later codec, allocation or I/O failures poison the writer/container and prevent sealing or successful reuse of partial state.
FE/BE variables No transmitted variable or FE send path is added; this is a BE-local write policy.
Performance and memory The inline histogram has fixed size and scans at most 100 bins per eligible observation after warmup. Already-built candidates are reused; there is no added compression pass, full-index buffering or posting rescan. Performance measurements were not reproduced.
Other module checkpoints No new PCH/hub-header dependency, template-instantiation family, source-cpp test inclusion, CLucene boundary or glibc API/toolchain change was found. No additional actionable defect remained after the final sweep.

Static review only: no builds, test runs, benchmarks, source modifications or submodule initialization were performed. Runtime behavior, forced-spill budget execution and built-artifact compatibility remain outside this review's validation. These limits are distinct from review convergence; all assigned static scopes and candidate dispositions are complete.

Review converged after 1 round(s).

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 28633 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit c360054eeea27241056d8ab256e1d487b7d611dc, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17617	5118	5025	5025
q2	2129	302	249	249
q3	10276	1401	841	841
q4	4681	457	329	329
q5	7420	798	524	524
q6	181	182	151	151
q7	733	786	598	598
q8	9258	1355	1079	1079
q9	5724	4192	4191	4191
q10	6829	1299	1028	1028
q11	433	240	226	226
q12	629	392	276	276
q13	18118	3593	3028	3028
q14	282	273	250	250
q15	q16	721	708	665	665
q17	1208	754	574	574
q18	6979	6182	6097	6097
q19	1100	962	599	599
q20	371	333	224	224
q21	5759	2435	2418	2418
q22	407	324	261	261
Total cold run time: 100855 ms
Total hot run time: 28633 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5914	5928	5879	5879
q2	706	579	566	566
q3	5606	5701	5438	5438
q4	2028	2170	1565	1565
q5	5874	5667	5740	5667
q6	241	186	146	146
q7	2231	1972	1838	1838
q8	2982	2526	2526	2526
q9	7491	7525	7510	7510
q10	3632	3576	3123	3123
q11	577	432	374	374
q12	667	669	499	499
q13	3280	3652	3052	3052
q14	296	290	273	273
q15	q16	695	669	601	601
q17	7666	6986	6875	6875
q18	13027	12166	12817	12166
q19	931	786	808	786
q20	2142	2129	1942	1942
q21	5611	4556	4682	4556
q22	505	448	407	407
Total cold run time: 72102 ms
Total hot run time: 65789 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 151186 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit c360054eeea27241056d8ab256e1d487b7d611dc, data reload: false

query5	4302	581	451	451
query6	428	204	190	190
query7	4812	447	243	243
query8	310	166	152	152
query9	8808	3282	3272	3272
query10	478	304	248	248
query11	6016	3849	3554	3554
query12	140	90	86	86
query13	1246	436	328	328
query14	6151	3911	3633	3633
query14_1	3433	3375	3420	3375
query15	208	204	181	181
query16	945	458	406	406
query17	850	675	540	540
query18	2416	425	306	306
query19	195	185	134	134
query20	87	84	85	84
query21	207	124	108	108
query22	12884	12992	12836	12836
query23	13448	12691	12379	12379
query23_1	12533	12626	12573	12573
query24	7360	834	480	480
query24_1	494	508	500	500
query25	548	422	366	366
query26	1064	257	144	144
query27	2755	447	277	277
query28	4593	2086	2081	2081
query29	1451	603	478	478
query30	299	224	183	183
query31	1007	936	810	810
query32	147	98	94	94
query33	533	310	243	243
query34	877	849	515	515
query35	741	773	658	658
query36	820	816	726	726
query37	135	102	83	83
query38	1771	1730	1675	1675
query39	799	781	739	739
query39_1	745	725	752	725
query40	223	115	98	98
query41	70	67	68	67
query42	88	83	83	83
query43	352	365	309	309
query44	1377	716	724	716
query45	185	170	178	170
query46	900	956	565	565
query47	2937	2801	2689	2689
query48	320	303	220	220
query49	567	407	289	289
query50	694	286	207	207
query51	10666	10208	10426	10208
query52	79	84	69	69
query53	186	207	154	154
query54	236	193	178	178
query55	69	73	63	63
query56	241	224	217	217
query57	1641	1601	1556	1556
query58	267	264	262	262
query59	2081	2140	1932	1932
query60	282	243	215	215
query61	151	147	139	139
query62	393	327	303	303
query63	187	163	151	151
query64	2436	922	806	806
query65	3316	3277	3267	3267
query66	1756	426	308	308
query67	20137	20172	20111	20111
query68	3067	1002	627	627
query69	440	283	249	249
query70	879	796	783	783
query71	305	243	211	211
query72	2747	2578	2289	2289
query73	528	532	307	307
query74	4604	4495	4327	4327
query75	2348	2285	1928	1928
query76	2326	1018	602	602
query77	345	386	278	278
query78	9611	9745	9144	9144
query79	1017	900	537	537
query80	1209	432	353	353
query81	575	317	273	273
query82	610	132	105	105
query83	295	200	182	182
query84	312	121	102	102
query85	855	423	371	371
query86	398	236	231	231
query87	1993	1944	1788	1788
query88	3885	2834	2801	2801
query89	347	287	242	242
query90	1867	168	159	159
query91	161	156	132	132
query92	100	90	86	86
query93	1031	997	569	569
query94	625	319	263	263
query95	557	415	323	323
query96	699	493	237	237
query97	2455	2444	2309	2309
query98	163	150	149	149
query99	763	758	655	655
Total cold run time: 234718 ms
Total hot run time: 151186 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.39 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit c360054eeea27241056d8ab256e1d487b7d611dc, data reload: false

query1	0.00	0.00	0.01
query2	0.10	0.05	0.04
query3	0.26	0.09	0.08
query4	1.61	0.10	0.09
query5	0.23	0.24	0.24
query6	1.17	0.68	0.67
query7	0.04	0.00	0.00
query8	0.05	0.04	0.04
query9	0.40	0.34	0.33
query10	0.58	0.61	0.58
query11	0.21	0.14	0.14
query12	0.17	0.14	0.14
query13	0.50	0.48	0.49
query14	1.02	1.00	1.01
query15	0.62	0.60	0.60
query16	0.34	0.35	0.33
query17	1.16	1.14	1.12
query18	0.19	0.18	0.18
query19	2.13	2.00	1.96
query20	0.02	0.01	0.02
query21	15.50	0.18	0.13
query22	5.04	0.06	0.05
query23	16.27	0.31	0.14
query24	2.86	0.53	0.38
query25	0.13	0.05	0.05
query26	1.39	0.21	0.16
query27	0.05	0.05	0.05
query28	3.75	0.79	0.35
query29	12.48	4.16	3.33
query30	0.28	0.12	0.10
query31	2.77	0.55	0.33
query32	3.22	0.59	0.50
query33	3.19	3.34	3.24
query34	15.34	4.01	3.40
query35	3.37	3.36	3.35
query36	0.56	0.46	0.43
query37	0.10	0.06	0.06
query38	0.05	0.04	0.04
query39	0.04	0.03	0.03
query40	0.17	0.15	0.15
query41	0.08	0.03	0.03
query42	0.04	0.03	0.03
query43	0.04	0.04	0.03
Total cold run time: 97.52 s
Total hot run time: 24.39 s

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 100.00% (6/6) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 77.45% (36276/46837)
Line Coverage 62.71% (409860/653606)
Region Coverage 59.15% (346900/586438)
Branch Coverage 60.07% (160671/267492)

@airborne12
airborne12 marked this pull request as ready for review October 28, 2026 23:37
@airborne12
airborne12 merged commit 0571eb6 into apache:master Oct 29, 2026
50 checks passed
@airborne12
airborne12 deleted the snii-prx-zstd-budget branch October 29, 2026 01:09
airborne12 added a commit that referenced this pull request Oct 29, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: #69649, #69769

Problem Summary: A phrase query decodes the positions of the selected
documents of every .prx window it reads. Timed per window on a 10M-row
AgentLogs load, a zstd window cost 18.6 us against 1.4 us for a PFOR
window: 11.7 us to decode the plain payload, 4.8 us to decompress, and
about 2 us more because every window created and freed its own zstd
context (about 96 KB) and a zero-filled output vector. Two commits:

1. **Decode the plain payload without a Status per document.**
`decode_payload_csr_selective` made at least one out-of-line call
returning a Status for every document of the window, selected or not
(`get_varint32_fast`, `skip_varints`, `decode_delta_run`). The loop now
keeps the cursor in a register and handles the usual cases inline: a
single-byte count, walking past the deltas of a document that is not
selected, and a run of single-byte deltas. Anything else -- a multi-byte
value, a run that does not fit, a prefix sum that would overflow -- goes
to the checked decoders from the same cursor, so they decide and report
as before. The output vector grows as before; nothing is reserved up
front.
2. **Reuse one zstd context and buffer per index source.**
`ZstdDecompressor` keeps one context, created at the first zstd frame,
and one output buffer that only grows and is not zero-filled
(decompression writes every byte before it is read). The buffer comes
from the Doris allocator (`DorisUniqueBufferPtr`), so the decoding
task's memory tracker accounts for it while it is held.
`SniiIndexSource` owns one, and the cursors it opens pass it to the .prx
decoder through `PrxDecodeContext`. The cursors of a source decode on
its thread one window at a time, and the decoded payload is consumed
before the next window is read. A source lives for one query on one
segment, so a query holds at most one context and one buffer per segment
it reads, and only if it decodes a zstd window; before, each window
created and released them. Compaction decodes through an allocation
gate, which keeps the old path, as does a cursor given no decompressor.

Hot queries, old against new read path in one process (run-time switch
in an experiment build, 6 paired cycles), on 99M AgentLogs rows written
with the smallest frames, as every segment written before #69649 is
(mostly zstd windows): DS7 (unfiltered phrase) 0.87x, A6 0.89x, A10
0.93x, DS4 (phrase prefix) 0.94x, A9 0.95x, DS1 0.96x; repeating the old
path against itself gave 0.93-1.05. The gain follows the zstd windows a
query reads: the phrase queries of AgentLogs written with #69649 read
PFOR windows and do not change, and on TextBench, whose large windows
stay zstd, the time per window is the decompression itself and the
change is within noise. Every query returns the same result.

### Release note

None
github-actions Bot pushed a commit that referenced this pull request Oct 29, 2026
### What problem does this PR solve?

Issue Number: None

Related PR: #69649, #69769

Problem Summary: A phrase query decodes the positions of the selected
documents of every .prx window it reads. Timed per window on a 10M-row
AgentLogs load, a zstd window cost 18.6 us against 1.4 us for a PFOR
window: 11.7 us to decode the plain payload, 4.8 us to decompress, and
about 2 us more because every window created and freed its own zstd
context (about 96 KB) and a zero-filled output vector. Two commits:

1. **Decode the plain payload without a Status per document.**
`decode_payload_csr_selective` made at least one out-of-line call
returning a Status for every document of the window, selected or not
(`get_varint32_fast`, `skip_varints`, `decode_delta_run`). The loop now
keeps the cursor in a register and handles the usual cases inline: a
single-byte count, walking past the deltas of a document that is not
selected, and a run of single-byte deltas. Anything else -- a multi-byte
value, a run that does not fit, a prefix sum that would overflow -- goes
to the checked decoders from the same cursor, so they decide and report
as before. The output vector grows as before; nothing is reserved up
front.
2. **Reuse one zstd context and buffer per index source.**
`ZstdDecompressor` keeps one context, created at the first zstd frame,
and one output buffer that only grows and is not zero-filled
(decompression writes every byte before it is read). The buffer comes
from the Doris allocator (`DorisUniqueBufferPtr`), so the decoding
task's memory tracker accounts for it while it is held.
`SniiIndexSource` owns one, and the cursors it opens pass it to the .prx
decoder through `PrxDecodeContext`. The cursors of a source decode on
its thread one window at a time, and the decoded payload is consumed
before the next window is read. A source lives for one query on one
segment, so a query holds at most one context and one buffer per segment
it reads, and only if it decodes a zstd window; before, each window
created and released them. Compaction decodes through an allocation
gate, which keeps the old path, as does a cursor given no decompressor.

Hot queries, old against new read path in one process (run-time switch
in an experiment build, 6 paired cycles), on 99M AgentLogs rows written
with the smallest frames, as every segment written before #69649 is
(mostly zstd windows): DS7 (unfiltered phrase) 0.87x, A6 0.89x, A10
0.93x, DS4 (phrase prefix) 0.94x, A9 0.95x, DS1 0.96x; repeating the old
path against itself gave 0.93-1.05. The gain follows the zstd windows a
query reads: the phrase queries of AgentLogs written with #69649 read
PFOR windows and do not change, and on TextBench, whose large windows
stay zstd, the time per window is the decompression itself and the
change is within noise. Every query returns the same result.

### Release note

None
yiguolei pushed a commit that referenced this pull request Oct 30, 2026
… on faster .prx windows #69769 (#69796)

### What problem does this PR solve?

Issue Number: None

Related PR: #69769, #69649 (backport #69795), #69770 (backport #69788)

Problem Summary: Backport of #69769 to branch-4.2, stacked on #69795
(the backport of #69649). #69649 stores a .prx window as PFOR instead of
zstd when zstd saves less than `snii_prx_zstd_min_saving_percent` (30)
of the PFOR frame, because every read of a zstd window pays a
decompression. What that costs and buys depends on how much zstd saves
on the data. Replaying the 1.6M windows of 10M AgentLogs rows, 30 spends
8% more bytes on the windows zstd compresses and cuts their decode time
by 42%; on the windows of TextBench service logs it spends 5% and cuts
6%; on data whose windows zstd shrinks by just under 30% it stores every
window as PFOR. No single percentage fits all data.

Each index of a segment now spends a size budget instead:
`PrxZstdBudget`, owned by its `LogicalIndexWriter` and reached through
`PrxWindowLimits`, so the resident and the spilled encoder, and the
ordinary and the compaction build, all use it. A window whose smallest
frame is zstd is stored in its faster frame (PFOR, or raw when smaller)
when
- its saving is below the share whose trades, over the windows the index
has seen, fit 8% of their smallest frames, which puts the budget on the
windows zstd shrinks least, and
- the bytes the index has actually traded, this window included, still
fit 8% of the smallest frames seen. This makes 8% a bound for any order
of windows: fed the TextBench windows from the largest saving to the
smallest, the share alone spends 36%.

Until it has seen 64 windows the share is 30, the previous default. The
windows counted are those that get a zstd frame (a plain payload of at
least 512 bytes), the only ones whose codec a share decides; singleton
and short windows are written as before and counted neither in the
allowance nor in the warmup. The choice uses the frames the writer
builds anyway: nothing is compressed twice or scanned ahead, and the
same input gives the same bytes. `snii_prx_zstd_min_saving_percent` now
defaults to -1 (the budget); 0-100 keeps a fixed share, 0 the smallest
frame.

The budget is 8% rather than less: with it the AgentLogs phrase queries
below read nearly the same frames as with 30, while in the replay a 5%
budget cuts the decode time by 31% instead of 39%.

Same binary, one stream load per table, size against the smallest frames
(0):

| data | index, 30 | index, budget | table, 30 | table, budget |
|---|---|---|---|---|
| AgentLogs 99M rows | +4.13% | +3.65% | +1.92% | +1.69% |
| AgentLogs 10M rows | +4.59% | +3.37% | +2.13% | +1.57% |
| TextBench 12M rows | +0.89% | +0.79% | +0.51% | +0.45% |

- The phrase queries read the same frames with 30 and with the budget:
at 99M rows all PFOR for DS1, DS6, DS7, A6 and A9 (for example 107,307
frames for DS7), and about 11,170 zstd frames of about 259,000 for A10
with both; on TextBench most windows stay zstd either way (582 against
583 zstd frames of 599 for 'connection reset').
- Hot queries in one process, budget data against 30 data with the same
read path (paired cycles: 6 for the 25 AgentLogs queries at 10M rows, 10
for the 15 TextBench queries): 0.96-1.06 and 0.97-1.05; every query
returns the same result.
- Against the data written before #69649 (smallest frames), the 11
AgentLogs phrase queries at 99M rows take 0.83x in geometric mean with
the budget data (DS7 0.63x, A6 0.67x, A9 0.70x, DS4 0.74x, A10 0.77x),
as with 30 (0.83x). This run also had the read-path change of #69770,
which does not touch the PFOR windows these queries read.
- Data bytes are identical, and the index written with 0 and with 30 has
exactly the size the previous binary writes.
- Write cost: the choice adds about 48 ns per window, 79 ms over the
1.6M AgentLogs windows.

### Branch adaptations

None. `git cherry-pick 0571eb6` applied cleanly on top of the
backport of #69649, which it needs: it changes the setting #69649 adds,
and the automatic pick to branch-4.2 failed because branch-4.2 does not
have #69649. This PR is stacked on that backport, #69795, so it shows
both commits until #69795 merges; merge #69795 first.

### Validation

- Built and run with both backports (this one and the stacked #69769
backport) applied to branch-4.2 at 711db43, the commit before #69625:
its lance-c update is not in the local third-party build, and the 16
commits after it touch no SNII file. `PrxPod*`, `Snii*`,
`DorisSniiFileReader*`, `*InvertedIndex*`, `*IndexFileWriter*` and
`*IndexFileReader*`: 1710 tests passed, including the new budget, codec
choice, writer and configuration tests.
- On master both changes passed CI.
- Not built locally on the branch-4.2 tip; this PR's BE UT run covers
that.

### Release note

SNII now chooses per index how many position windows to store in the
faster but larger PFOR form, spending at most 8% of the size of the
windows zstd compresses; `snii_prx_zstd_min_saving_percent` defaults to
-1 for this, and 0-100 still sets a fixed percentage.

### Check List (For Author)

- Test: Unit Test
- SniiPrxZstdBudget: the prior share, the windows zstd shrinks least are
traded first, the bound under a descending order, windows with nothing
to trade.
- SniiPrxPodTest: the faster frame (PFOR or raw) and when the budget is
asked.
- SniiStreamedWriterSessionTest: windows that save 1-29% through the
ordinary and the streamed (compaction) build; 30 trades all of them, the
budget at most 8% of the smallest frames.
    - SniiWriterPrxZstdSaving: the default and the config mapping.
- Manual test: index size of 10M and 99M AgentLogs and 12M TextBench
loads with 0, 30 and the budget; frames read per codec from query
profiles; interleaved hot A/B in one process.
- Behavior changed: Yes (the default .prx codec choice of newly written
segments; existing segments are read as before)
- Does this need documentation: No

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: martin.foo838 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants