Skip to content

perf: improve performance of CandidateBin.split_for_size - #8000

Open
fangbo wants to merge 5 commits into
lance-format:mainfrom
fangbo:perf-plan-compaction
Open

perf: improve performance of CandidateBin.split_for_size#8000
fangbo wants to merge 5 commits into
lance-format:mainfrom
fangbo:perf-plan-compaction

Conversation

@fangbo

@fangbo fangbo commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Close #7967

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 81628500-fac7-47a3-a965-54b79e7615e6

📥 Commits

Reviewing files that changed from the base of the PR and between cde981c and 75a609a.

📒 Files selected for processing (1)
  • rust/lance/src/dataset/optimize.rs
💤 Files with no reviewable changes (1)
  • rust/lance/src/dataset/optimize.rs

📝 Walkthrough

Walkthrough

Rewrites CandidateBin::split_for_size to calculate split boundaries from accumulated and remaining row counts, then materializes bins by slicing iterators instead of repeatedly draining input vectors.

Changes

Candidate bin splitting

Layer / File(s) Summary
Row-count-based splitting
rust/lance/src/dataset/optimize.rs
split_for_size tracks total, accumulated, and remaining rows, emits boundaries only when both portions meet min_num_rows, and materializes intermediate bins without front-draining vectors.

Estimated code review effort: 3 (Moderate) | ~15–30 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: improving CandidateBin.split_for_size performance.
Description check ✅ Passed The description is related to the PR and references the linked issue being closed.
Linked Issues check ✅ Passed The rewrite removes repeated tail sums and front drains, matching the issue's O(n^2) hotspots.
Out of Scope Changes check ✅ Passed The change stays focused on split_for_size performance and does not introduce unrelated scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/lance/src/dataset/optimize.rs`:
- Around line 2371-2393: Update the Fragment literal inside tail_too_small_bin
to initialize the required overlays field with an empty vector, matching
Fragment::new(), while leaving the split_for_size assertions unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 612bb216-91e4-43e7-a4f9-437f1d3f0e9e

📥 Commits

Reviewing files that changed from the base of the PR and between fd9f98d and f32e600.

📒 Files selected for processing (1)
  • rust/lance/src/dataset/optimize.rs

Comment thread rust/lance/src/dataset/optimize.rs Outdated
Comment on lines +2371 to +2393

let tail_too_small_bin = CandidateBin {
fragments: std::iter::repeat_n(
Fragment {
id: 0,
files: vec![],
deletion_file: None,
row_id_meta: None,
physical_rows: Some(0),
last_updated_at_version_meta: None,
created_at_version_meta: None,
},
3,
)
.collect(),
pos_range: 0..3,
candidacy: std::iter::repeat_n(CompactionCandidacy::CompactItself, 3).collect(),
row_counts: vec![600, 100, 100],
indices: vec![],
};
let split = tail_too_small_bin.split_for_size(500);
assert_eq!(split.len(), 1);
assert_eq!(split[0].pos_range, 0..3);

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.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the required fields of lance_table::format::Fragment
rg -nP -A 25 '^pub struct Fragment\b' rust/lance-table/src/format

Repository: lance-format/lance

Length of output: 2246


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate optimize.rs =="
fd -a 'optimize\.rs$' rust | sed 's#^\./##'

echo "== count Fragment literals in tail_too_small range =="
sed -n '2360,2396p' rust/lance/src/dataset/optimize.rs | nl -ba -v2360

echo "== Fragment struct full field list =="
python3 - <<'PY'
from pathlib import Path
p=Path('rust/lance-table/src/format/fragment.rs')
txt=p.read_text()
start=txt.index('pub struct Fragment {')
end=txt.index('}', start)+1
struct=txt[start:end]
print(struct)
print("fields:", [line.strip().removesuffix(',').split(':',1)[0].strip() for line in struct.splitlines() if ':' in line and not line.strip().startswith('///') and not line.strip().startswith('#[')])
PY

echo "== check whether FileFragment has overlays field =="
rg -nP -A 12 '^pub struct FileFragment\b|pub overlays' rust/lance-table/src/format/fragment.rs

Repository: lance-format/lance

Length of output: 452


🏁 Script executed:

#!/bin/sh
set -euo pipefail

echo "== optimize.rs tail_too_small block =="
sed -n '2360,2396p' rust/lance/src/dataset/optimize.rs | cat -n

echo "== Fragment struct definition and FileFragment overlay field =="
python3 - <<'PY'
from pathlib import Path
p = Path('rust/lance-table/src/format/fragment.rs')
text = p.read_text()

start_struct = text.index('pub struct Fragment {')
# find matching brace is optional for next extraction; stop at next matching brace roughly by line indentation
lines = text.splitlines()
found = False
field_lines = []
brace_nesting = 0
for i, line in enumerate(lines, 1):
    if found:
        field_lines.append(f"{i}: {line}")
        brace_nesting += line.count('{') - line.count('}')
        if brace_nesting == 0 and '}' in line:
            break
    if line.strip() == 'pub struct Fragment {':
        found = True

print("\nFragment struct:")
print("\n".join(field_lines))

ff_start = text.index('pub struct FileFragment {')
ff_lines = []
brace_nesting = 0
found = False
for i, line in enumerate(lines, 1):
    if found:
        ff_lines.append(f"{i}: {line}")
        brace_nesting += line.count('{') - line.count('}')
        if brace_nesting == 0 and '}' in line:
            break
    if line.strip() == 'pub struct FileFragment {':
        found = True

print("\nFileFragment struct:")
print("\n".join(ff_lines))
PY

Repository: lance-format/lance

Length of output: 3828


Don’t add a Fragment literal without overlays.

This Fragment struct literal still omits the required pub overlays: Vec<DataOverlayFile> field, so the test will fail to compile with E0063. Add overlays: vec![] here, matching how Fragment::new() initializes that field.

🧰 Tools
🪛 GitHub Check: format

[warning] 2382-2382:
Diff in /home/runner/work/lance/lance/rust/lance/src/dataset/optimize.rs

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance/src/dataset/optimize.rs` around lines 2371 - 2393, Update the
Fragment literal inside tail_too_small_bin to initialize the required overlays
field with an empty vector, matching Fragment::new(), while leaving the
split_for_size assertions unchanged.

@coderabbitai coderabbitai 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.

♻️ Duplicate comments (1)
rust/lance/src/dataset/optimize.rs (1)

2372-2393: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Missing required overlays field on Fragment literal — still breaks compilation.

This Fragment literal omits overlays: Vec<DataOverlayFile>, unlike the other Fragment literal earlier in the same test (line 2330) which correctly sets overlays: vec![]. This will fail to compile with E0063 (missing field).

🐛 Proposed fix
             fragments: std::iter::repeat_n(
                 Fragment {
                     id: 0,
                     files: vec![],
+                    overlays: vec![],
                     deletion_file: None,
                     row_id_meta: None,
                     physical_rows: Some(0),
                     last_updated_at_version_meta: None,
                     created_at_version_meta: None,
                 },
                 3,
             )
             .collect(),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance/src/dataset/optimize.rs` around lines 2372 - 2393, Update the
Fragment literal in the tail_too_small_bin test fixture to initialize the
required overlays field with an empty vector, matching the other Fragment
literal in the same test. Do not change the split_for_size assertions or other
fixture behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@rust/lance/src/dataset/optimize.rs`:
- Around line 2372-2393: Update the Fragment literal in the tail_too_small_bin
test fixture to initialize the required overlays field with an empty vector,
matching the other Fragment literal in the same test. Do not change the
split_for_size assertions or other fixture behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: b6137db2-1a75-4dcd-94b7-8a113d763f28

📥 Commits

Reviewing files that changed from the base of the PR and between f32e600 and cde981c.

📒 Files selected for processing (1)
  • rust/lance/src/dataset/optimize.rs

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@fangbo

fangbo commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

@yanghua @zhangyue19921010 Could you please review this?

Comment thread rust/lance/src/dataset/optimize.rs Outdated

// Only split once the current bin is large enough and there is
// enough left over to form another worthwhile bin.
if current_rows >= min_num_rows && remaining_rows >= min_num_rows {

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: When target_rows_per_fragment is 0, split_for_size creates an empty trailing bin, which eventually causes a panic when commit logic accesses old_fragments[0]. Reject zero at the API boundary or avoid creating the final empty bin, and add a regression test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Great catch ! In the context of Compaction, target_rows_per_fragments is allowed to be 0. Therefore, I fixed an issue in the split_for_size method where an empty CandidateBin could be produced when min_num_rows = 0. In addition, I added test cases for min_num_rows = 0.

@fangbo
fangbo force-pushed the perf-plan-compaction branch from db8bf38 to e62e996 Compare July 28, 2026 07:13

@zhangyue19921010 zhangyue19921010 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.

LGTM. Thanks for the precise fix! @yanghua Would u mind to give a final pass?

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.

perf: CandidateBin.split_for_size is too slow for hundreds of thousands fragments

2 participants