Skip to main content

polars_io/utils/
byte_source.rs

1use std::ops::Range;
2#[cfg(target_os = "linux")]
3use std::os::fd::AsRawFd;
4use std::path::Path;
5use std::sync::Arc;
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    /// # Panics
35    /// Panics if `range` is not in bounds.
36    async fn get_range(&self, range: Range<usize>) -> PolarsResult<Buffer<u8>>;
37    /// Note: This will mutably sort ranges for coalescing.
38    async fn get_ranges(
39        &self,
40        ranges: &mut [Range<usize>],
41    ) -> PolarsResult<PlHashMap<usize, Buffer<u8>>>;
42}
43
44/// Byte source backed by a `Buffer`, which can potentially be memory-mapped.
45pub struct BufferByteSource(pub Buffer<u8>);
46
47impl BufferByteSource {
48    async fn try_new_mmap_from_path(
49        path: &Path,
50        _cloud_options: Option<&CloudOptions>,
51    ) -> PolarsResult<Self> {
52        let file = Arc::new(
53            tokio::fs::File::open(path)
54                .await
55                .map_err(|err| _limit_path_len_io_err(path, err))?
56                .into_std()
57                .await,
58        );
59
60        Ok(Self(Buffer::from_owner(MMapSemaphore::new_from_file(
61            &file,
62        )?)))
63    }
64}
65
66impl ByteSource for BufferByteSource {
67    async fn get_size(&self) -> PolarsResult<usize> {
68        Ok(self.0.as_ref().len())
69    }
70
71    async fn get_range(&self, range: Range<usize>) -> PolarsResult<Buffer<u8>> {
72        let out = self.0.clone().sliced(range);
73        Ok(out)
74    }
75
76    async fn get_ranges(
77        &self,
78        ranges: &mut [Range<usize>],
79    ) -> PolarsResult<PlHashMap<usize, Buffer<u8>>> {
80        Ok(ranges
81            .iter()
82            .map(|x| (x.start, self.0.clone().sliced(x.clone())))
83            .collect())
84    }
85}
86
87/// Byte source backed by a `File`.
88pub struct FileByteSource {
89    file: Arc<std::fs::File>,
90    // Alignment for O_DIRECT, if supported.
91    o_direct_align: Option<DioAlign>,
92    // Manage concurrency.
93    concurrency: usize,
94    permits: Arc<Semaphore>,
95    // File size.
96    size: u64,
97    io_metrics: OptIOMetrics,
98}
99
100#[derive(Clone)]
101pub struct FileReadContext {
102    pub enable_o_direct: bool,
103    pub concurrency: usize,
104    pub permits: Arc<Semaphore>,
105    /// Ignored when direct I/O is active: there is no page cache to advise.
106    pub advice: FileAdvice,
107}
108
109impl std::fmt::Debug for FileReadContext {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        f.debug_struct("FileReadContext")
112            .field("available_permits", &self.permits.available_permits())
113            .field("enable_odirect", &self.enable_o_direct)
114            .field("concurrency", &self.concurrency)
115            .field("advice", &self.advice)
116            .finish()
117    }
118}
119
120#[cfg(target_os = "linux")]
121fn _fadvise(file: &std::fs::File, advice: FileAdvice) {
122    let advice = match advice {
123        // The OS default is already POSIX_FADV_NORMAL.
124        FileAdvice::Normal => return,
125        FileAdvice::Sequential => libc::POSIX_FADV_SEQUENTIAL,
126        FileAdvice::Random => libc::POSIX_FADV_RANDOM,
127        FileAdvice::WillNeed => libc::POSIX_FADV_WILLNEED,
128    };
129
130    unsafe {
131        libc::posix_fadvise(file.as_raw_fd(), 0, 0, advice);
132    }
133}
134
135#[cfg(unix)]
136pub(crate) fn pread_exact(
137    file: &std::fs::File,
138    buf: &mut [u8],
139    offset: u64,
140) -> std::io::Result<()> {
141    use std::os::unix::fs::FileExt;
142    file.read_exact_at(buf, offset)
143}
144
145#[cfg(windows)]
146fn pread_exact(file: &std::fs::File, buf: &mut [u8], offset: u64) -> std::io::Result<()> {
147    use std::os::windows::fs::FileExt;
148    let mut filled = 0;
149    while filled < buf.len() {
150        match file.seek_read(&mut buf[filled..], offset + filled as u64)? {
151            0 => return Err(std::io::ErrorKind::UnexpectedEof.into()),
152            n => filled += n,
153        }
154    }
155    Ok(())
156}
157
158fn read_buffered(file: &std::fs::File, offset: u64, len: usize) -> PolarsResult<Buffer<u8>> {
159    let mut buf = vec![0u8; len];
160    pread_exact(file, &mut buf, offset)?;
161    Ok(Buffer::from(buf))
162}
163
164/// Read `len` bytes at `offset`, through direct I/O when `align` says the file
165/// supports it and through the page cache otherwise.
166#[cfg(target_os = "linux")]
167fn read_at(
168    file: &std::fs::File,
169    align: Option<DioAlign>,
170    offset: u64,
171    len: usize,
172    size: u64,
173) -> PolarsResult<Buffer<u8>> {
174    match align {
175        Some(align) => direct_io::read_aligned(file, align, offset, len, size),
176        None => read_buffered(file, offset, len),
177    }
178}
179
180#[cfg(not(target_os = "linux"))]
181fn read_at(
182    file: &std::fs::File,
183    _align: Option<DioAlign>,
184    offset: u64,
185    len: usize,
186    _size: u64,
187) -> PolarsResult<Buffer<u8>> {
188    read_buffered(file, offset, len)
189}
190
191impl FileByteSource {
192    async fn try_new_from_path(
193        path: PlRefPath,
194        read_context: FileReadContext,
195        io_metrics: Option<Arc<IOMetrics>>,
196    ) -> PolarsResult<Self> {
197        // The open path is `open`, `fcntl`, `statx` and `fstat` - all blocking.
198        ASYNC
199            .spawn_blocking(move || Self::open_blocking(path, read_context, io_metrics))
200            .await
201            .expect("blocking task panicked")
202    }
203
204    fn open_blocking(
205        path: PlRefPath,
206        read_context: FileReadContext,
207        io_metrics: Option<Arc<IOMetrics>>,
208    ) -> PolarsResult<Self> {
209        let path = path.as_std_path();
210        let enable_o_direct = read_context.enable_o_direct;
211
212        let file = {
213            #[cfg(target_os = "linux")]
214            let f = if enable_o_direct {
215                direct_io::open_o_direct(path).or_else(|e| match e.raw_os_error() {
216                    // The filesystem does not support O_DIRECT.
217                    Some(libc::EINVAL) => std::fs::File::open(path),
218                    _ => Err(e),
219                })
220            } else {
221                std::fs::File::open(path)
222            };
223            #[cfg(not(target_os = "linux"))]
224            let f = std::fs::File::open(path);
225
226            f.map_err(|e| _limit_path_len_io_err(path, e))?
227        };
228
229        let file = Arc::new(file);
230
231        #[cfg(target_os = "linux")]
232        let o_direct_align =
233            direct_io::probe_or_disable(&file).map_err(|e| _limit_path_len_io_err(path, e))?;
234
235        #[cfg(not(target_os = "linux"))]
236        let o_direct_align: Option<DioAlign> = None;
237
238        #[cfg(target_os = "linux")]
239        if o_direct_align.is_none() {
240            // Inert under O_DIRECT: there is no page cache to advise.
241            _fadvise(&file, read_context.advice);
242        }
243
244        let concurrency = read_context.concurrency;
245        let permits = read_context.permits;
246
247        let size = file
248            .metadata()
249            .map_err(|e| _limit_path_len_io_err(path, e))?
250            .len();
251
252        if polars_config::config().verbose() {
253            let name = if polars_config::config().verbose_sensitive() {
254                path.display().to_string()
255            } else {
256                "<file>".to_string()
257            };
258            match (enable_o_direct, o_direct_align) {
259                (true, Some(a)) => eprintln!(
260                    "[FileByteSource]: {name}: direct IO active, \
261                        alignment offset: {}, memory: {}",
262                    a.offset, a.memory
263                ),
264                (true, None) => eprintln!(
265                    "[FileByteSource]: {name}: direct IO requested but not active, \
266                        using buffered reads"
267                ),
268                (false, _) => {},
269            }
270        }
271
272        Ok(FileByteSource {
273            file,
274            o_direct_align,
275            concurrency,
276            permits,
277            size,
278            io_metrics: OptIOMetrics(io_metrics),
279        })
280    }
281
282    pub fn try_new_from_std(
283        file: std::fs::File,
284        read_context: FileReadContext,
285        io_metrics: Option<Arc<IOMetrics>>,
286    ) -> PolarsResult<Self> {
287        let size = file.metadata()?.len();
288
289        #[cfg(target_os = "linux")]
290        let o_direct_align = direct_io::probe_or_disable(&file)?;
291
292        #[cfg(not(target_os = "linux"))]
293        let o_direct_align: Option<DioAlign> = None;
294
295        #[cfg(target_os = "linux")]
296        if o_direct_align.is_none() {
297            _fadvise(&file, read_context.advice);
298        }
299
300        let concurrency = read_context.concurrency;
301        let permits = read_context.permits;
302
303        // For verbose logging only. We cannot enable since the file_handle was handed to us.
304        let enable_o_direct = read_context.enable_o_direct;
305
306        if polars_config::config().verbose() {
307            match (enable_o_direct, o_direct_align) {
308                (true, Some(a)) => eprintln!(
309                    "[FileByteSource]: direct IO active, alignment offset: {}, memory: {}",
310                    a.offset, a.memory
311                ),
312                (true, None) => eprintln!(
313                    "[FileByteSource]: direct IO requested but not active, using buffered reads",
314                ),
315                (false, _) => {},
316            }
317        }
318
319        Ok(Self {
320            file: Arc::new(file),
321            o_direct_align,
322            concurrency,
323            permits,
324            size,
325            io_metrics: OptIOMetrics(io_metrics),
326        })
327    }
328
329    pub fn set_io_metrics(&mut self, io_metrics: Option<Arc<IOMetrics>>) -> &mut Self {
330        self.io_metrics = OptIOMetrics(io_metrics);
331        self
332    }
333
334    pub fn io_metrics(&self) -> &OptIOMetrics {
335        &self.io_metrics
336    }
337}
338
339impl ByteSource for FileByteSource {
340    async fn get_size(&self) -> PolarsResult<usize> {
341        usize::try_from(self.size)
342            .map_err(|_| polars_err!(ComputeError: "file size {} does not fit in usize", self.size))
343    }
344
345    async fn get_range(&self, range: Range<usize>) -> PolarsResult<Buffer<u8>> {
346        assert!(range.end as u64 <= self.size);
347
348        let file = self.file.clone();
349        let offset = range.start as u64;
350        let len = range.len();
351        let size = self.size;
352        let o_direct = self.o_direct_align;
353
354        let permit = self.permits.clone().acquire_owned().await.unwrap();
355
356        self.io_metrics()
357            .record_io_read(len as u64, async move {
358                ASYNC
359                    .spawn_blocking(move || {
360                        let _permit = permit;
361                        read_at(&file, o_direct, offset, len, size)
362                    })
363                    .await
364            })
365            .await
366            .expect("blocking task panicked")
367    }
368
369    async fn get_ranges(
370        &self,
371        ranges: &mut [Range<usize>],
372    ) -> PolarsResult<PlHashMap<usize, Buffer<u8>>> {
373        if let [range] = ranges {
374            let mut out = PlHashMap::with_capacity(1);
375            out.insert(range.start, self.get_range(range.clone()).await?);
376            return Ok(out);
377        }
378
379        ranges.sort_unstable_by_key(|r| r.start);
380
381        let mut spans: Vec<Range<usize>> = Vec::with_capacity(ranges.len());
382
383        // Threshold for coalescing. Note, individual ranges may exceed MAX_SPAN.
384        const MAX_SPAN: usize = 8 << 20;
385        // Tolerate small gaps. We match typical page size for now; this could be tuned.
386        const MAX_GAP: usize = 4096;
387
388        for r in ranges.iter() {
389            match spans.last_mut() {
390                Some(last)
391                    if r.start.saturating_sub(last.end) <= MAX_GAP
392                        && r.end.saturating_sub(last.start) <= MAX_SPAN =>
393                {
394                    last.end = last.end.max(r.end)
395                },
396                _ => spans.push(r.clone()),
397            }
398        }
399
400        let mut fetched: Vec<(Range<usize>, Buffer<u8>)> = futures::stream::iter(spans)
401            .map(|span| async move {
402                let buf = self.get_range(span.clone()).await?;
403                PolarsResult::Ok((span, buf))
404            })
405            .buffer_unordered(self.concurrency)
406            .try_collect()
407            .await?;
408
409        // Slice out of the containing span into the original ranges.
410        fetched.sort_unstable_by_key(|(s, _)| s.start);
411
412        if let Some(w) = ranges.windows(2).find(|w| w[0].start == w[1].start) {
413            polars_bail!(
414                ComputeError:
415                "duplicate range start {} in read request ({:?} and {:?})",
416                w[0].start, w[0], w[1]
417            );
418        }
419
420        let mut out = PlHashMap::with_capacity(ranges.len());
421        for r in ranges.iter() {
422            // No span available.
423            if r.is_empty() {
424                out.insert(r.start, Buffer::new());
425                continue;
426            }
427
428            // Spans are sorted by start and each range lies within one, so the
429            // containing span is the last one starting at or before `r.start`.
430            let idx = fetched.partition_point(|(s, _)| s.start <= r.start);
431            let (span, buf) = &fetched[idx - 1];
432            debug_assert!(span.start <= r.start && r.end <= span.end);
433
434            let off = r.start - span.start;
435            out.insert(r.start, buf.clone().sliced(off..off + r.len()));
436        }
437
438        debug_assert_eq!(out.len(), ranges.len());
439        Ok(out)
440    }
441}
442
443#[cfg(feature = "cloud")]
444pub struct ObjectStoreByteSource {
445    store: PolarsObjectStore,
446    path: ObjectStorePath,
447    config: FetchConfig,
448}
449
450#[cfg(feature = "cloud")]
451impl ObjectStoreByteSource {
452    async fn try_new_from_path(
453        path: PlRefPath,
454        cloud_options: Option<&CloudOptions>,
455        io_metrics: Option<Arc<IOMetrics>>,
456        config: FetchConfig,
457    ) -> PolarsResult<Self> {
458        let (CloudLocation { prefix, .. }, mut store) =
459            build_object_store(path, cloud_options, false).await?;
460        let path = object_path_from_str(&prefix)?;
461
462        store.set_io_metrics(io_metrics);
463
464        Ok(Self {
465            store,
466            path,
467            config,
468        })
469    }
470
471    #[allow(unused)]
472    fn chunk_size(&self) -> usize {
473        self.config.chunk_size
474    }
475
476    fn concurrency_strategy(&self) -> ConcurrencyStrategy {
477        self.config.strategy
478    }
479}
480
481#[cfg(feature = "cloud")]
482impl ByteSource for ObjectStoreByteSource {
483    async fn get_size(&self) -> PolarsResult<usize> {
484        Ok(self
485            .store
486            .head(&self.path, self.concurrency_strategy())
487            .await?
488            .size as usize)
489    }
490
491    async fn get_range(&self, range: Range<usize>) -> PolarsResult<Buffer<u8>> {
492        self.store.get_range(&self.path, range, self.config).await
493    }
494
495    async fn get_ranges(
496        &self,
497        ranges: &mut [Range<usize>],
498    ) -> PolarsResult<PlHashMap<usize, Buffer<u8>>> {
499        self.store
500            .get_ranges_sort(&self.path, ranges, self.config)
501            .await
502    }
503}
504
505/// Dynamic dispatch to async functions.
506pub enum DynByteSource {
507    Buffer(BufferByteSource),
508    File(FileByteSource),
509    #[cfg(feature = "cloud")]
510    Cloud(ObjectStoreByteSource),
511}
512
513impl DynByteSource {
514    pub fn variant_name(&self) -> &str {
515        match self {
516            Self::Buffer(_) => "Buffer",
517            Self::File(_) => "File",
518            #[cfg(feature = "cloud")]
519            Self::Cloud(_) => "Cloud",
520        }
521    }
522
523    pub fn is_cloud(&self) -> bool {
524        match self {
525            Self::Buffer(_) => false,
526            Self::File(_) => false,
527            #[cfg(feature = "cloud")]
528            Self::Cloud(_) => true,
529        }
530    }
531
532    pub fn chunk_size(&self) -> Option<usize> {
533        match self {
534            Self::Buffer(_) => None,
535            Self::File(_) => None,
536            #[cfg(feature = "cloud")]
537            Self::Cloud(source) => Some(source.config.chunk_size),
538        }
539    }
540
541    pub fn concurrency_strategy(&self) -> Option<ConcurrencyStrategy> {
542        match self {
543            Self::Buffer(_) => None,
544            // Concurrency is handled directly inside FileByteSource.
545            Self::File(_) => None,
546            #[cfg(feature = "cloud")]
547            Self::Cloud(source) => Some(source.concurrency_strategy()),
548        }
549    }
550}
551
552impl Default for DynByteSource {
553    fn default() -> Self {
554        Self::Buffer(BufferByteSource(Buffer::new()))
555    }
556}
557
558impl ByteSource for DynByteSource {
559    async fn get_size(&self) -> PolarsResult<usize> {
560        match self {
561            Self::Buffer(v) => v.get_size().await,
562            Self::File(v) => v.get_size().await,
563            #[cfg(feature = "cloud")]
564            Self::Cloud(v) => v.get_size().await,
565        }
566    }
567
568    async fn get_range(&self, range: Range<usize>) -> PolarsResult<Buffer<u8>> {
569        match self {
570            Self::Buffer(v) => v.get_range(range).await,
571            Self::File(v) => v.get_range(range).await,
572            #[cfg(feature = "cloud")]
573            Self::Cloud(v) => v.get_range(range).await,
574        }
575    }
576
577    async fn get_ranges(
578        &self,
579        ranges: &mut [Range<usize>],
580    ) -> PolarsResult<PlHashMap<usize, Buffer<u8>>> {
581        match self {
582            Self::Buffer(v) => v.get_ranges(ranges).await,
583            Self::File(v) => v.get_ranges(ranges).await,
584            #[cfg(feature = "cloud")]
585            Self::Cloud(v) => v.get_ranges(ranges).await,
586        }
587    }
588}
589
590impl From<BufferByteSource> for DynByteSource {
591    fn from(value: BufferByteSource) -> Self {
592        Self::Buffer(value)
593    }
594}
595
596impl From<FileByteSource> for DynByteSource {
597    fn from(value: FileByteSource) -> Self {
598        Self::File(value)
599    }
600}
601
602#[cfg(feature = "cloud")]
603impl From<ObjectStoreByteSource> for DynByteSource {
604    fn from(value: ObjectStoreByteSource) -> Self {
605        Self::Cloud(value)
606    }
607}
608
609impl From<Buffer<u8>> for DynByteSource {
610    fn from(value: Buffer<u8>) -> Self {
611        Self::Buffer(BufferByteSource(value))
612    }
613}
614
615#[derive(Clone, Debug)]
616pub enum DynByteSourceBuilder {
617    Mmap,
618    /// Use std::fs::File positional read (pread).
619    FilePread(FileReadContext),
620    /// Supports both cloud and local files, requires cloud feature.
621    ObjectStore(FetchConfig),
622}
623
624impl DynByteSourceBuilder {
625    pub async fn try_build_from_path(
626        &self,
627        path: PlRefPath,
628        cloud_options: Option<&CloudOptions>,
629        io_metrics: Option<Arc<IOMetrics>>,
630    ) -> PolarsResult<DynByteSource> {
631        Ok(match self {
632            Self::Mmap => {
633                BufferByteSource::try_new_mmap_from_path(path.as_std_path(), cloud_options)
634                    .await?
635                    .into()
636            },
637            Self::FilePread(read_context) => {
638                FileByteSource::try_new_from_path(path, read_context.clone(), io_metrics)
639                    .await?
640                    .into()
641            },
642            Self::ObjectStore(fetch_config) => feature_gated!("cloud", {
643                ObjectStoreByteSource::try_new_from_path(
644                    path,
645                    cloud_options,
646                    io_metrics,
647                    *fetch_config,
648                )
649                .await?
650                .into()
651            }),
652        })
653    }
654
655    pub fn chunk_size(&self) -> Option<usize> {
656        match self {
657            Self::Mmap => None,
658            Self::FilePread(_) => None,
659            Self::ObjectStore(fetch_config) => Some(fetch_config.chunk_size),
660        }
661    }
662
663    pub fn concurrency_strategy(&self) -> Option<&ConcurrencyStrategy> {
664        match self {
665            Self::Mmap => None,
666            Self::FilePread(_) => None,
667            Self::ObjectStore(fetch_config) => Some(&fetch_config.strategy),
668        }
669    }
670}
671
672#[cfg(test)]
673mod tests {
674    use super::*;
675
676    const MIB: usize = 1 << 20;
677
678    /// A file source over `len` bytes of `(i % 251)`, plus those bytes.
679    fn source(dir: &std::path::Path, len: usize) -> (FileByteSource, Vec<u8>) {
680        let path = dir.join("f.bin");
681        let contents: Vec<u8> = (0..len).map(|i| (i % 251) as u8).collect();
682        std::fs::write(&path, &contents).unwrap();
683
684        let read_context = FileReadContext {
685            enable_o_direct: false,
686            concurrency: 4,
687            permits: Arc::new(Semaphore::new(4)),
688            advice: FileAdvice::Normal,
689        };
690        let source = FileByteSource::try_new_from_std(
691            std::fs::File::open(&path).unwrap(),
692            read_context,
693            None,
694        )
695        .unwrap();
696        (source, contents)
697    }
698
699    fn runtime() -> tokio::runtime::Runtime {
700        tokio::runtime::Builder::new_multi_thread()
701            .worker_threads(2)
702            .enable_all()
703            .build()
704            .unwrap()
705    }
706
707    #[test]
708    fn get_ranges_resolves_every_range() {
709        let dir = tempfile::tempdir().unwrap();
710        let (source, contents) = source(dir.path(), 17 * MIB);
711        let rt = runtime();
712
713        let cases: [(&str, &[Range<usize>]); 6] = [
714            ("disjoint", &[0..100, 500..600, 900..1000]),
715            ("unsorted", &[900..1000, 0..100, 500..600]),
716            ("partial overlap", &[0..100, 50..150]),
717            ("contained", &[0..200, 50..100]),
718            // A zero-length column chunk is declarable in file metadata.
719            ("empty range at a span end", &[0..100, 100..100]),
720            // Spans become [0..4096, 5M..14M, 13M..16M]: the last two overlap,
721            // and both contain 13.5M, but only the third covers 16M.
722            (
723                "overlapping spans",
724                &[
725                    0..4096,
726                    5_000_000..14_000_000,
727                    13_000_000..13_500_000,
728                    13_500_000..16_000_000,
729                ],
730            ),
731        ];
732
733        for (name, ranges) in cases {
734            let mut ranges = ranges.to_vec();
735            let expect = ranges.clone();
736            let out = rt.block_on(source.get_ranges(&mut ranges)).unwrap();
737
738            assert_eq!(out.len(), expect.len(), "{name}");
739            for r in &expect {
740                let got = out
741                    .get(&r.start)
742                    .unwrap_or_else(|| panic!("{name}: {r:?} missing"));
743                assert_eq!(got.as_ref(), &contents[r.clone()], "{name}: {r:?}");
744            }
745        }
746    }
747}