use lightswitch_object::ExecutableId;
use std::collections::BinaryHeap;
use std::fs;
use std::io::BufWriter;
use std::io::ErrorKind;
use std::io::Read;
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
use std::time::Instant;
use std::{fs::File, io::BufReader};
use thiserror::Error;
use tracing::debug;
use super::persist::{Reader, Writer};
use crate::unwind_info::persist::{ReaderError, WriterError};
use crate::unwind_info::types::CompactUnwindRow;
const DEFAULT_MAX_CACHED_FILES: usize = 1_000;
#[derive(Debug, PartialEq, Eq)]
struct Usage {
executable_id: ExecutableId,
instant: Instant,
}
impl PartialOrd for Usage {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Usage {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
other.instant.cmp(&self.instant)
}
}
#[derive(Debug, Error)]
pub enum FetchUnwindInfoError {
#[error("not found in cache")]
NotFound,
#[error("i/o error")]
Io(#[from] std::io::Error),
#[error("reader error")]
Reader(#[from] ReaderError),
#[error("generic error {0}")]
UnwindInfoGeneric(String),
#[error("write error")]
Write(#[from] WriterError),
}
pub struct UnwindInfoManager {
cache_dir: PathBuf,
usage_tracking: BinaryHeap<Usage>,
max_cached_files: usize,
}
impl UnwindInfoManager {
pub fn new(cache_dir: &Path, max_cached_files: Option<usize>) -> Self {
let max_cached_files = max_cached_files.unwrap_or(DEFAULT_MAX_CACHED_FILES);
debug!(
"Storing unwind information cache in {}",
cache_dir.display()
);
let mut manager = UnwindInfoManager {
cache_dir: cache_dir.to_path_buf(),
usage_tracking: BinaryHeap::with_capacity(max_cached_files),
max_cached_files,
};
let _ = manager.bump_already_present();
manager
}
pub fn fetch_unwind_info(
&mut self,
executable_path: &Path,
executable_id: ExecutableId,
first_frame_override: Option<(u64, u64)>,
check_digest: bool,
) -> Result<Vec<CompactUnwindRow>, FetchUnwindInfoError> {
match self.read_from_cache(executable_id, check_digest) {
Ok(unwind_info) => Ok(unwind_info),
Err(e) => {
if matches!(e, FetchUnwindInfoError::NotFound) {
debug!("error fetch_unwind_info: {:?}, regenerating...", e);
}
let unwind_info =
self.write_to_cache(executable_path, executable_id, first_frame_override);
if unwind_info.is_ok() {
self.bump(executable_id, None);
}
unwind_info
}
}
}
fn read_from_cache(
&self,
executable_id: ExecutableId,
check_digest: bool,
) -> Result<Vec<CompactUnwindRow>, FetchUnwindInfoError> {
let unwind_info_path = self.path_for(executable_id);
let file = File::open(unwind_info_path).map_err(|e| {
if e.kind() == ErrorKind::NotFound {
FetchUnwindInfoError::NotFound
} else {
FetchUnwindInfoError::Io(e)
}
})?;
let mut buffer = BufReader::new(file);
let mut data = Vec::new();
buffer.read_to_end(&mut data)?;
let reader = Reader::new(&data, check_digest).map_err(FetchUnwindInfoError::Reader)?;
Ok(reader.unwind_info()?)
}
fn write_to_cache(
&self,
executable_path: &Path,
executable_id: ExecutableId,
first_frame_override: Option<(u64, u64)>,
) -> Result<Vec<CompactUnwindRow>, FetchUnwindInfoError> {
let unwind_info_path = self.path_for(executable_id);
let unwind_info_writer = Writer::new(executable_path, first_frame_override);
let mut file =
BufWriter::new(File::create(unwind_info_path).map_err(FetchUnwindInfoError::Io)?);
unwind_info_writer
.write(&mut file)
.map_err(FetchUnwindInfoError::Write)
}
fn path_for(&self, executable_id: ExecutableId) -> PathBuf {
self.cache_dir.join(format!("{executable_id}"))
}
pub fn bump_already_present(&mut self) -> anyhow::Result<()> {
for direntry in fs::read_dir(&self.cache_dir)?.flatten() {
let name = direntry.file_name();
let Some(name) = name.to_str() else { continue };
let executable_id = ExecutableId::from_str(name)?;
let metadata = fs::metadata(direntry.path())?;
let modified = metadata.created()?;
self.bump(executable_id, Some(Instant::now() - modified.elapsed()?));
}
Ok(())
}
fn bump(&mut self, executable_id: ExecutableId, instant: Option<Instant>) {
let instant = instant.unwrap_or(Instant::now());
self.usage_tracking.push(Usage {
executable_id,
instant,
});
self.maybe_evict()
}
fn maybe_evict(&mut self) {
if self.usage_tracking.len() > self.max_cached_files {
if let Some(evict) = self.usage_tracking.pop() {
let _ = fs::remove_file(self.path_for(evict.executable_id));
}
}
}
}
#[cfg(test)]
mod tests {
use std::{
io::{Seek, SeekFrom, Write},
path::PathBuf,
time::Duration,
};
use fs::OpenOptions;
use super::*;
use crate::unwind_info::compact_unwind_info;
#[test]
fn test_custom_usage_ordering() {
let now = Instant::now();
let before = Usage {
executable_id: ExecutableId(0xBAD),
instant: now,
};
let after = Usage {
executable_id: ExecutableId(0xFAD),
instant: now + Duration::from_secs(10),
};
assert_eq!(
[&before, &after].iter().max().unwrap().executable_id,
ExecutableId(0xBAD)
);
assert_eq!(before.cmp(&after), before.partial_cmp(&after).unwrap());
}
#[test]
fn test_unwind_info_manager_unwind_info() {
let unwind_info = compact_unwind_info("/proc/self/exe", None).unwrap();
let tmpdir = tempfile::TempDir::new().unwrap();
let mut manager = UnwindInfoManager::new(tmpdir.path(), None);
for _ in 0..2 {
let manager_unwind_info = manager.fetch_unwind_info(
&PathBuf::from("/proc/self/exe"),
ExecutableId(0xFABADA),
None,
true,
);
let manager_unwind_info = manager_unwind_info.unwrap();
assert_eq!(unwind_info, manager_unwind_info);
}
}
#[test]
fn test_unwind_info_manager_corrupt() {
let unwind_info = compact_unwind_info("/proc/self/exe", None).unwrap();
let tmpdir = tempfile::TempDir::new().unwrap();
let mut manager = UnwindInfoManager::new(tmpdir.path(), None);
let manager_unwind_info = manager.fetch_unwind_info(
&PathBuf::from("/proc/self/exe"),
ExecutableId(0xFABADA),
None,
true,
);
assert!(manager_unwind_info.is_ok());
let manager_unwind_info = manager_unwind_info.unwrap();
assert_eq!(unwind_info, manager_unwind_info);
let mut file = OpenOptions::new()
.write(true)
.open(tmpdir.path().join(format!("{:x}", 0xFABADA)))
.unwrap();
file.seek(SeekFrom::End(-20)).unwrap();
file.write_all(&[0; 20]).unwrap();
let manager_unwind_info = manager.fetch_unwind_info(
&PathBuf::from("/proc/self/exe"),
ExecutableId(0xFABADA),
None,
true,
);
let manager_unwind_info = manager_unwind_info.unwrap();
assert_eq!(unwind_info, manager_unwind_info);
}
#[test]
fn test_unwind_info_manager_cache_eviction() {
let tmpdir = tempfile::TempDir::new().unwrap();
let path = tmpdir.path();
for i in 0..20 {
File::create(path.join(format!("{i:x}"))).unwrap();
}
assert_eq!(fs::read_dir(path).unwrap().collect::<Vec<_>>().len(), 20);
UnwindInfoManager::new(path, Some(4));
assert_eq!(fs::read_dir(path).unwrap().collect::<Vec<_>>().len(), 4);
}
}