alamb commented on code in PR #11237:
URL: https://github.com/apache/arrow-rs/pull/11237#discussion_r4210684965


##########
parquet/src/util/push_buffers.rs:
##########
@@ -132,20 +147,44 @@ impl PushBuffers {
                 range.end
             ));
         }
-        self.ranges.push(range);
-        self.buffers.push(buffer);
+        // Insert after all buffers that start at or before `range.start`.
+        // Thus, ranges pushed in file order go at the end.
+        let idx = self.ranges.partition_point(|r| r.start <= range.start);
+        self.max_len = self.max_len.max(expected);
+        self.ranges.insert(idx, range);
+        self.buffers.insert(idx, buffer);
         Ok(())
     }
 
     /// Returns true if the Buffers contains data for the given range
     pub(crate) fn has_range(&self, range: &Range<u64>) -> bool {
-        self.ranges
-            .iter()
-            .any(|r| r.start <= range.start && r.end >= range.end)
+        self.find(range.start, range.end).is_some()
     }
 
-    fn iter(&self) -> impl Iterator<Item = (&Range<u64>, &Bytes)> {
-        self.ranges.iter().zip(self.buffers.iter())
+    /// Returns the index of a buffer that contains all bytes of `start..end`,
+    /// if any.
+    fn find(&self, start: u64, end: u64) -> Option<usize> {
+        // Ranges can overlap. Thus, the buffer that starts nearest to `start`
+        // is not always the buffer that contains `start..end`:
+        //
+        //   buffers:  0..100  ├────────────────────────────────┤
+        //             50..60                   ├───┤
+        //             55..58                     ├┤
+        //   find:     55..90                     ├──────────┤  only 0..100 
contains it
+        //
+        // Scan back from the last buffer that starts at or before `start`.
+        // Stop at the first buffer that starts more than `max_len` bytes
+        // before `end`: it and all buffers before it end before `end`. The

Review Comment:
   I am confused -- why do we need max_bytes here? Why don't you just look for 
the first buffer that has at least `end-start` bytes?



##########
parquet/src/util/push_buffers.rs:
##########
@@ -41,16 +41,30 @@ use std::ops::Range;
 ///
 /// Thus, the implementation defers to the caller to coalesce subsequent 
requests
 /// if desired.
+///
+/// # Ordering
+///
+/// The buffers are sorted by the start of their range. Thus, lookups use a
+/// binary search, not a scan of all buffers. Pushed ranges can overlap.
 #[derive(Debug, Clone, Default)]
 pub struct PushBuffers {
     /// the virtual "offset" of this buffers (added to any request)
     offset: u64,
     /// The total length of the file being decoded
     file_len: u64,
-    /// The ranges of data that are available for decoding (not adjusted for 
offset)
+    /// The ranges of data that are available for decoding (not adjusted for
+    /// offset), sorted by `start`
     ranges: Vec<Range<u64>>,
-    /// The buffers of data that can be used to decode the Parquet file
+    /// The buffers of data that can be used to decode the Parquet file, in the
+    /// same order as `ranges`
     buffers: Vec<Bytes>,
+    /// The maximum length of the ranges in `ranges`. A buffer that starts

Review Comment:
   I found this comment really hard to understand. COuld you cut this down / 
add an example of what max_len buys and why it is valuable 
   
   Since we know the range of each buffer, why do we need to remember the 
maximum length across all buffers?



##########
parquet/src/util/push_buffers.rs:
##########
@@ -275,6 +320,149 @@ mod tests {
         assert!(!buffers.has_range(&(10..20)));
     }
 
+    /// The bytes of a fake file: byte `i` is `i % 251`, so any slice of it is
+    /// easy to build and to check.
+    fn file_bytes(range: Range<u64>) -> Bytes {
+        range.map(|i| (i % 251) as u8).collect::<Vec<u8>>().into()
+    }
+
+    fn push(buffers: &mut PushBuffers, range: Range<u64>) {
+        buffers
+            .push_range(range.clone(), file_bytes(range))
+            .unwrap();
+    }
+
+    #[track_caller]
+    fn assert_invariants(buffers: &PushBuffers) {
+        assert_eq!(buffers.ranges.len(), buffers.buffers.len());
+        assert!(
+            buffers.ranges.is_sorted_by_key(|r| r.start),
+            "not sorted: {:?}",
+            buffers.ranges
+        );
+        for (range, buffer) in buffers.ranges.iter().zip(&buffers.buffers) {
+            assert_eq!(*buffer, file_bytes(range.clone()));
+            assert!(range.end - range.start <= buffers.max_len);
+        }
+        let max_len = buffers.ranges.iter().map(|r| r.end - r.start).max();
+        assert_eq!(buffers.max_len, max_len.unwrap_or(0));
+    }
+
+    #[test]
+    fn out_of_order_pushes_are_sorted() {

Review Comment:
   Is this tst really necessary? Maybe you could change one of the other tests 
to push the ranges in some different order  and then the call to 
`assert_invaraints` will ensure the data is sorted



##########
parquet/src/util/push_buffers.rs:
##########
@@ -275,6 +320,149 @@ mod tests {
         assert!(!buffers.has_range(&(10..20)));
     }
 
+    /// The bytes of a fake file: byte `i` is `i % 251`, so any slice of it is
+    /// easy to build and to check.
+    fn file_bytes(range: Range<u64>) -> Bytes {
+        range.map(|i| (i % 251) as u8).collect::<Vec<u8>>().into()
+    }
+
+    fn push(buffers: &mut PushBuffers, range: Range<u64>) {
+        buffers
+            .push_range(range.clone(), file_bytes(range))
+            .unwrap();
+    }
+
+    #[track_caller]
+    fn assert_invariants(buffers: &PushBuffers) {

Review Comment:
   This might be a nice `#test` method on the `PushBuffers` structure itself as 
it puts the invariant checks closer to the logic that uses them



##########
parquet/src/util/push_buffers.rs:
##########
@@ -132,20 +147,44 @@ impl PushBuffers {
                 range.end
             ));
         }
-        self.ranges.push(range);
-        self.buffers.push(buffer);
+        // Insert after all buffers that start at or before `range.start`.
+        // Thus, ranges pushed in file order go at the end.
+        let idx = self.ranges.partition_point(|r| r.start <= range.start);
+        self.max_len = self.max_len.max(expected);
+        self.ranges.insert(idx, range);
+        self.buffers.insert(idx, buffer);
         Ok(())
     }
 
     /// Returns true if the Buffers contains data for the given range
     pub(crate) fn has_range(&self, range: &Range<u64>) -> bool {
-        self.ranges
-            .iter()
-            .any(|r| r.start <= range.start && r.end >= range.end)
+        self.find(range.start, range.end).is_some()
     }
 
-    fn iter(&self) -> impl Iterator<Item = (&Range<u64>, &Bytes)> {
-        self.ranges.iter().zip(self.buffers.iter())
+    /// Returns the index of a buffer that contains all bytes of `start..end`,
+    /// if any.
+    fn find(&self, start: u64, end: u64) -> Option<usize> {
+        // Ranges can overlap. Thus, the buffer that starts nearest to `start`
+        // is not always the buffer that contains `start..end`:
+        //
+        //   buffers:  0..100  ├────────────────────────────────┤
+        //             50..60                   ├───┤
+        //             55..58                     ├┤
+        //   find:     55..90                     ├──────────┤  only 0..100 
contains it
+        //
+        // Scan back from the last buffer that starts at or before `start`.
+        // Stop at the first buffer that starts more than `max_len` bytes
+        // before `end`: it and all buffers before it end before `end`. The

Review Comment:
   I also found this coment super confusing



##########
parquet/src/util/push_buffers.rs:
##########
@@ -275,6 +320,149 @@ mod tests {
         assert!(!buffers.has_range(&(10..20)));
     }
 
+    /// The bytes of a fake file: byte `i` is `i % 251`, so any slice of it is
+    /// easy to build and to check.
+    fn file_bytes(range: Range<u64>) -> Bytes {
+        range.map(|i| (i % 251) as u8).collect::<Vec<u8>>().into()
+    }
+
+    fn push(buffers: &mut PushBuffers, range: Range<u64>) {
+        buffers
+            .push_range(range.clone(), file_bytes(range))
+            .unwrap();
+    }
+
+    #[track_caller]
+    fn assert_invariants(buffers: &PushBuffers) {
+        assert_eq!(buffers.ranges.len(), buffers.buffers.len());
+        assert!(
+            buffers.ranges.is_sorted_by_key(|r| r.start),
+            "not sorted: {:?}",
+            buffers.ranges
+        );
+        for (range, buffer) in buffers.ranges.iter().zip(&buffers.buffers) {
+            assert_eq!(*buffer, file_bytes(range.clone()));
+            assert!(range.end - range.start <= buffers.max_len);
+        }
+        let max_len = buffers.ranges.iter().map(|r| r.end - r.start).max();
+        assert_eq!(buffers.max_len, max_len.unwrap_or(0));
+    }
+
+    #[test]
+    fn out_of_order_pushes_are_sorted() {
+        let mut buffers = PushBuffers::new(1000);
+        for range in [50..60, 10..20, 30..40, 0..5, 90..100, 10..15] {
+            push(&mut buffers, range);
+        }
+        assert_invariants(&buffers);
+        assert_eq!(
+            buffers.ranges,
+            vec![0..5, 10..20, 10..15, 30..40, 50..60, 90..100]
+        );
+        assert!(buffers.has_range(&(12..18)));
+        assert!(buffers.has_range(&(30..40)));
+        assert!(!buffers.has_range(&(18..22)));
+        assert!(!buffers.has_range(&(60..61)));
+        assert_eq!(buffers.get_bytes(52, 4).unwrap(), file_bytes(52..56));
+        assert!(matches!(
+            buffers.get_bytes(40, 20),
+            Err(ParquetError::NeedMoreDataRange(r)) if r == (40..60)
+        ));
+    }
+
+    #[test]
+    fn overlapping_pushes_find_the_containing_buffer() {
+        let mut buffers = PushBuffers::new(1000);
+        // Small buffers inside a large one start closer to most offsets.
+        for range in [0..100, 10..20, 50..60, 55..58] {
+            push(&mut buffers, range);
+        }
+        assert_invariants(&buffers);
+        // 55..90 starts in 50..60 and 55..58, but only 0..100 contains it.
+        assert_eq!(buffers.get_bytes(55, 35).unwrap(), file_bytes(55..90));
+        assert!(buffers.has_range(&(0..100)));
+        assert!(buffers.has_range(&(99..100)));
+        assert!(!buffers.has_range(&(99..101)));
+
+        // `Read` finds the same buffer.
+        let mut reader = buffers.get_read(56).unwrap();
+        let mut out = [0u8; 30];
+        std::io::Read::read_exact(&mut reader, &mut out).unwrap();
+        assert_eq!(&out[..], &file_bytes(56..86)[..]);
+    }
+
+    #[test]
+    #[cfg(feature = "arrow")]
+    fn clear_ranges_drops_exact_matches_only() {
+        let mut buffers = PushBuffers::new(1000);
+        for range in [10..20, 0..30, 10..15, 40..50] {
+            push(&mut buffers, range);
+        }
+        buffers.clear_ranges(&[40..50, 10..15, 5..30]);
+        assert_invariants(&buffers);
+        assert_eq!(buffers.ranges, vec![0..30, 10..20]);
+    }
+
+    /// Random pushes, clears and lookups, compared with a list that is
+    /// scanned in full for each lookup.
+    #[test]
+    #[cfg(feature = "arrow")]
+    fn matches_a_linear_scan() {

Review Comment:
   Perhaps using the word `fuzz` in this  test name would help people 
understand it more easily



##########
parquet/src/util/push_buffers.rs:
##########
@@ -132,20 +147,44 @@ impl PushBuffers {
                 range.end
             ));
         }
-        self.ranges.push(range);
-        self.buffers.push(buffer);
+        // Insert after all buffers that start at or before `range.start`.
+        // Thus, ranges pushed in file order go at the end.
+        let idx = self.ranges.partition_point(|r| r.start <= range.start);
+        self.max_len = self.max_len.max(expected);
+        self.ranges.insert(idx, range);
+        self.buffers.insert(idx, buffer);
         Ok(())
     }
 
     /// Returns true if the Buffers contains data for the given range
     pub(crate) fn has_range(&self, range: &Range<u64>) -> bool {
-        self.ranges
-            .iter()
-            .any(|r| r.start <= range.start && r.end >= range.end)
+        self.find(range.start, range.end).is_some()
     }
 
-    fn iter(&self) -> impl Iterator<Item = (&Range<u64>, &Bytes)> {
-        self.ranges.iter().zip(self.buffers.iter())
+    /// Returns the index of a buffer that contains all bytes of `start..end`,
+    /// if any.
+    fn find(&self, start: u64, end: u64) -> Option<usize> {
+        // Ranges can overlap. Thus, the buffer that starts nearest to `start`
+        // is not always the buffer that contains `start..end`:
+        //
+        //   buffers:  0..100  ├────────────────────────────────┤
+        //             50..60                   ├───┤
+        //             55..58                     ├┤
+        //   find:     55..90                     ├──────────┤  only 0..100 
contains it
+        //
+        // Scan back from the last buffer that starts at or before `start`.
+        // Stop at the first buffer that starts more than `max_len` bytes
+        // before `end`: it and all buffers before it end before `end`. The

Review Comment:
   > Thus, the buffer that starts nearest to `start`
   >         // is not always the buffer that contains `start..end`:
   
   I think the key insight I was missing here is that `max_len` is an 
optimization for the common case of non overlapping buffers (what the decoder 
will ask for) to avoid having to check all earlier buffers in the queue for 
overlap
   
   Maybe the example could show the common case with non overlapping buffers 
and explain that we want to stop early once we get before the requested range. 
However, with overlap in general we would have to check all of them (and then 
add the example of one giant range)
   



##########
parquet/src/util/push_buffers.rs:
##########
@@ -41,16 +41,30 @@ use std::ops::Range;
 ///
 /// Thus, the implementation defers to the caller to coalesce subsequent 
requests
 /// if desired.
+///
+/// # Ordering
+///
+/// The buffers are sorted by the start of their range. Thus, lookups use a
+/// binary search, not a scan of all buffers. Pushed ranges can overlap.
 #[derive(Debug, Clone, Default)]
 pub struct PushBuffers {
     /// the virtual "offset" of this buffers (added to any request)
     offset: u64,
     /// The total length of the file being decoded
     file_len: u64,
-    /// The ranges of data that are available for decoding (not adjusted for 
offset)
+    /// The ranges of data that are available for decoding (not adjusted for
+    /// offset), sorted by `start`
     ranges: Vec<Range<u64>>,
-    /// The buffers of data that can be used to decode the Parquet file
+    /// The buffers of data that can be used to decode the Parquet file, in the
+    /// same order as `ranges`
     buffers: Vec<Bytes>,
+    /// The maximum length of the ranges in `ranges`. A buffer that starts

Review Comment:
   I think the detail I was missing on first read is that this is an 
optimization for `find` of the common case where there are relatively equal 
sized non- overlapping buffers
   
   The comment as now sort of describes implementation details of how `find` 
work 🤯 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to