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