Skip to content

Commit 7abbe13

Browse files
committed
rename and better document some behavior
Signed-off-by: Adam Gutglick <adam@spiraldb.com>
1 parent 983468c commit 7abbe13

6 files changed

Lines changed: 95 additions & 51 deletions

File tree

vortex-layout/src/children.rs

Lines changed: 73 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,13 @@ pub trait LayoutChildren: 'static + Send + Sync {
3838

3939
fn nchildren(&self) -> usize;
4040

41-
/// Returns `true` if the child at `idx` is known — without materializing it — to register no
42-
/// split boundaries strictly inside its row range (see
43-
/// [`VTable::registers_interior_splits`](crate::VTable::registers_interior_splits)).
41+
/// Returns `true` if the child at `idx` is divisible: it may register split boundaries
42+
/// strictly inside its row range (see [`VTable::is_divisible`](crate::VTable::is_divisible)).
4443
///
45-
/// Implementations may conservatively return `false` when the answer would require
44+
/// Implementations must conservatively return `true` when answering would require
4645
/// materializing the child.
47-
fn child_divisibility(&self, _idx: usize) -> bool {
48-
false
46+
fn child_is_divisible(&self, _idx: usize) -> bool {
47+
true
4948
}
5049
}
5150

@@ -74,8 +73,8 @@ impl LayoutChildren for Arc<dyn LayoutChildren> {
7473
self.as_ref().nchildren()
7574
}
7675

77-
fn child_divisibility(&self, idx: usize) -> bool {
78-
self.as_ref().child_divisibility(idx)
76+
fn child_is_divisible(&self, idx: usize) -> bool {
77+
self.as_ref().child_is_divisible(idx)
7978
}
8079
}
8180

@@ -119,8 +118,8 @@ impl LayoutChildren for OwnedLayoutChildren {
119118
self.0.len()
120119
}
121120

122-
fn child_divisibility(&self, idx: usize) -> bool {
123-
!self.0[idx].dyn_registers_interior_splits()
121+
fn child_is_divisible(&self, idx: usize) -> bool {
122+
self.0[idx].dyn_is_divisible()
124123
}
125124
}
126125

@@ -134,10 +133,7 @@ pub(crate) struct ViewedLayoutChildren {
134133
allow_unknown: bool,
135134
session: VortexSession,
136135
cache: Arc<[OnceCell<LayoutRef>]>,
137-
/// Single-slot memo for [`LayoutChildren::child_has_no_interior_splits`]: children of one
138-
/// layout node almost always share an encoding, so remember the last resolved flatbuffer
139-
/// encoding tag and its answer. Packed as `tag | (answer << 16) | (valid << 17)`.
140-
no_interior_splits_memo: Arc<AtomicU32>,
136+
divisibility_memo: DivisibilityMemo,
141137
}
142138

143139
impl ViewedLayoutChildren {
@@ -170,7 +166,7 @@ impl ViewedLayoutChildren {
170166
allow_unknown,
171167
session,
172168
cache,
173-
no_interior_splits_memo: Arc::new(AtomicU32::new(0)),
169+
divisibility_memo: DivisibilityMemo::empty(),
174170
}
175171
}
176172

@@ -296,37 +292,85 @@ impl LayoutChildren for ViewedLayoutChildren {
296292
self.cache.len()
297293
}
298294

299-
fn child_divisibility(&self, idx: usize) -> bool {
295+
fn child_is_divisible(&self, idx: usize) -> bool {
300296
if idx >= self.nchildren() {
301-
return false;
297+
return true;
302298
}
303299
// Resolve the child's layout encoding from the flatbuffer tag alone, without
304-
// deserializing the child layout. Unknown encodings conservatively report interior
305-
// splits so callers fall back to materializing the child.
300+
// deserializing the child layout. Unknown encodings are conservatively divisible so
301+
// callers fall back to materializing the child.
306302
let encoding = self
307303
.flatbuffer()
308304
.children()
309305
.unwrap_or_default()
310306
.get(idx)
311307
.encoding();
312308

313-
const MEMO_VALID: u32 = 1 << 17;
314-
const MEMO_ANSWER: u32 = 1 << 16;
315-
const MEMO_TAG: u32 = 0xFFFF;
316-
let memo = self.no_interior_splits_memo.load(Ordering::Relaxed);
317-
if memo & MEMO_VALID != 0 && memo & MEMO_TAG == u32::from(encoding) {
318-
return memo & MEMO_ANSWER != 0;
309+
if let Some(answer) = self.divisibility_memo.get(encoding) {
310+
return answer;
319311
}
320312

321313
let answer = self
322314
.layout_read_ctx
323315
.resolve(encoding)
324316
.and_then(|encoding_id| self.layouts.get(&encoding_id))
325-
.is_some_and(|encoding| !encoding.registers_interior_splits());
326-
self.no_interior_splits_memo.store(
327-
u32::from(encoding) | MEMO_VALID | if answer { MEMO_ANSWER } else { 0 },
317+
.is_none_or(|encoding| encoding.is_divisible());
318+
self.divisibility_memo.set(encoding, answer);
319+
answer
320+
}
321+
}
322+
323+
/// Single-slot cache for [`ViewedLayoutChildren::child_is_divisible`] answers, keyed by the
324+
/// child's flatbuffer encoding tag and shared across clones of the owning
325+
/// [`ViewedLayoutChildren`].
326+
///
327+
/// Divisibility depends only on the child's layout encoding, and children of one layout node
328+
/// almost always share an encoding — so caching the answer for the last-seen tag turns the
329+
/// per-child read-context resolve and registry lookup into a single atomic load for all but the
330+
/// first child.
331+
///
332+
/// The tag, the answer, and a validity flag are packed into one `AtomicU32` so lookups and
333+
/// updates are each a single atomic operation, with no locking and no torn state between the key
334+
/// and its answer:
335+
///
336+
/// ```text
337+
/// bit: 17 16 15..0
338+
/// valid | answer | encoding tag
339+
/// ```
340+
///
341+
/// `Relaxed` ordering suffices throughout: this is purely a performance cache, and each packed
342+
/// word is internally consistent on its own. The worst a racing `get`/`set` can cause is a
343+
/// redundant recomputation.
344+
#[derive(Clone)]
345+
struct DivisibilityMemo(Arc<AtomicU32>);
346+
347+
impl DivisibilityMemo {
348+
/// Low 16 bits: the flatbuffer encoding tag the cached answer belongs to.
349+
const TAG: u32 = 0xFFFF;
350+
/// Bit 16: the cached answer for the stored tag.
351+
const ANSWER: u32 = 1 << 16;
352+
/// Bit 17: set once the memo holds an entry. Needed because the atomic starts at zero, which
353+
/// would otherwise be indistinguishable from a cached `(tag 0, answer false)` entry.
354+
const VALID: u32 = 1 << 17;
355+
356+
/// Create a memo holding no entry.
357+
fn empty() -> Self {
358+
Self(Arc::new(AtomicU32::new(0)))
359+
}
360+
361+
/// Return the cached answer for `tag`, or `None` if the memo is empty or holds a different
362+
/// tag.
363+
fn get(&self, tag: u16) -> Option<bool> {
364+
let memo = self.0.load(Ordering::Relaxed);
365+
(memo & Self::VALID != 0 && memo & Self::TAG == u32::from(tag))
366+
.then_some(memo & Self::ANSWER != 0)
367+
}
368+
369+
/// Cache `answer` for `tag`, replacing any previous entry.
370+
fn set(&self, tag: u16, answer: bool) {
371+
self.0.store(
372+
u32::from(tag) | Self::VALID | if answer { Self::ANSWER } else { 0 },
328373
Ordering::Relaxed,
329374
);
330-
answer
331375
}
332376
}

vortex-layout/src/encoding.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@ pub trait LayoutVTablePlugin: 'static + Send + Sync + Debug {
6868
build_ctx: &LayoutBuildContext<'_>,
6969
) -> VortexResult<LayoutRef>;
7070

71-
/// Returns `true` if readers of this layout may register natural split boundaries strictly
72-
/// inside their row range (see [`VTable::registers_interior_splits`]).
73-
fn registers_interior_splits(&self) -> bool {
71+
/// Returns `true` if this layout is divisible: its readers may register natural split
72+
/// boundaries strictly inside their row range (see [`VTable::is_divisible`]).
73+
fn is_divisible(&self) -> bool {
7474
true
7575
}
7676
}
@@ -109,8 +109,8 @@ impl<V: VTable> LayoutVTablePlugin for V {
109109
.into_layout())
110110
}
111111

112-
fn registers_interior_splits(&self) -> bool {
113-
VTable::registers_interior_splits(self)
112+
fn is_divisible(&self) -> bool {
113+
VTable::is_divisible(self)
114114
}
115115
}
116116

vortex-layout/src/layout.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -268,9 +268,9 @@ pub trait DynLayout: 'static + Send + Sync + Debug {
268268
ctx: &LayoutReaderContext,
269269
) -> VortexResult<LayoutReaderRef>;
270270

271-
/// Returns `true` if readers of this layout may register natural split boundaries strictly
272-
/// inside their row range (see [`crate::VTable::registers_interior_splits`]).
273-
fn dyn_registers_interior_splits(&self) -> bool {
271+
/// Returns `true` if this layout is divisible: its readers may register natural split
272+
/// boundaries strictly inside their row range (see [`crate::VTable::is_divisible`]).
273+
fn dyn_is_divisible(&self) -> bool {
274274
true
275275
}
276276
}
@@ -330,8 +330,8 @@ impl<V: VTable> DynLayout for Layout<V> {
330330
Layout::new_reader(self, name, segment_source, session, ctx)
331331
}
332332

333-
fn dyn_registers_interior_splits(&self) -> bool {
334-
self.vtable().registers_interior_splits()
333+
fn dyn_is_divisible(&self) -> bool {
334+
self.vtable().is_divisible()
335335
}
336336
}
337337

vortex-layout/src/layouts/chunked/reader.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ impl ChunkedReader {
108108
self.lazy_children.get(idx)
109109
}
110110

111-
/// Classify which chunks are known to register no interior splits, without materializing
112-
/// any chunk layouts or readers.
111+
/// Classify which chunks are known to be indivisible, without materializing any chunk
112+
/// layouts or readers.
113113
fn chunk_skips(&self) -> &ChunkSkips {
114114
self.chunk_skips.get_or_init(|| {
115115
let children = self.layout.children();
@@ -118,7 +118,7 @@ impl ChunkedReader {
118118
return ChunkSkips::None;
119119
}
120120
let skips = (0..nchildren)
121-
.map(|idx| children.child_divisibility(idx))
121+
.map(|idx| !children.child_is_divisible(idx))
122122
.collect::<Box<[bool]>>();
123123
if skips.iter().all(|&skip| skip) {
124124
ChunkSkips::All

vortex-layout/src/layouts/flat/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ impl VTable for Flat {
6565
*ID
6666
}
6767

68-
/// Flat readers only ever register the end of the requested range, so split collection can
69-
/// skip materializing flat children.
70-
fn registers_interior_splits(&self) -> bool {
68+
/// Flat readers only ever register the end of the requested range, so flat layouts are
69+
/// indivisible and split collection can skip materializing flat children.
70+
fn is_divisible(&self) -> bool {
7171
false
7272
}
7373

vortex-layout/src/vtable.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,13 @@ pub trait VTable: 'static + Clone + Send + Sync + Debug {
114114
ctx: &LayoutReaderContext,
115115
) -> VortexResult<LayoutReaderRef>;
116116

117-
/// Returns `true` if readers of this layout may register natural split boundaries strictly
118-
/// inside their row range (see [`crate::LayoutReader::register_splits`]).
117+
/// Returns `true` if this layout is divisible: its readers may register natural split
118+
/// boundaries strictly inside their row range (see [`crate::LayoutReader::register_splits`]).
119119
///
120-
/// Layouts whose readers only ever push the end of the requested range — like flat — return
121-
/// `false`, which lets parent layouts skip materializing the child entirely during split
122-
/// collection.
123-
fn registers_interior_splits(&self) -> bool {
120+
/// Indivisible layouts — like flat, whose readers only ever push the end of the requested
121+
/// range — return `false`, which lets parent layouts skip materializing the child entirely
122+
/// during split collection.
123+
fn is_divisible(&self) -> bool {
124124
true
125125
}
126126
}

0 commit comments

Comments
 (0)