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 //! See top level documentation for `crate::tracer`.
16
17 use std::collections::hash_map::Iter;
18 use std::fs::symlink_metadata;
19 use std::io::{ErrorKind, Write};
20 use std::iter::Iterator;
21 use std::mem::take;
22 use std::os::unix::fs::MetadataExt;
23 use std::{
24 collections::{HashMap, HashSet},
25 fs::read_to_string,
26 option::Option,
27 path::{Path, PathBuf},
28 };
29
30 use log::{debug, error, info, warn};
31 use regex::Regex;
32 use serde::Deserialize;
33 use serde::Serialize;
34 use walkdir::{DirEntry, WalkDir};
35
36 use crate::format::{coalesce_records, FsInfo};
37 use crate::tracer::{page_size, TracerConfigs};
38 use crate::{
39 format::{DeviceNumber, InodeNumber},
40 tracer::{TraceSubsystem, EXCLUDE_PATHS},
41 Error, FileId, Record, RecordsFile,
42 };
43
44 static MOUNTINFO_PATH: &str = "/proc/self/mountinfo";
45
46 // Trace events to enable
47 // Paths are relative to trace mount point
48 static TRACE_EVENTS: &[&str] =
49 &["events/filemap/mm_filemap_add_to_page_cache/enable", "tracing_on"];
50
51 // Filesystem types to ignore
52 static EXCLUDED_FILESYSTEM_TYPES: &[&str] = &[
53 "binder",
54 "bpf",
55 "cgroup",
56 "cgroup2",
57 "configfs",
58 "devpts",
59 "fuse", // No emulated storage
60 "fusectl",
61 "proc",
62 "pstore",
63 "selinuxfs",
64 "sysfs",
65 "tmpfs", // Check for apex mount points
66 "tracefs",
67 "functionfs", // adb, fastboot
68 "f2fs", // Skip /data mounts
69 ];
70
71 #[cfg(target_os = "linux")]
72 type MajorMinorType = u32;
73 #[cfg(target_os = "android")]
74 type MajorMinorType = i32;
75
76 // TODO(b/302056482): Once we uprev nix crate, we can use the function exported by the crate.
major(dev: DeviceNumber) -> MajorMinorType77 fn major(dev: DeviceNumber) -> MajorMinorType {
78 (((dev >> 32) & 0xffff_f000) | ((dev >> 8) & 0x0000_0fff)) as MajorMinorType
79 }
80
81 // TODO(b/302056482): Once we uprev nix crate, we can use the function exported by the crate.
minor(dev: DeviceNumber) -> MajorMinorType82 fn minor(dev: DeviceNumber) -> MajorMinorType {
83 (((dev >> 12) & 0xffff_ff00) | ((dev) & 0x0000_00ff)) as MajorMinorType
84 }
85
86 // TODO(b/302056482): Once we uprev nix crate, we can use the function exported by the crate.
makedev(major: MajorMinorType, minor: MajorMinorType) -> DeviceNumber87 fn makedev(major: MajorMinorType, minor: MajorMinorType) -> DeviceNumber {
88 let major = major as DeviceNumber;
89 let minor = minor as DeviceNumber;
90 ((major & 0xffff_f000) << 32)
91 | ((major & 0x0000_0fff) << 8)
92 | ((minor & 0xffff_ff00) << 12)
93 | (minor & 0x0000_00ff)
94 }
95
build_device_number(major: &str, minor: &str) -> Result<DeviceNumber, Error>96 fn build_device_number(major: &str, minor: &str) -> Result<DeviceNumber, Error> {
97 Ok(makedev(
98 major.parse::<MajorMinorType>().map_err(|e| Error::Custom {
99 error: format!("Failed to parse major number from {} with {}", major, e),
100 })?,
101 minor.parse::<MajorMinorType>().map_err(|e| Error::Custom {
102 error: format!("Failed to parse major number from {} with {}", major, e),
103 })?,
104 ))
105 }
106
107 // Returns timestamp in nanoseconds
build_timestamp(seconds: &str, microseconds: &str) -> Result<u64, Error>108 fn build_timestamp(seconds: &str, microseconds: &str) -> Result<u64, Error> {
109 let seconds = seconds.parse::<u64>().map_err(|e| Error::Custom {
110 error: format!("Failed to parse seconds from {} with {}", seconds, e),
111 })?;
112 let microseconds = microseconds.parse::<u64>().map_err(|e| Error::Custom {
113 error: format!("Failed to parse microseconds from {} with {}", seconds, e),
114 })?;
115 Ok((seconds * 1_000_000_000) + (microseconds * 1_000))
116 }
117
118 #[cfg(not(target_os = "android"))]
is_highly_privileged_path(_path: &Path) -> bool119 fn is_highly_privileged_path(_path: &Path) -> bool {
120 false
121 }
122
123 #[cfg(target_os = "android")]
is_highly_privileged_path(path: &Path) -> bool124 fn is_highly_privileged_path(path: &Path) -> bool {
125 // Following directories contain a mix of files with and without access to stat/read.
126 // We do not completely exclude these directories as there is still a lot of
127 // file we can issue readahead on. Some of the files on which readahead fails include
128 // - /system/bin/run-as
129 // - /data/data/com.android.storagemanager
130 // - /system/apex/com.android.art/bin/dex2oat32
131 // - /data/user/0/com.android.systemui
132 //
133 // - TODO: /system/apex: Apex files in read-only partition may be read during boot.
134 // However, some files may not have access. Double check the record files
135 // to filter out the exact path.
136 let privileged_paths = [
137 "/data/data",
138 "/data/user/0",
139 "/data/user_de/0",
140 "/system/bin/",
141 "/system/etc/selinux/",
142 "/system/system_ext/etc/selinux/",
143 "/system/product/etc/selinux/",
144 "/system/vendor/etc/selinux/",
145 "/system_ext/etc/selinux/",
146 "/product/etc/selinux/",
147 "/vendor/etc/selinux/",
148 "/system/xbin",
149 "/system/etc/",
150 "/data/",
151 "/postinstall/",
152 "/mnt/",
153 "/metadata/",
154 ];
155 for privileged in privileged_paths {
156 if path.to_str().unwrap().starts_with(privileged) {
157 return true;
158 }
159 }
160 false
161 }
162
163 enum DeviceState {
164 Include((DeviceNumber, PathBuf)),
165 Exclude(DeviceNumber),
166 }
167
168 /// Utility struct that helps to include and exclude devices and mount points that need and don't
169 /// need prefetching.
170 #[derive(Debug, Deserialize, Serialize)]
171 struct MountInfo {
172 // Map of device number to mount points
173 included_devices: HashMap<DeviceNumber, PathBuf>,
174
175 // Devices that we don't want to prefetch - like devices backing tempfs and sysfs
176 excluded_devices: HashSet<DeviceNumber>,
177 }
178
179 impl MountInfo {
180 // Parses file at `path` to build `Self`.`
create(path: &str) -> Result<Self, Error>181 fn create(path: &str) -> Result<Self, Error> {
182 let buf = read_to_string(path)
183 .map_err(|e| Error::Read { error: format!("Reading {} failed with: {}", path, e) })?;
184 Self::with_buf(&buf)
185 }
186
187 // Parses string in `buf` to build `Self`.
with_buf(buf: &str) -> Result<Self, Error>188 fn with_buf(buf: &str) -> Result<Self, Error> {
189 let regex = Self::get_regex()?;
190 let mut included_devices: HashMap<DeviceNumber, PathBuf> = HashMap::new();
191 let mut excluded_devices = HashSet::new();
192 let excluded_filesystem_types: HashSet<String> =
193 EXCLUDED_FILESYSTEM_TYPES.iter().map(|s| String::from(*s)).collect();
194 for line in buf.lines() {
195 if let Some(state) = Self::parse_line(®ex, &excluded_filesystem_types, line)? {
196 match state {
197 DeviceState::Include((device, path)) => {
198 included_devices.insert(device, path);
199 }
200 DeviceState::Exclude(device) => {
201 excluded_devices.insert(device);
202 }
203 }
204 }
205 }
206
207 Ok(Self { included_devices, excluded_devices })
208 }
209
parse_line( re: &Regex, excluded_filesystem_types: &HashSet<String>, line: &str, ) -> Result<Option<DeviceState>, Error>210 fn parse_line(
211 re: &Regex,
212 excluded_filesystem_types: &HashSet<String>,
213 line: &str,
214 ) -> Result<Option<DeviceState>, Error> {
215 let caps = match re.captures(line) {
216 Some(caps) => caps,
217 None => {
218 return Ok(None);
219 }
220 };
221 if &caps["relative_path"] != "/" {
222 return Ok(None);
223 }
224
225 let mount_point = &caps["mount_point"];
226 let mnt_pnt_with_slash = format!("{}/", mount_point);
227 let device_number = build_device_number(&caps["major"], &caps["minor"])?;
228 let fs_type = &caps["fs_type"];
229
230 if excluded_filesystem_types.contains(fs_type) {
231 info!(
232 "excluding fs type: {} for {} mount-point {} slash {}",
233 fs_type, line, mount_point, mnt_pnt_with_slash
234 );
235 return Ok(Some(DeviceState::Exclude(device_number)));
236 }
237
238 for excluded in EXCLUDE_PATHS {
239 if mnt_pnt_with_slash.starts_with(excluded) {
240 info!(
241 "exclude-paths fs type: {} for {} mount-point {} slash {}",
242 fs_type, line, mount_point, mnt_pnt_with_slash
243 );
244 return Ok(Some(DeviceState::Exclude(device_number)));
245 }
246 }
247
248 Ok(Some(DeviceState::Include((device_number, PathBuf::from(mount_point)))))
249 }
250
get_regex() -> Result<Regex, Error>251 fn get_regex() -> Result<Regex, Error> {
252 Regex::new(concat!(
253 r"^\s*(?P<id_unknown1>\S+)",
254 r"\s+(?P<id_unknown2>\S+)",
255 r"\s+(?P<major>[0-9]+):(?P<minor>[0-9]+)",
256 r"\s+(?P<relative_path>\S+)",
257 r"\s+(?P<mount_point>\S+)",
258 r"\s+(?P<mount_opt>\S+)",
259 r"\s+(?P<shared>\S+)",
260 r"\s+\S+",
261 r"\s+(?P<fs_type>\S+)",
262 r"\s+(?P<device_path>\S+)"
263 ))
264 .map_err(|e| Error::Custom {
265 error: format!("create regex for parsing mountinfo failed with: {}", e),
266 })
267 }
268
is_excluded(&self, device: &DeviceNumber) -> bool269 fn is_excluded(&self, device: &DeviceNumber) -> bool {
270 self.excluded_devices.contains(device)
271 }
272
get_included(&self) -> Iter<DeviceNumber, PathBuf>273 fn get_included(&self) -> Iter<DeviceNumber, PathBuf> {
274 self.included_devices.iter()
275 }
276 }
277
278 #[derive(Default, PartialEq, Debug, Eq, Hash)]
279 struct TraceLineInfo {
280 device: DeviceNumber,
281 inode: InodeNumber,
282 offset: u64,
283 timestamp: u64,
284 }
285
286 impl TraceLineInfo {
from_trace_line(re: &Regex, line: &str) -> Result<Option<Self>, Error>287 pub fn from_trace_line(re: &Regex, line: &str) -> Result<Option<Self>, Error> {
288 let caps = match re.captures(line) {
289 Some(caps) => caps,
290 None => return Ok(None),
291 };
292 let major = &caps["major"];
293 let minor = &caps["minor"];
294 let ino = &caps["ino"];
295 let offset = &caps["offset"];
296 let timestamp = build_timestamp(&caps["seconds"], &caps["microseconds"])?;
297 Ok(Some(TraceLineInfo {
298 device: build_device_number(major, minor)?,
299 inode: u64::from_str_radix(ino, 16).map_err(|e| Error::Custom {
300 error: format!("failed parsing inode: {} : {}", ino, e),
301 })?,
302 offset: offset.parse::<u64>().map_err(|e| Error::Custom {
303 error: format!("failed parsing offset: {} : {}", offset, e),
304 })?,
305 timestamp,
306 }))
307 }
308
309 #[cfg(test)]
from_fields( major: MajorMinorType, minor: MajorMinorType, inode: u64, offset: u64, timestamp: u64, ) -> Self310 pub fn from_fields(
311 major: MajorMinorType,
312 minor: MajorMinorType,
313 inode: u64,
314 offset: u64,
315 timestamp: u64,
316 ) -> Self {
317 Self { device: makedev(major, minor), inode, offset, timestamp }
318 }
319
320 // Convenience function to create regex. Used once per life of `record` but multiple times in
321 // case of tests.
get_trace_line_regex() -> Result<Regex, Error>322 pub fn get_trace_line_regex() -> Result<Regex, Error> {
323 // `page=[hex]` entry exists in 5.x kernel format but not in 6.x.
324 // Conversely, `order=[digit]` entry exists in 6.x kernel format but not in 5.x.
325 Regex::new(concat!(
326 r"^\s+(?P<cmd_pid>\S+)",
327 r"\s+(?P<cpu>\S+)",
328 r"\s+(?P<irq_stuff>\S+)",
329 r"\s+(?P<seconds>[0-9]+)\.(?P<microseconds>[0-9]+):",
330 r"\s+mm_filemap_add_to_page_cache:",
331 r"\s+dev\s+(?P<major>[0-9]+):(?P<minor>[0-9]+)",
332 r"\s+ino\s+(?P<ino>\S+)",
333 r"(?:\s+(?P<page>page=\S+))?",
334 r"\s+(?P<pfn>\S+)",
335 r"\s+ofs=(?P<offset>[0-9]+)",
336 r"(?:\s+(?P<order>\S+))?"
337 ))
338 .map_err(|e| Error::Custom {
339 error: format!("create regex for tracing failed with: {}", e),
340 })
341 }
342 }
343
344 #[derive(Debug, Serialize, Deserialize)]
345 struct MissingFile {
346 major_no: MajorMinorType,
347 minor_no: MajorMinorType,
348 inode: InodeNumber,
349 records: Vec<Record>,
350 }
351
352 #[derive(Debug, Default, Deserialize, Serialize)]
353 struct DebugInfo {
354 // Check all inodes for which paths don't exists. These are the files which
355 // * got deleted before we got to them
356 // * are filesystem internal files that fs access only via inode numbers.
357 missing_files: HashMap<FileId, MissingFile>,
358
359 // Number of bytes read that belongs to directory type inodes.
360 directory_read_bytes: u64,
361
362 // Number of bytes read from files for which we could not find a path in
363 // the filesystems.
364 missing_path_bytes: u64,
365
366 // Paths for which the current process doesn't have read permission.
367 privileged_paths: Vec<PathBuf>,
368 }
369
370 #[derive(Debug, Serialize)]
371 pub(crate) struct MemTraceSubsystem {
372 device_inode_map: HashMap<DeviceNumber, HashMap<InodeNumber, FileId>>,
373 // Count of all InodeNumber held by `device_inode_map`. This is handy to assign unique
374 // FileId.
375 inode_count: u64,
376
377 // `Record`s built from parsing read trace lines.
378 records: Vec<Record>,
379
380 // Regex to parse lines from trace_pipe.
381 #[serde(skip_serializing)]
382 regex: Regex,
383
384 // Mounted devices/filesystems either at the time of parsing trace file or at the time
385 // of building RecordsFile from parsed lines.
386 mount_info: MountInfo,
387
388 // A copy of TracerConfigs
389 tracer_configs: Option<TracerConfigs>,
390
391 // system page size stored to avoid frequent syscall to get the page size.
392 page_size: u64,
393
394 // The fields of the debug_info are populated when build_records_file is called (after lines
395 // are parsed from the trace file/pipe).
396 debug_info: DebugInfo,
397 }
398
399 impl MemTraceSubsystem {
update_configs(configs: &mut TracerConfigs)400 pub fn update_configs(configs: &mut TracerConfigs) {
401 for path in EXCLUDE_PATHS {
402 configs.excluded_paths.push(path.to_owned().to_string());
403 }
404
405 for event in TRACE_EVENTS {
406 configs.trace_events.push(event.to_owned().to_string());
407 }
408 configs.mountinfo_path = Some(MOUNTINFO_PATH.to_string());
409 }
410
create_with_configs(tracer_configs: TracerConfigs) -> Result<Self, Error>411 pub fn create_with_configs(tracer_configs: TracerConfigs) -> Result<Self, Error> {
412 static INITIAL_RECORDS_CAPACITY: usize = 100_000;
413 debug!("TracerConfig: {:#?}", tracer_configs);
414
415 let regex = TraceLineInfo::get_trace_line_regex()?;
416 let mount_info = MountInfo::create(tracer_configs.mountinfo_path.as_ref().unwrap())?;
417 debug!("mountinfo: {:#?}", mount_info);
418
419 Ok(Self {
420 device_inode_map: HashMap::new(),
421 inode_count: 0,
422 // For one product of android, we see around 50k records. To avoid a lot allocations
423 // and copying of records, we create a vec of this size.
424 //
425 // We do this to reduces chances of losing data, however unlikely, coming over
426 // `trace_pipe`.
427 //
428 // Note: Once we are done reading trace lines, we are less pedantic about allocations
429 // and mem copies.
430 records: Vec::with_capacity(INITIAL_RECORDS_CAPACITY),
431 regex,
432 mount_info,
433 tracer_configs: Some(tracer_configs),
434 page_size: page_size()? as u64,
435 debug_info: DebugInfo {
436 missing_files: HashMap::new(),
437 directory_read_bytes: 0,
438 missing_path_bytes: 0,
439 privileged_paths: vec![],
440 },
441 })
442 }
443
new_file_id(&mut self) -> FileId444 fn new_file_id(&mut self) -> FileId {
445 let id = self.inode_count;
446 self.inode_count += 1;
447 FileId(id)
448 }
449
get_trace_info(&self, line: &str) -> Result<Option<TraceLineInfo>, Error>450 fn get_trace_info(&self, line: &str) -> Result<Option<TraceLineInfo>, Error> {
451 TraceLineInfo::from_trace_line(&self.regex, line)
452 }
453
454 // Returns true if the file or directory is on a device which is excluded from walking.
455 // If the path was excluded because the current process doesn't have privileged to read it,
456 // the path gets added to `privileged` list.
is_excluded(&self, entry: &DirEntry, device: u64, privileged: &mut Vec<PathBuf>) -> bool457 fn is_excluded(&self, entry: &DirEntry, device: u64, privileged: &mut Vec<PathBuf>) -> bool {
458 // We skip paths that are reside on excluded devices here. This is ok because a
459 // non-excluded mount point will have a separate entry in MountInfo. For example
460 // - `/` has ext4
461 // - `/tmp` has tempfs
462 // - `/tmp/mnt` has ext4 that we are interested in.
463 // MountInfo will have three entries - `/`, `/tmp/` and `/tmp/mnt`. Skipping walking
464 // `/tmp` while walking `/` is ok as next `mount_info.get_included()` will return
465 // `/tmp/mnt` path.
466 //
467 //
468 // We skip links here as they can refer to mount points across
469 // filesystems. If that path is valid and access are valid, then
470 // we should have entry by the file's <device, inode> pair.
471 //
472 //
473 // We skip devices that don't match current walking device because we eventually
474 // walk other devices.
475 match symlink_metadata(entry.path()) {
476 Ok(lstat) => {
477 if self.mount_info.is_excluded(&lstat.dev())
478 || lstat.dev() != device
479 || lstat.file_type().is_symlink()
480 {
481 return true;
482 }
483 }
484 Err(e) => {
485 error!("stat on {} failed with {}", entry.path().to_str().unwrap(), e);
486
487 // We treat EACCES special because on some platforms, like android, process needs to
488 // have very special set of permissions to access some inodes.
489 // We ignore errors in such cases *after* making an effort to get to them.
490 if e.kind() == ErrorKind::PermissionDenied
491 && is_highly_privileged_path(entry.path())
492 {
493 privileged.push(entry.path().to_owned());
494 return true;
495 }
496 }
497 }
498
499 // On error, we return false because if lstat has failed, it will fail following operations
500 // including stat.
501 false
502 }
503 }
504
505 impl TraceSubsystem for MemTraceSubsystem {
add_line(&mut self, line: &str) -> Result<(), Error>506 fn add_line(&mut self, line: &str) -> Result<(), Error> {
507 if let Some(info) = self.get_trace_info(line)? {
508 if self.mount_info.is_excluded(&info.device) {
509 return Ok(());
510 }
511
512 self.device_inode_map.entry(info.device).or_default();
513
514 let file_id = if let Some(id) =
515 self.device_inode_map.get_mut(&info.device).unwrap().get(&info.inode)
516 {
517 id.clone()
518 } else {
519 self.new_file_id()
520 };
521 self.device_inode_map
522 .get_mut(&info.device)
523 .unwrap()
524 .insert(info.inode, file_id.clone());
525
526 self.records.push(Record {
527 file_id,
528 offset: info.offset,
529 length: self.page_size,
530 timestamp: info.timestamp,
531 });
532 }
533
534 Ok(())
535 }
536
build_records_file(&mut self) -> Result<RecordsFile, Error>537 fn build_records_file(&mut self) -> Result<RecordsFile, Error> {
538 // reset debug_info in case build_records_file was called twice.
539 self.debug_info = DebugInfo::default();
540 let mut rf = RecordsFile::default();
541 let mut directories = HashSet::new();
542
543 // TODO(b/302194377): We are holding all privileged_paths in this variable and then
544 // transferring it to `self.debug_info.privileged_paths` later. We can avoid this step
545 // if we directly update `self.debug_info.privileged_paths`. To do so, we need to refactor
546 // code to make borrow not complain at several places - ex. immutably borrowing
547 // `self.mount_info` in outer loop and then mutably borrowing
548 // `self.debug_info.privileged_paths`.
549 let mut privileged_paths = vec![];
550
551 // Reload mount_info. When we created mount_info for the first time, maybe
552 // the system was in early boot phase. Reload the mount_info so as to get
553 // current/new mount points.
554 if let Some(tracer_config) = &self.tracer_configs {
555 self.mount_info = MountInfo::create(tracer_config.mountinfo_path.as_ref().unwrap())?;
556 debug!("reloaded mountinfo: {:#?}", self.mount_info);
557 }
558
559 for (device, root_path) in self.mount_info.get_included() {
560 let inode_map = if let Some(map) = self.device_inode_map.get(device) {
561 map
562 } else {
563 continue;
564 };
565
566 if inode_map.is_empty() {
567 return Err(Error::Custom {
568 error: format!("Unexpected empty records for {:?}", root_path),
569 });
570 }
571
572 let mut block_size = 0;
573 let walker = WalkDir::new(root_path).into_iter();
574
575 for entry in
576 walker.filter_entry(|e| !self.is_excluded(e, *device, &mut privileged_paths))
577 {
578 let path = match entry {
579 Ok(entry) => entry.path().to_owned(),
580 Err(e) => {
581 error!("walking directory failed: {} {}", root_path.to_str().unwrap(), e);
582 continue;
583 }
584 };
585
586 let stat = match path.metadata() {
587 Ok(stat) => stat,
588 Err(e) => {
589 error!("stat on {} failed with {}", path.to_str().unwrap(), e);
590 continue;
591 }
592 };
593
594 block_size = stat.blksize();
595
596 let file_id = if let Some(id) = inode_map.get(&stat.ino()) {
597 id.clone()
598 } else {
599 continue;
600 };
601
602 // We cannot issue a normal readahead on directories. So we skip those records that
603 // belong to directories.
604 if stat.file_type().is_dir() {
605 info!(
606 "skipping directory readahead record for file_id:{file_id} ino:{} path:{} ",
607 stat.ino(),
608 path.to_str().unwrap()
609 );
610 directories.insert(file_id.clone());
611 continue;
612 }
613
614 rf.insert_or_update_inode(file_id, &stat, path.to_str().unwrap().to_owned());
615 }
616
617 rf.inner.filesystems.insert(*device, FsInfo { block_size });
618 }
619
620 self.debug_info.privileged_paths.append(&mut privileged_paths);
621
622 for (device, inode_map) in &self.device_inode_map {
623 for (inode, file_id) in inode_map {
624 if !rf.inner.inode_map.contains_key(file_id) {
625 let major_no: MajorMinorType = major(*device);
626 let minor_no: MajorMinorType = minor(*device);
627 self.debug_info.missing_files.insert(
628 file_id.clone(),
629 MissingFile { major_no, minor_no, inode: *inode, records: vec![] },
630 );
631 }
632 }
633 }
634
635 // Remove all records that belong to directories or for which we did not find paths.
636 let mut records = vec![];
637 for record in take(&mut self.records) {
638 if directories.contains(&record.file_id) {
639 self.debug_info.directory_read_bytes += record.length;
640 } else if let Some(missing_file) =
641 self.debug_info.missing_files.get_mut(&record.file_id)
642 {
643 self.debug_info.missing_path_bytes += record.length;
644 missing_file.records.push(record);
645 } else {
646 records.push(record);
647 }
648 }
649
650 warn!(
651 "Recorded {} bytes worth of data read from directories",
652 self.debug_info.directory_read_bytes
653 );
654 warn!(
655 "Recorded {} bytes worth of data read from files that don't have paths",
656 self.debug_info.missing_path_bytes
657 );
658
659 rf.inner.records = coalesce_records(records, true);
660
661 Ok(rf)
662 }
663
serialize(&self, write: &mut dyn Write) -> Result<(), Error>664 fn serialize(&self, write: &mut dyn Write) -> Result<(), Error> {
665 write
666 .write_all(
667 &serde_json::to_vec(&self)
668 .map_err(|e| Error::Serialize { error: e.to_string() })?,
669 )
670 .map_err(|source| Error::Write { path: "intermediate file".to_owned(), source })
671 }
672 }
673
674 #[cfg(test)]
675 mod tests {
676 use nix::sys::stat::{major, minor};
677 use std::assert_eq;
678 use std::path::Path;
679
680 use crate::tracer::tests::{copy_uncached_files_and_record_from, setup_test_dir};
681
682 use crate::replay::tests::generate_cached_files_and_record;
683
684 use super::*;
685
686 static TRACE_BUFFER: &str = concat!(
687 // kernel 5.x
688 " Settingide-502 [001] .... 484.360292: mm_filemap_add_to_page_CACHE: dev 254:6 ino cf1 page=68d477 pfn=59833 ofs=32768\n",
689 " Settingide-502 [001] .... 484.360311: mm_filemap_add_to_page_cache: dev 254:6 ino cf1 page=759458 pfn=59827 ofs=57344\n",
690 " BOX_ENTDED-3071 [001] .... 485.276715: mm_filemap_add_to_pag_ecache: dev 254:6 ino 1 page=00cc1c pfn=81748 ofs=13574144\n",
691 " BOX_ENTDED-3071 [001] .... 485.276990: mm_filemap_add_to_page_cache: dev 254:6 ino cf2 page=36540b pfn=60952 ofs=0\n",
692 " .gms.peent-843 [001] .... 485.545516: mm_filemap_add_to_page_cache: dev 254:6 ino 1 page=002e8b pfn=58928 ofs=13578240\n",
693 " .gms.peent-843 [001] .... 485.545820: mm_filemap_add_to_page_cache: dev 254:6 ino cf3 page=6233ce pfn=58108 ofs=0\n",
694 " an.bg-459 [001] .... 494.029396: mm_filemap_add_to_page_cache: dev 254:3 ino 7cf page=c5b5c7 pfn=373933 ofs=1310720\n",
695 " an.bg-459 [001] .... 494.029398: mm_filemap_add_to_page_cache: dev 254:3 ino 7cf page=b8b9ec pfn=410074 ofs=1314816\n",
696
697 // kernel 6.x
698 " logcat-686 [006] ..... 148216.040320: mm_filemap_add_to_page_CACHE: dev 254:85 ino 3f15 pfn=0x213bc2 ofs=528384 order=0\n",
699 " logcat-686 [001] ..... 148217.776227: mm_filemap_add_to_page_cache: dev 254:85 ino 3f15 pfn=0x21d306 ofs=532480 order=0\n",
700 " logcat-686 [003] ..... 148219.044389: mm_filemap_add_to_pag_ecache: dev 254:85 ino 3f15 pfn=0x224b8d ofs=536576 order=0\n",
701 " logcat-686 [001] ..... 148220.780964: mm_filemap_add_to_page_cache: dev 254:85 ino 3f15 pfn=0x1bfe0a ofs=540672 order=0\n",
702 " logcat-686 [001] ..... 148223.046560: mm_filemap_add_to_page_cache: dev 254:85 ino 3f15 pfn=0x1f3d29 ofs=544768 order=0",
703 );
704
sample_mem_traces() -> (String, Vec<Option<TraceLineInfo>>)705 fn sample_mem_traces() -> (String, Vec<Option<TraceLineInfo>>) {
706 (
707 TRACE_BUFFER.to_owned(),
708 vec![
709 // 5.x
710 None,
711 Some(TraceLineInfo::from_fields(254, 6, 0xcf1, 57344, 484360311000)),
712 None,
713 Some(TraceLineInfo::from_fields(254, 6, 0xcf2, 0, 485276990000)),
714 Some(TraceLineInfo::from_fields(254, 6, 0x1, 13578240, 485545516000)),
715 Some(TraceLineInfo::from_fields(254, 6, 0xcf3, 0, 485545820000)),
716 Some(TraceLineInfo::from_fields(254, 3, 0x7cf, 1310720, 494029396000)),
717 Some(TraceLineInfo::from_fields(254, 3, 0x7cf, 1314816, 494029398000)),
718 // 6.x
719 None,
720 Some(TraceLineInfo::from_fields(254, 85, 0x3f15, 532480, 148217776227000)),
721 None,
722 Some(TraceLineInfo::from_fields(254, 85, 0x3f15, 540672, 148220780964000)),
723 Some(TraceLineInfo::from_fields(254, 85, 0x3f15, 544768, 148223046560000)),
724 ],
725 )
726 }
727
728 #[test]
test_parse_trace_line()729 fn test_parse_trace_line() {
730 let (buf, res) = sample_mem_traces();
731 let re = TraceLineInfo::get_trace_line_regex().unwrap();
732 for (index, line) in buf.lines().enumerate() {
733 let found = TraceLineInfo::from_trace_line(&re, line).unwrap();
734 let expected = res.get(index).unwrap();
735 assert_eq!(found.is_some(), expected.is_some());
736 if found.is_some() {
737 assert_eq!(found.unwrap(), *expected.as_ref().unwrap());
738 }
739 }
740 }
741
742 #[test]
test_add_line()743 fn test_add_line() {
744 let test_base_dir = setup_test_dir();
745 let (rf, mut files) =
746 generate_cached_files_and_record(None, true, Some(page_size().unwrap() as u64));
747 let (_uncached_rf, uncached_files) =
748 copy_uncached_files_and_record_from(Path::new(&test_base_dir), &mut files, &rf);
749 let mut mount_include = HashMap::new();
750
751 let included_dev = uncached_files.get(0).unwrap().0.metadata().unwrap().dev();
752 let included_inode1 = uncached_files.get(0).unwrap().0.metadata().unwrap().ino();
753 let included_inode2 = uncached_files.get(1).unwrap().0.metadata().unwrap().ino();
754 let included_major = major(included_dev);
755 let included_minor = minor(included_dev);
756 mount_include.insert(included_dev, std::fs::canonicalize(test_base_dir).unwrap());
757 let mut mount_exclude = HashSet::new();
758 mount_exclude.insert(0);
759
760 let mut mem_tracer = MemTraceSubsystem {
761 device_inode_map: HashMap::new(),
762 inode_count: 0,
763 records: vec![],
764 regex: TraceLineInfo::get_trace_line_regex().unwrap(),
765 mount_info: MountInfo {
766 included_devices: mount_include,
767 excluded_devices: mount_exclude,
768 },
769 tracer_configs: None,
770 page_size: page_size().unwrap() as u64,
771 debug_info: DebugInfo {
772 missing_files: HashMap::new(),
773 directory_read_bytes: 0,
774 missing_path_bytes: 0,
775 privileged_paths: vec![],
776 },
777 };
778
779 let pg_size = page_size().unwrap();
780 // Format is major, minor, inode, offset
781 let inputs = vec![
782 (0, 0, 2, 10), // to be excluded. bad device.
783 (included_major, included_minor, included_inode1, 0),
784 (included_major, included_minor, included_inode1, 3 * pg_size),
785 // duplicate read
786 (included_major, included_minor, included_inode1, 3 * pg_size),
787 (0, 0, included_inode1, 10), // to be excluded. bad device.
788 (included_major, included_minor, included_inode1, 2 * pg_size), // contiguous
789 // non-contiguous
790 (included_major, included_minor, included_inode1, 12 * pg_size),
791 // same offset different inode
792 (included_major, included_minor, included_inode2, 3 * pg_size),
793 // Contiguous offset different inode
794 (included_major, included_minor, included_inode2, pg_size),
795 ];
796
797 for (i, (major, minor, inode, offset)) in inputs.iter().enumerate() {
798 // used to timestamp the log line.
799 let seconds = i;
800 // used to timestamp the log line.
801 let microseconds = i;
802 for operation in &["mm_filemap_add_to_page_cache", "some_other_operation"] {
803 let line = format!(
804 " BOX_ENTRY_ADDED-3071 [001] .... {}.{}: {}: \
805 dev {}:{} ino {:x} page=00000000f936540b pfn=60952 ofs={}",
806 seconds, microseconds, operation, major, minor, inode, offset
807 );
808 mem_tracer.add_line(&line).unwrap();
809 }
810 }
811 assert_eq!(mem_tracer.records.len(), 7);
812 assert_eq!(mem_tracer.device_inode_map.len(), 1);
813 assert_eq!(mem_tracer.device_inode_map.get(&included_dev).unwrap().len(), 2);
814 assert!(mem_tracer
815 .device_inode_map
816 .get(&included_dev)
817 .unwrap()
818 .contains_key(&included_inode1));
819 assert!(mem_tracer
820 .device_inode_map
821 .get(&included_dev)
822 .unwrap()
823 .contains_key(&included_inode2));
824 }
825
new_record(file: u64, offset: u64, length: u64, timestamp: u64) -> Record826 fn new_record(file: u64, offset: u64, length: u64, timestamp: u64) -> Record {
827 Record { file_id: FileId(file), offset, length, timestamp }
828 }
829
830 #[test]
test_get_records_file()831 fn test_get_records_file() {
832 let test_base_dir = setup_test_dir();
833 let (rf, mut files) =
834 generate_cached_files_and_record(None, true, Some(page_size().unwrap() as u64));
835 let (_uncached_rf, uncached_files) =
836 copy_uncached_files_and_record_from(Path::new(&test_base_dir), &mut files, &rf);
837 let mut mount_include = HashMap::new();
838
839 let included_dev = uncached_files.get(0).unwrap().0.metadata().unwrap().dev();
840 let included_inode1 = uncached_files.get(0).unwrap().0.metadata().unwrap().ino();
841 let included_inode2 = uncached_files.get(1).unwrap().0.metadata().unwrap().ino();
842 let included_major = major(included_dev);
843 let included_minor = minor(included_dev);
844 mount_include.insert(included_dev, std::fs::canonicalize(test_base_dir).unwrap());
845 let mut mount_exclude = HashSet::new();
846 mount_exclude.insert(0);
847
848 let mut mem_tracer = MemTraceSubsystem {
849 device_inode_map: HashMap::new(),
850 inode_count: 0,
851 records: vec![],
852 regex: TraceLineInfo::get_trace_line_regex().unwrap(),
853 mount_info: MountInfo {
854 included_devices: mount_include,
855 excluded_devices: mount_exclude,
856 },
857 tracer_configs: None,
858 page_size: page_size().unwrap() as u64,
859 debug_info: DebugInfo {
860 missing_files: HashMap::new(),
861 directory_read_bytes: 0,
862 missing_path_bytes: 0,
863 privileged_paths: vec![],
864 },
865 };
866
867 let pg_size = page_size().unwrap() as u64;
868 // Format is major, minor, inode, offset
869 let inputs = vec![
870 (0, 0, 2, 10), // to be excluded. bad device.
871 (included_major, included_minor, included_inode1, 0),
872 (included_major, included_minor, included_inode1, 3 * pg_size),
873 // duplicate read
874 (included_major, included_minor, included_inode1, 3 * pg_size),
875 (0, 0, included_inode1, 10), // to be excluded. bad device.
876 (included_major, included_minor, included_inode1, 2 * pg_size), // contiguous
877 // non-contiguous
878 (included_major, included_minor, included_inode1, 12 * pg_size),
879 // same offset different inode
880 (included_major, included_minor, included_inode2, 3 * pg_size),
881 // Contiguous offset different inode
882 (included_major, included_minor, included_inode2, pg_size),
883 ];
884
885 for (i, (major, minor, inode, offset)) in inputs.iter().enumerate() {
886 // used to timestamp the log line.
887 let seconds = i;
888 // used to timestamp the log line.
889 let microseconds = i;
890 for operation in &["mm_filemap_add_to_page_cache", "some_other_operation"] {
891 let line = format!(
892 " BOX_ENTRY_ADDED-3071 [001] .... {}.{}: {}: \
893 dev {}:{} ino {:x} page=00000000f936540b pfn=60952 ofs={}",
894 seconds, microseconds, operation, major, minor, inode, offset
895 );
896 mem_tracer.add_line(&line).unwrap();
897 }
898 }
899 let rf = mem_tracer.build_records_file().unwrap();
900 assert_eq!(
901 rf.inner.records,
902 vec![
903 new_record(0, 0, pg_size, 1000001000),
904 new_record(0, 2 * pg_size, 2 * pg_size, 2000002000),
905 new_record(0, 12 * pg_size, pg_size, 6000006000),
906 new_record(1, pg_size, pg_size, 8000008000),
907 new_record(1, 3 * pg_size, pg_size, 7000007000),
908 ]
909 );
910 }
911 }
912