xref: /aosp_15_r20/system/core/init/libprefetch/prefetch/src/replay.rs (revision 00c7fec1bb09f3284aad6a6f96d2f63dfc3650ad)
1 // Copyright (C) 2024 The Android Open Source Project
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //      http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 use std::clone::Clone;
16 use std::convert::TryInto;
17 use std::fmt::Display;
18 use std::mem::replace;
19 use std::os::unix::io::AsRawFd;
20 use std::sync::Arc;
21 use std::sync::Mutex;
22 use std::sync::RwLock;
23 use std::thread;
24 
25 use log::debug;
26 use log::error;
27 use log::warn;
28 use lru_cache::LruCache;
29 use nix::errno::Errno;
30 use nix::fcntl::posix_fadvise;
31 use regex::Regex;
32 
33 use crate::args::ConfigFile;
34 use crate::format::Record;
35 use crate::format::{FileId, RecordsFile};
36 use crate::Error;
37 use crate::ReplayArgs;
38 use libc::{c_void, off64_t, pread64};
39 use std::fs::File;
40 
41 const READ_SZ: usize = 1024 * 1024;
42 
43 struct ScopedLog<T: Display + Sized> {
44     msg: T,
45     thd_id: usize,
46 }
47 
scoped_log<T: Display + Sized>(ctx: usize, msg: T) -> ScopedLog<T>48 fn scoped_log<T: Display + Sized>(ctx: usize, msg: T) -> ScopedLog<T> {
49     let thd_id = ctx;
50     debug!("{} {} start", thd_id, msg);
51     ScopedLog { msg, thd_id }
52 }
53 
54 impl<T: Display> Drop for ScopedLog<T> {
drop(&mut self)55     fn drop(&mut self) {
56         debug!("{} {} end", self.thd_id, self.msg);
57     }
58 }
59 
readahead( id: usize, file: Arc<File>, record: &Record, buffer: &mut [u8; READ_SZ], ) -> Result<(), Error>60 fn readahead(
61     id: usize,
62     file: Arc<File>,
63     record: &Record,
64     buffer: &mut [u8; READ_SZ],
65 ) -> Result<(), Error> {
66     debug!("readahead {:?}", record);
67     let _dbg = scoped_log(id, "readahead");
68 
69     let mut current_offset: off64_t = record
70         .offset
71         .try_into()
72         .map_err(|_| Error::Read { error: "Failed to convert offset".to_string() })?;
73     let mut remaining_data: usize = record
74         .length
75         .try_into()
76         .map_err(|_| Error::Read { error: "Failed to convert length".to_string() })?;
77 
78     while remaining_data > 0 {
79         let read_size = std::cmp::min(READ_SZ, remaining_data);
80 
81         // SAFETY: This is safe because
82         // - the file is known to exist and opened
83         // - buffer is allocated upfront and is guaranteed by the fact it comes from a mutable slice reference.
84         // - read_size is guaranteed not to exceed length of the buffer.
85         let bytes_read = unsafe {
86             pread64(file.as_raw_fd(), buffer.as_mut_ptr() as *mut c_void, read_size, current_offset)
87         };
88 
89         if bytes_read == -1 {
90             return Err(Error::Read { error: format!("readahead failed: {}", Errno::last_raw()) });
91         }
92 
93         if bytes_read == 0 {
94             break; // End of file reached
95         }
96 
97         current_offset += bytes_read as off64_t;
98         remaining_data -= bytes_read as usize;
99     }
100 
101     // TODO: Try readahead() syscall or async I/O
102     Ok(())
103 }
104 
worker_internal( id: usize, state: Arc<Mutex<SharedState>>, records_file: Arc<RwLock<RecordsFile>>, exit_on_error: bool, exclude_files_regex: Vec<Regex>, buffer: &mut [u8], ) -> Result<(), Error>105 fn worker_internal(
106     id: usize,
107     state: Arc<Mutex<SharedState>>,
108     records_file: Arc<RwLock<RecordsFile>>,
109     exit_on_error: bool,
110     exclude_files_regex: Vec<Regex>,
111     buffer: &mut [u8],
112 ) -> Result<(), Error> {
113     loop {
114         let index = {
115             let mut state = state.lock().unwrap();
116             if state.result.is_err() {
117                 return Ok(());
118             }
119             state.next_record()
120         };
121 
122         let record = {
123             let rf = records_file.read().unwrap();
124             if index >= rf.inner.records.len() {
125                 return Ok(());
126             }
127             rf.inner.records.get(index).unwrap().clone()
128         };
129 
130         let _dbg = scoped_log(id, "record_replay");
131 
132         let file = state.lock().unwrap().fds.get_mut(&record.file_id).map(|f| f.clone());
133 
134         let file = match file {
135             Some(file) => file,
136             None => {
137                 let file = Arc::new({
138                     let file = records_file
139                         .read()
140                         .unwrap()
141                         .open_file(record.file_id.clone(), &exclude_files_regex);
142                     if let Err(e) = file {
143                         if exit_on_error {
144                             return Err(e);
145                         } else {
146                             match e {
147                                 Error::SkipPrefetch { path } => {
148                                     debug!("Skipping file during replay: {}", path);
149                                 }
150                                 _ => error!(
151                                     "Failed to open file id: {} with {}",
152                                     record.file_id.clone(),
153                                     e.to_string()
154                                 ),
155                             }
156                             continue;
157                         }
158                     }
159 
160                     let file = file.unwrap();
161                     // We do not want the filesystem be intelligent and prefetch more than what this
162                     // code is reading. So turn off prefetch.
163 
164                     if let Err(e) = posix_fadvise(
165                         file.as_raw_fd(),
166                         0,
167                         0,
168                         nix::fcntl::PosixFadviseAdvice::POSIX_FADV_RANDOM,
169                     ) {
170                         warn!(
171                             "Failed to turn off filesystem read ahead for file id: {} with {}",
172                             record.file_id.clone(),
173                             e.to_string()
174                         );
175                     }
176                     file
177                 });
178                 let cache_file = file.clone();
179                 state.lock().unwrap().fds.insert(record.file_id.clone(), cache_file);
180                 file
181             }
182         };
183         if let Err(e) = readahead(id, file, &record, buffer.try_into().unwrap()) {
184             if exit_on_error {
185                 return Err(e);
186             } else {
187                 error!(
188                     "readahead failed on file id: {} with: {}",
189                     record.file_id.clone(),
190                     e.to_string()
191                 );
192                 continue;
193             }
194         }
195     }
196 }
197 
worker( id: usize, state: Arc<Mutex<SharedState>>, records_file: Arc<RwLock<RecordsFile>>, exit_on_error: bool, exclude_files_regex: Vec<Regex>, buffer: &mut [u8], )198 fn worker(
199     id: usize,
200     state: Arc<Mutex<SharedState>>,
201     records_file: Arc<RwLock<RecordsFile>>,
202     exit_on_error: bool,
203     exclude_files_regex: Vec<Regex>,
204     buffer: &mut [u8],
205 ) {
206     let _dbg = scoped_log(id, "read_loop");
207     let result = worker_internal(
208         id,
209         state.clone(),
210         records_file,
211         exit_on_error,
212         exclude_files_regex,
213         buffer,
214     );
215     if result.is_err() {
216         error!("worker failed with {:?}", result);
217         let mut state = state.lock().unwrap();
218         if state.result.is_ok() {
219             state.result = result;
220         }
221     }
222 }
223 
224 #[derive(Debug)]
225 pub struct SharedState {
226     fds: LruCache<FileId, Arc<File>>,
227     records_index: usize,
228     result: Result<(), Error>,
229 }
230 
231 impl SharedState {
next_record(&mut self) -> usize232     fn next_record(&mut self) -> usize {
233         let ret = self.records_index;
234         self.records_index += 1;
235         ret
236     }
237 }
238 
239 /// Runtime, in-memory, representation of records file structure.
240 #[derive(Debug)]
241 pub struct Replay {
242     records_file: Arc<RwLock<RecordsFile>>,
243     io_depth: u16,
244     exit_on_error: bool,
245     state: Arc<Mutex<SharedState>>,
246     exclude_files_regex: Vec<Regex>,
247 }
248 
249 impl Replay {
250     /// Creates Replay from input `args`.
new(args: &ReplayArgs) -> Result<Self, Error>251     pub fn new(args: &ReplayArgs) -> Result<Self, Error> {
252         let _dbg = scoped_log(1, "new");
253         let reader: File = File::open(&args.path).map_err(|source| Error::Open {
254             source,
255             path: args.path.to_str().unwrap().to_owned(),
256         })?;
257         let rf: RecordsFile = serde_cbor::from_reader(reader)
258             .map_err(|error| Error::Deserialize { error: error.to_string() })?;
259 
260         let mut exclude_files_regex: Vec<Regex> = Vec::new();
261         // The path to the configuration file is optional in the command.
262         // If the path is provided, the configuration file will be read.
263         if !&args.config_path.as_os_str().is_empty() {
264             let config_reader = File::open(&args.config_path).map_err(|source| Error::Open {
265                 source,
266                 path: args.path.to_str().unwrap().to_owned(),
267             })?;
268             let cf: ConfigFile = serde_json::from_reader(config_reader)
269                 .map_err(|error| Error::Deserialize { error: error.to_string() })?;
270 
271             for file_to_exclude in &cf.files_to_exclude_regex {
272                 exclude_files_regex.push(Regex::new(file_to_exclude).unwrap());
273             }
274         }
275 
276         Ok(Self {
277             records_file: Arc::new(RwLock::new(rf)),
278             io_depth: args.io_depth,
279             exit_on_error: args.exit_on_error,
280             state: Arc::new(Mutex::new(SharedState {
281                 fds: LruCache::new(args.max_fds.into()),
282                 records_index: 0,
283                 result: Ok(()),
284             })),
285             exclude_files_regex,
286         })
287     }
288 
289     /// Replay records.
replay(self) -> Result<(), Error>290     pub fn replay(self) -> Result<(), Error> {
291         let _dbg = scoped_log(1, "replay");
292         let mut threads = vec![];
293         for i in 0..self.io_depth {
294             let i_clone = i as usize;
295             let state = self.state.clone();
296             let records_file = self.records_file.clone();
297             let exit_on_error = self.exit_on_error;
298             let exclude_files_regex = self.exclude_files_regex.clone();
299 
300             let mut buffer = Box::new([0u8; READ_SZ]);
301 
302             threads.push(thread::Builder::new().spawn(move || {
303                 worker(
304                     i_clone,
305                     state,
306                     records_file,
307                     exit_on_error,
308                     exclude_files_regex,
309                     buffer.as_mut_slice(),
310                 )
311             }));
312         }
313         for thread in threads {
314             thread.unwrap().join().unwrap();
315         }
316         replace(&mut self.state.lock().unwrap().result, Ok(()))
317     }
318 }
319 
320 // WARNING: flaky tests.
321 // In these tests we create files, invalidate their caches and then replay.
322 // Verify that after reply the same portions of data is in memory.
323 //
324 // Since these tests to rely on presence or absence of data in cache, the
325 // files used by the tests should not be in tmp filesystem. So we use relative
326 // path as target directory. There is no guarantee that this target directory
327 // is not on temp filesystem but chances are better than using target directory
328 // in tempfs.
329 //
330 // Tests can be flaky if the system under tests is running low on memory. The
331 // tests create file using O_DIRECT so that no data is left in file cache.
332 // Though this is sufficient to avoid caching, but other processes reading these
333 // files(like anti-virus) or some other system processes might change the state
334 // of the cache. Or it may happen that the filesystem evicts the file before
335 // we verify that read ahead worked as intended.
336 #[cfg(test)]
337 pub mod tests {
338     use std::{
339         assert,
340         io::Write,
341         ops::Range,
342         path::{Path, PathBuf},
343         time::Duration,
344     };
345 
346     use crate::format::DeviceNumber;
347     use crate::format::FsInfo;
348     use crate::format::InodeNumber;
349     use crate::nanoseconds_since_boot;
350     use nix::sys::mman::MapFlags;
351     use nix::sys::mman::ProtFlags;
352     use serde::Deserialize;
353     use serde::Serialize;
354     use std::collections::HashMap;
355     use std::fs::OpenOptions;
356     use std::num::NonZeroUsize;
357     use std::os::fd::AsFd;
358     use std::os::unix::fs::symlink;
359     use std::os::unix::fs::MetadataExt;
360     use std::ptr::NonNull;
361     use tempfile::NamedTempFile;
362 
363     use super::*;
364     use crate::tracer::{
365         page_size,
366         tests::{copy_uncached_files_and_record_from, setup_test_dir},
367     };
368 
369     static MB: u64 = 1024 * 1024;
370     static KB: u64 = 1024;
371 
random_write(file: &mut NamedTempFile, base: u64) -> Range<u64>372     fn random_write(file: &mut NamedTempFile, base: u64) -> Range<u64> {
373         let start: u64 = base + (rand::random::<u64>() % (base / 2)) as u64;
374         let len: u64 = rand::random::<u64>() % (32 * KB);
375         let buf = vec![5; len as usize];
376         nix::sys::uio::pwrite(file.as_fd(), &buf, start as i64).unwrap();
377         start..(start + len)
378     }
379 
create_file( path: Option<&Path>, align: Option<u64>, ) -> (NamedTempFile, Vec<Range<u64>>)380     pub(crate) fn create_file(
381         path: Option<&Path>,
382         align: Option<u64>,
383     ) -> (NamedTempFile, Vec<Range<u64>>) {
384         let mut file = if let Some(path) = path {
385             NamedTempFile::new_in(path).unwrap()
386         } else {
387             NamedTempFile::new().unwrap()
388         };
389         let range1 = random_write(&mut file, 32 * KB);
390         let range2 = random_write(&mut file, 128 * KB);
391         let range3 = random_write(&mut file, 4 * MB);
392         if let Some(align) = align {
393             let orig_size = file.metadata().unwrap().len();
394             let aligned_size = orig_size + (align - (orig_size % align));
395             file.set_len(aligned_size).unwrap();
396         }
397         (file, vec![range1, range2, range3])
398     }
399 
generate_cached_files_and_record( path: Option<&Path>, create_symlink: bool, align: Option<u64>, ) -> (RecordsFile, Vec<(NamedTempFile, Vec<Range<u64>>)>)400     pub(crate) fn generate_cached_files_and_record(
401         path: Option<&Path>,
402         create_symlink: bool,
403         align: Option<u64>,
404     ) -> (RecordsFile, Vec<(NamedTempFile, Vec<Range<u64>>)>) {
405         let file1 = create_file(path, align);
406         let file2 = create_file(path, align);
407         let file3 = create_file(path, align);
408 
409         let mut f: RecordsFileBuilder = Default::default();
410         f.add_file(file1.0.path().to_str().unwrap());
411         f.add_file(file2.0.path().to_str().unwrap());
412         f.add_file(file3.0.path().to_str().unwrap());
413         if create_symlink {
414             let symlink_path = format!("{}-symlink", file1.0.path().to_str().unwrap());
415             symlink(file1.0.path().file_name().unwrap(), &symlink_path).unwrap();
416 
417             f.add_file(&symlink_path);
418         }
419         let rf = f.build().unwrap();
420         (rf, vec![file1, file2, file3])
421     }
422 
423     /// RecordsFileBuilder is primarily used for testing purpose. This
424     /// is a thin wrapper around "Record". This gives the ability
425     /// to test Records functionality. The flow of this test is as follows:
426     ///
427     /// 1: generate_cached_files_and_record -> This will create temporary files of different length
428     /// and builds the "RecordFile" format.
429     /// 2: For each of the file path create, a "RecordsFile" is generated.
430     ///    a: mmap the file based on the length.
431     ///    b: call mincore() to get the residency of pages in memory for the given
432     ///    length.
433     ///    c: Iterate over the buffer of pages returned by mincore(). If a page
434     ///    is not resident in RAM, construct the "Record" structure.
435     /// 3: build() function will finally return a constructed Prefetch Record which
436     /// contains all the "Record" structure required for "Replay".
437     #[derive(Debug, Default, Deserialize, Serialize)]
438     pub struct RecordsFileBuilder {
439         // Temporarily holds paths of all files opened by other processes.
440         pub(crate) paths: HashMap<String, FileId>,
441 
442         // Read inode numbers
443         inode_numbers: HashMap<(DeviceNumber, InodeNumber), FileId>,
444     }
445 
446     impl RecordsFileBuilder {
add_file(&mut self, path: &str)447         pub fn add_file(&mut self, path: &str) {
448             if self.paths.contains_key(path) {
449                 return;
450             }
451 
452             self.paths.insert(path.to_owned(), FileId(self.paths.len() as u64));
453         }
454 
build(&mut self) -> Result<RecordsFile, Error>455         pub fn build(&mut self) -> Result<RecordsFile, Error> {
456             let mut rf = RecordsFile::default();
457             for (path, mut id) in self.paths.drain() {
458                 let stat = Path::new(&path)
459                     .metadata()
460                     .map_err(|source| Error::Stat { source, path: path.clone() })?;
461 
462                 rf.inner
463                     .filesystems
464                     .entry(stat.dev())
465                     .or_insert(FsInfo { block_size: stat.blksize() });
466 
467                 if let Some(orig_id) = self.inode_numbers.get(&(stat.dev(), stat.ino())) {
468                     let inode = rf.inner.inode_map.get_mut(orig_id).unwrap();
469                     inode.paths.push(path.clone());
470 
471                     // There may be multiple paths for the file so from those path we may have multiple
472                     // ids. Override the id.
473                     id = orig_id.clone();
474                 } else {
475                     self.inode_numbers.insert((stat.dev(), stat.ino()), id.clone());
476                     rf.insert_or_update_inode(id.clone(), &stat, path.clone());
477                 }
478                 if let Some(mmap) = Mmap::create(&path, id)? {
479                     mmap.get_records(&mut rf.inner.records)?;
480                 }
481             }
482             Ok(rf)
483         }
484     }
485 
486     #[derive(Debug)]
487     pub(crate) struct Mmap {
488         map_addr: *mut c_void,
489         length: usize,
490         #[allow(dead_code)]
491         file: File,
492         file_id: FileId,
493     }
494 
495     impl Mmap {
create(path: &str, file_id: FileId) -> Result<Option<Self>, Error>496         pub fn create(path: &str, file_id: FileId) -> Result<Option<Self>, Error> {
497             let file = OpenOptions::new()
498                 .read(true)
499                 .write(false)
500                 .open(path)
501                 .map_err(|source| Error::Open { source, path: path.to_owned() })?;
502 
503             let length = file
504                 .metadata()
505                 .map_err(|source| Error::Stat { source, path: path.to_owned() })?
506                 .len() as usize;
507 
508             if length == 0 {
509                 return Ok(None);
510             }
511 
512             // SAFETY: This is safe because
513             // - the length is checked for zero
514             // - offset is set to 0
515             let map_addr = unsafe {
516                 nix::sys::mman::mmap(
517                     None,
518                     NonZeroUsize::new(length).unwrap(),
519                     ProtFlags::PROT_READ,
520                     MapFlags::MAP_SHARED,
521                     file.as_fd(),
522                     0,
523                 )
524                 .map_err(|source| Error::Mmap {
525                     error: source.to_string(),
526                     path: path.to_owned(),
527                 })?
528             };
529 
530             Ok(Some(Self { map_addr: map_addr.as_ptr(), length, file, file_id }))
531         }
532 
533         /// Construct the "Record" file based on pages resident in RAM.
get_records(&self, records: &mut Vec<Record>) -> Result<(), Error>534         pub(crate) fn get_records(&self, records: &mut Vec<Record>) -> Result<(), Error> {
535             let page_size = page_size()?;
536             let page_count = (self.length + page_size - 1) / page_size;
537             let mut buf: Vec<u8> = vec![0_u8; page_count];
538             // SAFETY: This is safe because
539             // - the file is mapped
540             // - buf points to a valid and sufficiently large memory region with the
541             //   requirement of (length+PAGE_SIZE-1) / PAGE_SIZE bytes
542             let ret = unsafe { libc::mincore(self.map_addr, self.length, buf.as_mut_ptr()) };
543             if ret < 0 {
544                 return Err(Error::Custom {
545                     error: format!("failed to query resident pages: {}", Errno::last_raw()),
546                 });
547             }
548             let mut i = 0;
549 
550             let mut offset_length: Option<(u64, u64)> = None;
551             for (index, resident) in buf.iter().enumerate() {
552                 if *resident != 0 {
553                     if let Some((_, length)) = &mut offset_length {
554                         *length += page_size as u64;
555                     } else {
556                         offset_length = Some((index as u64 * page_size as u64, page_size as u64));
557                     }
558                 } else if let Some((offset, length)) = offset_length {
559                     i += 1;
560                     records.push(Record {
561                         file_id: self.file_id.clone(),
562                         offset,
563                         length,
564                         timestamp: nanoseconds_since_boot(),
565                     });
566 
567                     offset_length = None;
568                 }
569             }
570 
571             if let Some((offset, length)) = offset_length {
572                 i += 1;
573                 records.push(Record {
574                     file_id: self.file_id.clone(),
575                     offset,
576                     length,
577                     timestamp: nanoseconds_since_boot(),
578                 });
579             }
580             debug!("records found: {} for {:?}", i, self);
581 
582             Ok(())
583         }
584     }
585 
586     impl Drop for Mmap {
drop(&mut self)587         fn drop(&mut self) {
588             // SAFETY: This is safe because
589             // - addr is mapped and is multiple of page_size
590             let ret = unsafe {
591                 nix::sys::mman::munmap(NonNull::new(self.map_addr).unwrap(), self.length)
592             };
593             if let Err(e) = ret {
594                 error!(
595                     "failed to munmap {:p} {} with {}",
596                     self.map_addr,
597                     self.length,
598                     e.to_string()
599                 );
600             }
601         }
602     }
603 
604     // Please see comment above RecordsFileBuilder.
rebuild_records_file(files: &[(PathBuf, Vec<Range<u64>>)]) -> RecordsFile605     fn rebuild_records_file(files: &[(PathBuf, Vec<Range<u64>>)]) -> RecordsFile {
606         // Validate that caches are dropped
607         let mut f: RecordsFileBuilder = Default::default();
608         for (path, _) in files {
609             f.add_file(path.to_str().unwrap());
610         }
611         f.build().unwrap()
612     }
613 
ensure_files_not_cached(files: &mut [(PathBuf, Vec<Range<u64>>)])614     fn ensure_files_not_cached(files: &mut [(PathBuf, Vec<Range<u64>>)]) {
615         assert!(rebuild_records_file(files).inner.records.is_empty());
616     }
617 
has_record(records: &[Record], key: &Record) -> bool618     fn has_record(records: &[Record], key: &Record) -> bool {
619         for r in records {
620             if r.offset == key.offset && r.length == key.length {
621                 return true;
622             }
623         }
624         false
625     }
626 
compare_records(old: &[Record], new: &[Record])627     fn compare_records(old: &[Record], new: &[Record]) {
628         for key in new {
629             if !has_record(old, key) {
630                 panic!("Failed to file {:?} in {:?}", key, old);
631             }
632         }
633     }
634 
create_test_config_file(files_to_exclude_regex: Vec<String>) -> String635     fn create_test_config_file(files_to_exclude_regex: Vec<String>) -> String {
636         let cfg = ConfigFile { files_to_exclude_regex, ..Default::default() };
637         serde_json::to_string(&cfg).unwrap()
638     }
639 
640     // TODO: Split this into individual tests for better readability.
641     // b/378554334
test_replay_internal( create_symlink: bool, exit_on_error: bool, inject_error: bool, exclude_all_files: bool, empty_exclude_file_list: bool, )642     fn test_replay_internal(
643         create_symlink: bool,
644         exit_on_error: bool,
645         inject_error: bool,
646         exclude_all_files: bool,
647         empty_exclude_file_list: bool,
648     ) {
649         let page_size = page_size().unwrap() as u64;
650         let test_base_dir = setup_test_dir();
651         let (rf, mut files) =
652             generate_cached_files_and_record(None, create_symlink, Some(page_size));
653 
654         // Here "uncached_files" emulate the files after reboot when none of those files data is in cache.
655         let (mut uncached_rf, mut uncached_files) =
656             copy_uncached_files_and_record_from(Path::new(&test_base_dir), &mut files, &rf);
657 
658         // Injects error(s) in the form of invalid filename
659         if inject_error {
660             if let Some(v) = uncached_rf.inner.inode_map.values_mut().next() {
661                 for path in &mut v.paths {
662                     path.push('-');
663                 }
664             }
665         }
666 
667         let mut file = NamedTempFile::new().unwrap();
668         file.write_all(&uncached_rf.add_checksum_and_serialize().unwrap()).unwrap();
669         let mut config_file = NamedTempFile::new().unwrap();
670 
671         let mut files_to_exclude: Vec<String> = Vec::new();
672         if exclude_all_files {
673             // Exclude files from replay by adding them in config
674             for v in uncached_rf.inner.inode_map.values_mut() {
675                 for path in &mut v.paths {
676                     files_to_exclude.push(path.to_string())
677                 }
678             }
679         } else if empty_exclude_file_list {
680             files_to_exclude.extend(vec![]);
681         } else {
682             // Exclude file1 and file2 during replay
683             files_to_exclude.extend(vec!["file1".to_owned(), "file2".to_owned()]);
684         }
685 
686         // Create a config json to exclude files during replay
687         let config_file_contents = create_test_config_file(files_to_exclude);
688         config_file.write_all(config_file_contents.as_bytes()).unwrap();
689 
690         ensure_files_not_cached(&mut uncached_files);
691 
692         let replay = Replay::new(&ReplayArgs {
693             path: file.path().to_owned(),
694             io_depth: 32,
695             max_fds: 128,
696             exit_on_error,
697             config_path: config_file.path().to_owned(),
698         })
699         .unwrap();
700 
701         let result = replay.replay();
702         // Sleep a bit so that readaheads are complete.
703         thread::sleep(Duration::from_secs(1));
704 
705         if exit_on_error && inject_error {
706             result.expect_err("Failure was expected");
707         } else if exclude_all_files {
708             let new_rf = rebuild_records_file(&uncached_files);
709             assert!(new_rf.inner.records.is_empty());
710         } else {
711             result.unwrap();
712 
713             // At this point, we have prefetched data for uncached file bringing same set of
714             // data in memory as the original cached files.
715             // If we record prefetch data for new files, we should get same records files
716             // (offset and lengths) except that the file names should be different.
717             // This block verifies it.
718             // Note: `new_rf` is for uncached_files. But, [un]fortunately, those "uncached_files"
719             // are now cached after we replayed the records.
720             let new_rf = rebuild_records_file(&uncached_files);
721             assert!(!new_rf.inner.records.is_empty());
722             assert_eq!(rf.inner.inode_map.len(), new_rf.inner.inode_map.len());
723             assert_eq!(rf.inner.records.len(), new_rf.inner.records.len());
724             compare_records(&rf.inner.records, &new_rf.inner.records);
725         }
726     }
727 
728     #[test]
test_replay()729     fn test_replay() {
730         test_replay_internal(true, false, false, false, false);
731     }
732 
733     #[test]
test_replay_strict()734     fn test_replay_strict() {
735         test_replay_internal(true, true, false, false, false);
736     }
737 
738     #[test]
test_replay_no_symlink()739     fn test_replay_no_symlink() {
740         test_replay_internal(false, false, false, false, false);
741     }
742 
743     #[test]
test_replay_no_symlink_strict()744     fn test_replay_no_symlink_strict() {
745         test_replay_internal(false, true, false, false, false);
746     }
747 
748     #[test]
test_replay_fails_on_error()749     fn test_replay_fails_on_error() {
750         test_replay_internal(true, true, true, false, false);
751     }
752 
753     #[test]
test_replay_exclude_all_files()754     fn test_replay_exclude_all_files() {
755         test_replay_internal(true, false, false, true, false);
756     }
757 
758     #[test]
test_replay_empty_exclude_files_list()759     fn test_replay_empty_exclude_files_list() {
760         test_replay_internal(true, false, false, false, true);
761     }
762 }
763