1use std::ops::Range;
2#[cfg(target_os = "linux")]
3use std::os::fd::AsRawFd;
4use std::path::Path;
5use std::sync::{Arc, LazyLock};
6
7use dio_align::DioAlign;
8use futures::{StreamExt, TryStreamExt};
9use polars_buffer::Buffer;
10use polars_config::FileAdvice;
11use polars_core::prelude::PlHashMap;
12use polars_core::runtime::ASYNC;
13use polars_error::{PolarsResult, feature_gated, polars_bail, polars_err};
14use polars_utils::aliases::InitHashMaps;
15use polars_utils::io::_limit_path_len_io_err;
16use polars_utils::mmap::MMapSemaphore;
17use polars_utils::pl_path::PlRefPath;
18use tokio::sync::Semaphore;
19
20use crate::cloud::concurrency_config::{ConcurrencyStrategy, FetchConfig};
21use crate::cloud::options::CloudOptions;
22#[cfg(feature = "cloud")]
23use crate::cloud::{
24 CloudLocation, ObjectStorePath, PolarsObjectStore, build_object_store, object_path_from_str,
25};
26use crate::metrics::{IOMetrics, OptIOMetrics};
27
28pub mod dio_align;
29#[cfg(target_os = "linux")]
30mod direct_io;
31#[allow(async_fn_in_trait)]
32pub trait ByteSource: Send + Sync {
33 async fn get_size(&self) -> PolarsResult<usize>;
34 async fn get_suffix(&self, n: usize) -> PolarsResult<(Buffer<u8>, usize)>;
37 async fn get_prefix(&self, n: usize) -> PolarsResult<(Buffer<u8>, usize)>;
40 async fn get_range(&self, range: Range<usize>) -> PolarsResult<Buffer<u8>>;
43 async fn get_ranges(
45 &self,
46 ranges: &mut [Range<usize>],
47 ) -> PolarsResult<PlHashMap<usize, Buffer<u8>>>;
48}
49
50pub struct BufferByteSource(pub Buffer<u8>);
52
53impl BufferByteSource {
54 async fn try_new_mmap_from_path(
55 path: &Path,
56 _cloud_options: Option<&CloudOptions>,
57 ) -> PolarsResult<Self> {
58 let file = Arc::new(
59 tokio::fs::File::open(path)
60 .await
61 .map_err(|err| _limit_path_len_io_err(path, err))?
62 .into_std()
63 .await,
64 );
65
66 Ok(Self(Buffer::from_owner(MMapSemaphore::new_from_file(
67 &file,
68 )?)))
69 }
70}
71
72impl ByteSource for BufferByteSource {
73 async fn get_size(&self) -> PolarsResult<usize> {
74 Ok(self.0.as_ref().len())
75 }
76
77 async fn get_suffix(&self, n: usize) -> PolarsResult<(Buffer<u8>, usize)> {
78 let len = self.0.as_ref().len();
79 Ok((self.0.clone().sliced(len.saturating_sub(n)..len), len))
80 }
81
82 async fn get_prefix(&self, n: usize) -> PolarsResult<(Buffer<u8>, usize)> {
83 let len = self.0.as_ref().len();
84 Ok((self.0.clone().sliced(0..n.min(len)), len))
85 }
86
87 async fn get_range(&self, range: Range<usize>) -> PolarsResult<Buffer<u8>> {
88 let out = self.0.clone().sliced(range);
89 Ok(out)
90 }
91
92 async fn get_ranges(
93 &self,
94 ranges: &mut [Range<usize>],
95 ) -> PolarsResult<PlHashMap<usize, Buffer<u8>>> {
96 Ok(ranges
97 .iter()
98 .map(|x| (x.start, self.0.clone().sliced(x.clone())))
99 .collect())
100 }
101}
102
103pub struct FileByteSource {
105 file: Arc<std::fs::File>,
106 o_direct_align: Option<DioAlign>,
108 concurrency: usize,
110 permits: Arc<Semaphore>,
111 size: u64,
113 io_metrics: OptIOMetrics,
114}
115
116pub fn global_read_permits() -> Arc<Semaphore> {
118 static PERMITS: LazyLock<Arc<Semaphore>> = LazyLock::new(|| {
119 Arc::new(Semaphore::new(
120 polars_config::config().file_read_concurrency().max(1) as usize,
121 ))
122 });
123
124 PERMITS.clone()
125}
126
127#[derive(Clone)]
128pub struct FileReadContext {
129 pub enable_o_direct: bool,
130 pub concurrency: usize,
131 pub permits: Arc<Semaphore>,
132 pub advice: FileAdvice,
134}
135
136impl FileReadContext {
137 pub fn from_config(name: &str) -> Self {
138 let cfg = polars_config::config();
139 let enable_o_direct = cfg.direct_io();
140 let concurrency = cfg.file_read_concurrency().max(1) as usize;
141
142 let fadv = cfg.file_posix_fadv();
145
146 if cfg.verbose() {
147 eprintln!(
148 "[{name}]: file read_context as configured: \
149 read_concurrency: {concurrency}, \
150 posix_fadv: {fadv}, \
151 o_direct: {enable_o_direct}"
152 );
153 }
154
155 Self {
156 enable_o_direct,
157 concurrency,
158 permits: global_read_permits(),
159 advice: fadv,
160 }
161 }
162}
163
164impl std::fmt::Debug for FileReadContext {
165 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166 f.debug_struct("FileReadContext")
167 .field("available_permits", &self.permits.available_permits())
168 .field("enable_odirect", &self.enable_o_direct)
169 .field("concurrency", &self.concurrency)
170 .field("advice", &self.advice)
171 .finish()
172 }
173}
174
175#[cfg(target_os = "linux")]
176fn _fadvise(file: &std::fs::File, advice: FileAdvice) {
177 let advice = match advice {
178 FileAdvice::Normal => return,
180 FileAdvice::Sequential => libc::POSIX_FADV_SEQUENTIAL,
181 FileAdvice::Random => libc::POSIX_FADV_RANDOM,
182 FileAdvice::WillNeed => libc::POSIX_FADV_WILLNEED,
183 };
184
185 unsafe {
186 libc::posix_fadvise(file.as_raw_fd(), 0, 0, advice);
187 }
188}
189
190#[cfg(all(unix, not(feature = "nightly")))]
191fn pread_exact(file: &std::fs::File, buf: &mut [u8], offset: u64) -> std::io::Result<()> {
192 use std::os::unix::fs::FileExt;
193 file.read_exact_at(buf, offset)
194}
195
196#[cfg(all(windows, not(feature = "nightly")))]
197fn pread_exact(file: &std::fs::File, buf: &mut [u8], offset: u64) -> std::io::Result<()> {
198 use std::os::windows::fs::FileExt;
199 let mut filled = 0;
200 while filled < buf.len() {
201 match file.seek_read(&mut buf[filled..], offset + filled as u64)? {
202 0 => return Err(std::io::ErrorKind::UnexpectedEof.into()),
203 n => filled += n,
204 }
205 }
206 Ok(())
207}
208
209#[cfg(all(feature = "nightly", unix))]
212fn pread_exact_uninit(
213 file: &std::fs::File,
214 buf: std::io::BorrowedCursor<'_, u8>,
215 offset: u64,
216) -> std::io::Result<()> {
217 use std::os::unix::fs::FileExt;
218 file.read_buf_exact_at(buf, offset)
219}
220
221#[cfg(all(feature = "nightly", windows))]
222fn pread_exact_uninit(
223 file: &std::fs::File,
224 mut buf: std::io::BorrowedCursor<'_, u8>,
225 mut offset: u64,
226) -> std::io::Result<()> {
227 use std::os::windows::fs::FileExt;
228 while buf.capacity() > 0 {
230 let written = buf.written();
231 file.seek_read_buf(buf.reborrow(), offset)?;
232 match buf.written() - written {
233 0 => return Err(std::io::ErrorKind::UnexpectedEof.into()),
234 n => offset += n as u64,
235 }
236 }
237 Ok(())
238}
239
240#[cfg(feature = "nightly")]
246fn read_buffered(file: &std::fs::File, offset: u64, len: usize) -> PolarsResult<Buffer<u8>> {
247 let mut v: Vec<u8> = Vec::with_capacity(len);
248
249 let filled = {
250 let mut buf = std::io::BorrowedBuf::from(&mut v.spare_capacity_mut()[..len]);
251 pread_exact_uninit(file, buf.unfilled(), offset)?;
252 buf.len()
253 };
254
255 debug_assert_eq!(filled, len);
256 unsafe { v.set_len(filled) };
259
260 Ok(Buffer::from(v))
261}
262
263#[cfg(not(feature = "nightly"))]
264fn read_buffered(file: &std::fs::File, offset: u64, len: usize) -> PolarsResult<Buffer<u8>> {
265 let mut buf = vec![0u8; len];
266 pread_exact(file, &mut buf, offset)?;
267 Ok(Buffer::from(buf))
268}
269
270#[cfg(target_os = "linux")]
273fn read_at(
274 file: &std::fs::File,
275 align: Option<DioAlign>,
276 offset: u64,
277 len: usize,
278 size: u64,
279) -> PolarsResult<Buffer<u8>> {
280 match align {
281 Some(align) => direct_io::read_aligned(file, align, offset, len, size),
282 None => read_buffered(file, offset, len),
283 }
284}
285
286#[cfg(not(target_os = "linux"))]
287fn read_at(
288 file: &std::fs::File,
289 _align: Option<DioAlign>,
290 offset: u64,
291 len: usize,
292 _size: u64,
293) -> PolarsResult<Buffer<u8>> {
294 read_buffered(file, offset, len)
295}
296
297impl FileByteSource {
298 async fn try_new_from_path(
299 path: PlRefPath,
300 read_context: FileReadContext,
301 io_metrics: Option<Arc<IOMetrics>>,
302 ) -> PolarsResult<Self> {
303 ASYNC
305 .spawn_blocking(move || Self::open_blocking(path, read_context, io_metrics))
306 .await
307 .expect("blocking task panicked")
308 }
309
310 fn open_blocking(
311 path: PlRefPath,
312 read_context: FileReadContext,
313 io_metrics: Option<Arc<IOMetrics>>,
314 ) -> PolarsResult<Self> {
315 let path = path.as_std_path();
316 let enable_o_direct = read_context.enable_o_direct;
317
318 let file = {
319 #[cfg(target_os = "linux")]
320 let f = if enable_o_direct {
321 direct_io::open_o_direct(path).or_else(|e| match e.raw_os_error() {
322 Some(libc::EINVAL) => std::fs::File::open(path),
324 _ => Err(e),
325 })
326 } else {
327 std::fs::File::open(path)
328 };
329 #[cfg(not(target_os = "linux"))]
330 let f = std::fs::File::open(path);
331
332 f.map_err(|e| _limit_path_len_io_err(path, e))?
333 };
334
335 let file = Arc::new(file);
336
337 #[cfg(target_os = "linux")]
338 let o_direct_align =
339 direct_io::probe_or_disable(&file).map_err(|e| _limit_path_len_io_err(path, e))?;
340
341 #[cfg(not(target_os = "linux"))]
342 let o_direct_align: Option<DioAlign> = None;
343
344 #[cfg(target_os = "linux")]
345 if o_direct_align.is_none() {
346 _fadvise(&file, read_context.advice);
348 }
349
350 let concurrency = read_context.concurrency;
351 let permits = read_context.permits;
352
353 let size = file
354 .metadata()
355 .map_err(|e| _limit_path_len_io_err(path, e))?
356 .len();
357
358 if polars_config::config().verbose() {
359 let name = if polars_config::config().verbose_sensitive() {
360 path.display().to_string()
361 } else {
362 "<file>".to_string()
363 };
364 match (enable_o_direct, o_direct_align) {
365 (true, Some(a)) => eprintln!(
366 "[FileByteSource]: {name}: direct IO active, \
367 alignment offset: {}, memory: {}",
368 a.offset, a.memory
369 ),
370 (true, None) => eprintln!(
371 "[FileByteSource]: {name}: direct IO requested but not active, \
372 using buffered reads"
373 ),
374 (false, _) => {},
375 }
376 }
377
378 Ok(FileByteSource {
379 file,
380 o_direct_align,
381 concurrency,
382 permits,
383 size,
384 io_metrics: OptIOMetrics(io_metrics),
385 })
386 }
387
388 pub fn try_new_from_std(
389 file: std::fs::File,
390 read_context: FileReadContext,
391 io_metrics: Option<Arc<IOMetrics>>,
392 ) -> PolarsResult<Self> {
393 let size = file.metadata()?.len();
394
395 #[cfg(target_os = "linux")]
396 let o_direct_align = direct_io::probe_or_disable(&file)?;
397
398 #[cfg(not(target_os = "linux"))]
399 let o_direct_align: Option<DioAlign> = None;
400
401 #[cfg(target_os = "linux")]
402 if o_direct_align.is_none() {
403 _fadvise(&file, read_context.advice);
404 }
405
406 let concurrency = read_context.concurrency;
407 let permits = read_context.permits;
408
409 let enable_o_direct = read_context.enable_o_direct;
411
412 if polars_config::config().verbose() {
413 match (enable_o_direct, o_direct_align) {
414 (true, Some(a)) => eprintln!(
415 "[FileByteSource]: direct IO active, alignment offset: {}, memory: {}",
416 a.offset, a.memory
417 ),
418 (true, None) => eprintln!(
419 "[FileByteSource]: direct IO requested but not active, using buffered reads",
420 ),
421 (false, _) => {},
422 }
423 }
424
425 Ok(Self {
426 file: Arc::new(file),
427 o_direct_align,
428 concurrency,
429 permits,
430 size,
431 io_metrics: OptIOMetrics(io_metrics),
432 })
433 }
434
435 pub fn set_io_metrics(&mut self, io_metrics: Option<Arc<IOMetrics>>) -> &mut Self {
436 self.io_metrics = OptIOMetrics(io_metrics);
437 self
438 }
439
440 pub fn io_metrics(&self) -> &OptIOMetrics {
441 &self.io_metrics
442 }
443
444 pub fn is_cached(&self, ranges: &[Range<usize>]) -> bool {
447 #[cfg(all(
448 target_os = "linux",
449 any(target_arch = "x86_64", target_arch = "aarch64")
450 ))]
451 if self.o_direct_align.is_none() {
452 return cachestat::all_cached(&self.file, ranges);
453 }
454
455 let _ = ranges;
456 false
457 }
458
459 pub fn read_blocking(&self, range: Range<usize>) -> PolarsResult<Buffer<u8>> {
461 assert!(range.end as u64 <= self.size);
462
463 self.io_metrics()
464 .record_io_read_blocking(range.len() as u64, || {
465 read_at(
466 &self.file,
467 self.o_direct_align,
468 range.start as u64,
469 range.len(),
470 self.size,
471 )
472 })
473 }
474}
475
476#[cfg(all(
478 target_os = "linux",
479 any(target_arch = "x86_64", target_arch = "aarch64")
480))]
481mod cachestat {
482 use std::ops::Range;
483 use std::os::fd::AsRawFd;
484 use std::sync::atomic::{AtomicBool, Ordering};
485
486 use polars_utils::mem::PAGE_SIZE;
487
488 #[repr(C)]
489 struct CachestatRange {
490 off: u64,
491 len: u64,
492 }
493
494 #[repr(C)]
495 #[derive(Default)]
496 struct Cachestat {
497 nr_cache: u64,
498 nr_dirty: u64,
499 nr_writeback: u64,
500 nr_evicted: u64,
501 nr_recently_evicted: u64,
502 }
503
504 const SYS_CACHESTAT: libc::c_long = 451;
506
507 static UNSUPPORTED: AtomicBool = AtomicBool::new(false);
509
510 pub(super) fn all_cached(file: &std::fs::File, ranges: &[Range<usize>]) -> bool {
512 if UNSUPPORTED.load(Ordering::Relaxed) {
513 return false;
514 }
515
516 let mut spans: Vec<Range<usize>> =
519 ranges.iter().filter(|r| !r.is_empty()).cloned().collect();
520 spans.sort_unstable_by_key(|r| r.start);
521 spans.dedup_by(|next, cur| {
522 let merge = next.start <= cur.end;
523 if merge {
524 cur.end = cur.end.max(next.end);
525 }
526 merge
527 });
528
529 let page = *PAGE_SIZE;
530 for r in &spans {
531 let mut range = CachestatRange {
532 off: r.start as u64,
533 len: r.len() as u64,
534 };
535 let mut cs = Cachestat::default();
536 let ret = unsafe {
538 libc::syscall(
539 SYS_CACHESTAT,
540 file.as_raw_fd() as libc::c_long,
541 &mut range as *mut CachestatRange,
542 &mut cs as *mut Cachestat,
543 0 as libc::c_long,
544 )
545 };
546 if ret != 0 {
547 if let Some(libc::ENOSYS | libc::EPERM) =
550 std::io::Error::last_os_error().raw_os_error()
551 {
552 UNSUPPORTED.store(true, Ordering::Relaxed);
553 }
554 return false;
555 }
556 let pages = (r.end - 1) / page - r.start / page + 1;
558 if cs.nr_cache < pages as u64 {
559 return false;
560 }
561 }
562 true
563 }
564}
565
566impl ByteSource for FileByteSource {
567 async fn get_size(&self) -> PolarsResult<usize> {
568 usize::try_from(self.size)
569 .map_err(|_| polars_err!(ComputeError: "file size {} does not fit in usize", self.size))
570 }
571
572 async fn get_suffix(&self, n: usize) -> PolarsResult<(Buffer<u8>, usize)> {
573 let size = self.get_size().await?;
574 let bytes = self.get_range(size.saturating_sub(n)..size).await?;
575 Ok((bytes, size))
576 }
577
578 async fn get_prefix(&self, n: usize) -> PolarsResult<(Buffer<u8>, usize)> {
579 let size = self.get_size().await?;
580 let bytes = self.get_range(0..n.min(size)).await?;
581 Ok((bytes, size))
582 }
583
584 async fn get_range(&self, range: Range<usize>) -> PolarsResult<Buffer<u8>> {
585 assert!(range.end as u64 <= self.size);
586
587 let file = self.file.clone();
588 let offset = range.start as u64;
589 let len = range.len();
590 let size = self.size;
591 let o_direct = self.o_direct_align;
592
593 let permit = self.permits.clone().acquire_owned().await.unwrap();
594
595 self.io_metrics()
596 .record_io_read(len as u64, async move {
597 ASYNC
598 .spawn_blocking(move || {
599 let _permit = permit;
600 read_at(&file, o_direct, offset, len, size)
601 })
602 .await
603 })
604 .await
605 .expect("blocking task panicked")
606 }
607
608 async fn get_ranges(
609 &self,
610 ranges: &mut [Range<usize>],
611 ) -> PolarsResult<PlHashMap<usize, Buffer<u8>>> {
612 if let [range] = ranges {
613 let mut out = PlHashMap::with_capacity(1);
614 out.insert(range.start, self.get_range(range.clone()).await?);
615 return Ok(out);
616 }
617
618 ranges.sort_unstable_by_key(|r| r.start);
619
620 let mut spans: Vec<Range<usize>> = Vec::with_capacity(ranges.len());
621
622 const MAX_SPAN: usize = 8 << 20;
624 const MAX_GAP: usize = 4096;
626
627 for r in ranges.iter() {
628 match spans.last_mut() {
629 Some(last)
630 if r.start.saturating_sub(last.end) <= MAX_GAP
631 && r.end.saturating_sub(last.start) <= MAX_SPAN =>
632 {
633 last.end = last.end.max(r.end)
634 },
635 _ => spans.push(r.clone()),
636 }
637 }
638
639 let mut fetched: Vec<(Range<usize>, Buffer<u8>)> = futures::stream::iter(spans)
640 .map(|span| async move {
641 let buf = self.get_range(span.clone()).await?;
642 PolarsResult::Ok((span, buf))
643 })
644 .buffer_unordered(self.concurrency)
645 .try_collect()
646 .await?;
647
648 fetched.sort_unstable_by_key(|(s, _)| s.start);
650
651 if let Some(w) = ranges.windows(2).find(|w| w[0].start == w[1].start) {
652 polars_bail!(
653 ComputeError:
654 "duplicate range start {} in read request ({:?} and {:?})",
655 w[0].start, w[0], w[1]
656 );
657 }
658
659 let mut out = PlHashMap::with_capacity(ranges.len());
660 for r in ranges.iter() {
661 if r.is_empty() {
663 out.insert(r.start, Buffer::new());
664 continue;
665 }
666
667 let idx = fetched.partition_point(|(s, _)| s.start <= r.start);
670 let (span, buf) = &fetched[idx - 1];
671 debug_assert!(span.start <= r.start && r.end <= span.end);
672
673 let off = r.start - span.start;
674 out.insert(r.start, buf.clone().sliced(off..off + r.len()));
675 }
676
677 debug_assert_eq!(out.len(), ranges.len());
678 Ok(out)
679 }
680}
681
682#[cfg(feature = "cloud")]
683#[derive(Clone)]
684pub struct ObjectStoreByteSource {
685 store: PolarsObjectStore,
686 path: ObjectStorePath,
687 config: FetchConfig,
688}
689
690#[cfg(feature = "cloud")]
691impl ObjectStoreByteSource {
692 async fn try_new_from_path(
693 path: PlRefPath,
694 cloud_options: Option<&CloudOptions>,
695 io_metrics: Option<Arc<IOMetrics>>,
696 config: FetchConfig,
697 ) -> PolarsResult<Self> {
698 let (CloudLocation { prefix, .. }, mut store) =
699 build_object_store(path, cloud_options, false).await?;
700 let path = object_path_from_str(&prefix)?;
701
702 store.set_io_metrics(io_metrics);
703
704 Ok(Self {
705 store,
706 path,
707 config,
708 })
709 }
710
711 #[allow(unused)]
712 fn chunk_size(&self) -> usize {
713 self.config.chunk_size
714 }
715
716 fn concurrency_strategy(&self) -> ConcurrencyStrategy {
717 self.config.strategy
718 }
719}
720
721#[cfg(feature = "cloud")]
722impl ByteSource for ObjectStoreByteSource {
723 async fn get_size(&self) -> PolarsResult<usize> {
724 Ok(self
725 .store
726 .head(&self.path, self.concurrency_strategy())
727 .await?
728 .size as usize)
729 }
730
731 async fn get_suffix(&self, n: usize) -> PolarsResult<(Buffer<u8>, usize)> {
732 self.store.get_suffix(&self.path, n, self.config).await
733 }
734
735 async fn get_prefix(&self, n: usize) -> PolarsResult<(Buffer<u8>, usize)> {
736 self.store.get_prefix(&self.path, n, self.config).await
737 }
738
739 async fn get_range(&self, range: Range<usize>) -> PolarsResult<Buffer<u8>> {
740 self.store.get_range(&self.path, range, self.config).await
741 }
742
743 async fn get_ranges(
744 &self,
745 ranges: &mut [Range<usize>],
746 ) -> PolarsResult<PlHashMap<usize, Buffer<u8>>> {
747 self.store
748 .get_ranges_sort(&self.path, ranges, self.config)
749 .await
750 }
751}
752
753pub enum DynByteSource {
755 Buffer(BufferByteSource),
756 File(FileByteSource),
757 #[cfg(feature = "cloud")]
758 Cloud(ObjectStoreByteSource),
759}
760
761impl DynByteSource {
762 pub fn variant_name(&self) -> &str {
763 match self {
764 Self::Buffer(_) => "Buffer",
765 Self::File(_) => "File",
766 #[cfg(feature = "cloud")]
767 Self::Cloud(_) => "Cloud",
768 }
769 }
770
771 #[cfg_attr(not(feature = "cloud"), allow(unused_variables))]
775 pub fn with_fetch_config(&self, config: FetchConfig) -> Option<Self> {
776 match self {
777 Self::Buffer(_) | Self::File(_) => None,
778 #[cfg(feature = "cloud")]
779 Self::Cloud(v) => Some(Self::Cloud(ObjectStoreByteSource {
780 config,
781 ..v.clone()
782 })),
783 }
784 }
785
786 pub fn is_cloud(&self) -> bool {
787 match self {
788 Self::Buffer(_) => false,
789 Self::File(_) => false,
790 #[cfg(feature = "cloud")]
791 Self::Cloud(_) => true,
792 }
793 }
794
795 pub fn chunk_size(&self) -> Option<usize> {
796 match self {
797 Self::Buffer(_) => None,
798 Self::File(_) => None,
799 #[cfg(feature = "cloud")]
800 Self::Cloud(source) => Some(source.config.chunk_size),
801 }
802 }
803
804 pub fn concurrency_strategy(&self) -> Option<ConcurrencyStrategy> {
805 match self {
806 Self::Buffer(_) => None,
807 Self::File(_) => None,
809 #[cfg(feature = "cloud")]
810 Self::Cloud(source) => Some(source.concurrency_strategy()),
811 }
812 }
813}
814
815impl Default for DynByteSource {
816 fn default() -> Self {
817 Self::Buffer(BufferByteSource(Buffer::new()))
818 }
819}
820
821impl ByteSource for DynByteSource {
822 async fn get_size(&self) -> PolarsResult<usize> {
823 match self {
824 Self::Buffer(v) => v.get_size().await,
825 Self::File(v) => v.get_size().await,
826 #[cfg(feature = "cloud")]
827 Self::Cloud(v) => v.get_size().await,
828 }
829 }
830
831 async fn get_suffix(&self, n: usize) -> PolarsResult<(Buffer<u8>, usize)> {
832 match self {
833 Self::Buffer(v) => v.get_suffix(n).await,
834 Self::File(v) => v.get_suffix(n).await,
835 #[cfg(feature = "cloud")]
836 Self::Cloud(v) => v.get_suffix(n).await,
837 }
838 }
839
840 async fn get_prefix(&self, n: usize) -> PolarsResult<(Buffer<u8>, usize)> {
841 match self {
842 Self::Buffer(v) => v.get_prefix(n).await,
843 Self::File(v) => v.get_prefix(n).await,
844 #[cfg(feature = "cloud")]
845 Self::Cloud(v) => v.get_prefix(n).await,
846 }
847 }
848
849 async fn get_range(&self, range: Range<usize>) -> PolarsResult<Buffer<u8>> {
850 match self {
851 Self::Buffer(v) => v.get_range(range).await,
852 Self::File(v) => v.get_range(range).await,
853 #[cfg(feature = "cloud")]
854 Self::Cloud(v) => v.get_range(range).await,
855 }
856 }
857
858 async fn get_ranges(
859 &self,
860 ranges: &mut [Range<usize>],
861 ) -> PolarsResult<PlHashMap<usize, Buffer<u8>>> {
862 match self {
863 Self::Buffer(v) => v.get_ranges(ranges).await,
864 Self::File(v) => v.get_ranges(ranges).await,
865 #[cfg(feature = "cloud")]
866 Self::Cloud(v) => v.get_ranges(ranges).await,
867 }
868 }
869}
870
871impl From<BufferByteSource> for DynByteSource {
872 fn from(value: BufferByteSource) -> Self {
873 Self::Buffer(value)
874 }
875}
876
877impl From<FileByteSource> for DynByteSource {
878 fn from(value: FileByteSource) -> Self {
879 Self::File(value)
880 }
881}
882
883#[cfg(feature = "cloud")]
884impl From<ObjectStoreByteSource> for DynByteSource {
885 fn from(value: ObjectStoreByteSource) -> Self {
886 Self::Cloud(value)
887 }
888}
889
890impl From<Buffer<u8>> for DynByteSource {
891 fn from(value: Buffer<u8>) -> Self {
892 Self::Buffer(BufferByteSource(value))
893 }
894}
895
896#[derive(Clone, Debug)]
897pub enum DynByteSourceBuilder {
898 Mmap,
899 FilePread(FileReadContext),
901 ObjectStore(FetchConfig),
903}
904
905impl DynByteSourceBuilder {
906 pub async fn try_build_from_path(
907 &self,
908 path: PlRefPath,
909 cloud_options: Option<&CloudOptions>,
910 io_metrics: Option<Arc<IOMetrics>>,
911 ) -> PolarsResult<DynByteSource> {
912 Ok(match self {
913 Self::Mmap => {
914 BufferByteSource::try_new_mmap_from_path(path.as_std_path(), cloud_options)
915 .await?
916 .into()
917 },
918 Self::FilePread(read_context) => {
919 FileByteSource::try_new_from_path(path, read_context.clone(), io_metrics)
920 .await?
921 .into()
922 },
923 Self::ObjectStore(fetch_config) => feature_gated!("cloud", {
924 ObjectStoreByteSource::try_new_from_path(
925 path,
926 cloud_options,
927 io_metrics,
928 *fetch_config,
929 )
930 .await?
931 .into()
932 }),
933 })
934 }
935
936 pub fn chunk_size(&self) -> Option<usize> {
937 match self {
938 Self::Mmap => None,
939 Self::FilePread(_) => None,
940 Self::ObjectStore(fetch_config) => Some(fetch_config.chunk_size),
941 }
942 }
943
944 pub fn concurrency_strategy(&self) -> Option<&ConcurrencyStrategy> {
945 match self {
946 Self::Mmap => None,
947 Self::FilePread(_) => None,
948 Self::ObjectStore(fetch_config) => Some(&fetch_config.strategy),
949 }
950 }
951}
952
953#[cfg(test)]
954mod tests {
955 use super::*;
956
957 const MIB: usize = 1 << 20;
958
959 fn source(dir: &std::path::Path, len: usize) -> (FileByteSource, Vec<u8>) {
961 let path = dir.join("f.bin");
962 let contents: Vec<u8> = (0..len).map(|i| (i % 251) as u8).collect();
963 std::fs::write(&path, &contents).unwrap();
964
965 let read_context = FileReadContext {
966 enable_o_direct: false,
967 concurrency: 4,
968 permits: Arc::new(Semaphore::new(4)),
969 advice: FileAdvice::Normal,
970 };
971 let source = FileByteSource::try_new_from_std(
972 std::fs::File::open(&path).unwrap(),
973 read_context,
974 None,
975 )
976 .unwrap();
977 (source, contents)
978 }
979
980 fn runtime() -> tokio::runtime::Runtime {
981 tokio::runtime::Builder::new_multi_thread()
982 .worker_threads(2)
983 .enable_all()
984 .build()
985 .unwrap()
986 }
987
988 #[test]
989 fn get_ranges_resolves_every_range() {
990 let dir = tempfile::tempdir().unwrap();
991 let (source, contents) = source(dir.path(), 17 * MIB);
992 let rt = runtime();
993
994 let cases: [(&str, &[Range<usize>]); 6] = [
995 ("disjoint", &[0..100, 500..600, 900..1000]),
996 ("unsorted", &[900..1000, 0..100, 500..600]),
997 ("partial overlap", &[0..100, 50..150]),
998 ("contained", &[0..200, 50..100]),
999 ("empty range at a span end", &[0..100, 100..100]),
1001 (
1004 "overlapping spans",
1005 &[
1006 0..4096,
1007 5_000_000..14_000_000,
1008 13_000_000..13_500_000,
1009 13_500_000..16_000_000,
1010 ],
1011 ),
1012 ];
1013
1014 for (name, ranges) in cases {
1015 let mut ranges = ranges.to_vec();
1016 let expect = ranges.clone();
1017 let out = rt.block_on(source.get_ranges(&mut ranges)).unwrap();
1018
1019 assert_eq!(out.len(), expect.len(), "{name}");
1020 for r in &expect {
1021 let got = out
1022 .get(&r.start)
1023 .unwrap_or_else(|| panic!("{name}: {r:?} missing"));
1024 assert_eq!(got.as_ref(), &contents[r.clone()], "{name}: {r:?}");
1025 }
1026 }
1027 }
1028
1029 #[test]
1030 fn read_blocking_reads_the_range() {
1031 let dir = tempfile::tempdir().unwrap();
1032 let (source, contents) = source(dir.path(), 3 * MIB);
1033
1034 for r in [0..0, 0..1, 100..4096, MIB - 7..MIB + 9, 0..3 * MIB] {
1035 let got = source.read_blocking(r.clone()).unwrap();
1036 assert_eq!(got.as_ref(), &contents[r.clone()], "{r:?}");
1037 }
1038 }
1039
1040 #[cfg(all(
1041 target_os = "linux",
1042 any(target_arch = "x86_64", target_arch = "aarch64")
1043 ))]
1044 #[test]
1045 fn is_cached_follows_the_page_cache() {
1046 let dir = tempfile::tempdir().unwrap();
1047 let (source, _) = source(dir.path(), 4 * MIB);
1048 let ranges = [0..100, MIB + 1..2 * MIB + 1, 4 * MIB - 1..4 * MIB, 5..5];
1049
1050 if !source.is_cached(&ranges) {
1052 return;
1053 }
1054 assert!(source.is_cached(&[]));
1055
1056 source.file.sync_all().unwrap();
1058 unsafe { libc::posix_fadvise(source.file.as_raw_fd(), 0, 0, libc::POSIX_FADV_DONTNEED) };
1059 let mut st: libc::statfs = unsafe { std::mem::zeroed() };
1060 let path = std::ffi::CString::new(dir.path().as_os_str().as_encoded_bytes()).unwrap();
1061 assert_eq!(unsafe { libc::statfs(path.as_ptr(), &mut st) }, 0);
1062 if st.f_type == libc::TMPFS_MAGIC {
1063 return;
1064 }
1065 assert!(!source.is_cached(&ranges));
1066
1067 let read = MIB + 1..2 * MIB + 1;
1068 source.read_blocking(read.clone()).unwrap();
1069 assert!(source.is_cached(std::slice::from_ref(&read)));
1070 assert!(!source.is_cached(&ranges));
1071 }
1072
1073 #[cfg(all(
1074 target_os = "linux",
1075 any(target_arch = "x86_64", target_arch = "aarch64")
1076 ))]
1077 #[test]
1078 fn is_cached_does_not_bridge_gaps() {
1079 let page = *polars_utils::mem::PAGE_SIZE;
1080 let dir = tempfile::tempdir().unwrap();
1081 let (source, _) = source(dir.path(), 3 * page);
1082 let (all, middle) = (0..3 * page, page..2 * page);
1083 if !source.is_cached(std::slice::from_ref(&all)) {
1085 return;
1086 }
1087
1088 source.file.sync_all().unwrap();
1090 unsafe {
1091 libc::posix_fadvise(
1092 source.file.as_raw_fd(),
1093 page as libc::off_t,
1094 page as libc::off_t,
1095 libc::POSIX_FADV_DONTNEED,
1096 )
1097 };
1098 if source.is_cached(std::slice::from_ref(&middle)) {
1099 return;
1100 }
1101 assert!(source.is_cached(&[2 * page..3 * page, 0..page]));
1102 assert!(!source.is_cached(&[0..page, page..2 * page]));
1103 assert!(!source.is_cached(std::slice::from_ref(&all)));
1104 }
1105}