Skip to content

perf(query): remaining full-blob leaf opens in fast paths — convert dir-only sites, add lazy local leaf handle for conditional column loads #1327

Description

@bplatz

Context

BinaryIndexStore::open_leaf_handle on local storage reads the entire leaf blob (std::fs::read) even when the caller only needs the leaflet directory. The metadata-only count walks were converted to the prefix-reading, cached open_leaf_dir (see the dir-only leaf opens work), and fast_string_fold / fast_min_max_string already use a dir-first + lazy-escalation pattern. A sweep of the remaining ~24 open_leaf_handle(..., false) call sites in fluree-db-query found three tiers.

Tier 1 — pure directory-only sites (mechanical open_leaf_dir conversions)

These never call load_columns; the full-blob read is pure waste:

site function walk size
count_plan_exec.rs:3469 predicate_objects_all_iri (optional-chain gate) every leaf of a predicate
fast_post_order_limit.rs:368 base_predicate_o_type (type-homogeneity prepass for ORDER BY LIMIT) every leaf of a predicate
count_plan_exec.rs:1023 collect_k_first_subject_bounds_psot (partition bounds) k leaves
fast_count.rs:417 boundary_leaf_pid_extent (numeric-compare extent) ~2 leaves/query

The first two are the same shape as the global distinct-count fix and walk a predicate's whole leaf list per query. Conversion also picks up the decoded-dir cache for free.

Tier 2 — conditional column loads (dir-first + lazy escalation)

~14 sites load columns only for some leaflets (mixed o_type_const, boundary leaflets of a range, threshold-interior skipping), e.g.:

  • fast_count.rs:753/855 — LANG-filter count: loads OType column only for mixed leaflets
  • fast_count.rs:1369 — literal-count leaflet fallback: same mixed-leaflet condition
  • fast_count.rs:461 — numeric-compare count: skips leaflets provably all-in/all-out of the threshold
  • fast_count.rs:1459 — blank-node subject count: decodes only range-boundary leaflets
  • fast_path_common.rs:977/1171/1404/1688/1877, count_plan_exec.rs:2832/3682, fast_predicate_scalar_agg.rs:373, fast_exists_join_count_distinct_object.rs:97 — various per-leaflet skip conditions

These should follow the fast_string_fold.rs pattern: open_leaf_dir first, open a handle only when a leaflet actually needs columns. The payoff per site is proportional to its skip rate.

Building block: lazy local leaf handle

The infrastructure for doing this well already exists end-to-end:

  • ContentStoreRangeFetcher::fetch_range (binary_index_store.rs) already does positional read_at on local files
  • RangeReadLeafHandle already implements lazy per-leaflet column loads over byte ranges — it is just only constructed for the remote path

Exposing a lazy handle for local leaves (e.g. open_leaf_handle_lazy) fixes the remaining flaw in the existing hybrid sites too: today, escalating for a single mixed leaflet reads the whole blob; a lazy handle would pread only that leaflet's column blocks. With it, Tier 2 migrations become the same mechanical pattern everywhere.

Tier 3 — leave alone

Sites that decode essentially every leaflet (collect_subjects_for_predicate_*, PsotSubjectCountIter, collect_subjects_with_object_in, collect_post_desc_topk*): one sequential full-blob read beats many small preads when you're decoding everything. Keep open_leaf_handle there — this is per-site judgment, not a blanket swap. (binary_history.rs needs the sidecar via need_replay=true; also correct as-is.)

Suggested order

  1. Tier 1 conversions (one-liners, immediate win on the two full-predicate walks)
  2. open_leaf_handle_lazy building block + tests
  3. Tier 2 migrations, highest-skip-rate sites first, each validated against its existing fast-path tests

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions