Skip to main content

ghostscope_dwarf/
loader.rs

1//! Module loading with Builder pattern and parallel support
2
3use crate::{
4    analyzer::{ModuleLoadingEvent, ModuleLoadingStats},
5    core::{mapping::ModuleMapping, Result},
6    objfile::LoadedObjfile,
7};
8use ghostscope_debuginfod::DebuginfodClient;
9use std::ffi::OsStr;
10use std::os::unix::fs::MetadataExt;
11use std::path::{Component, Path, PathBuf};
12use std::sync::Arc;
13use tokio::task;
14
15/// A user-provided debug file bound to one loaded module.
16#[derive(Debug, Clone)]
17pub struct ExplicitDebugFile {
18    pub target_module: PathBuf,
19    pub debug_file: PathBuf,
20}
21
22impl ExplicitDebugFile {
23    pub fn new(target_module: PathBuf, debug_file: PathBuf) -> Self {
24        Self {
25            target_module,
26            debug_file,
27        }
28    }
29
30    fn matches_module(&self, module_path: &Path) -> bool {
31        paths_equivalent(module_path, &self.target_module)
32    }
33}
34
35/// Configuration for module loading (parallel only)
36#[derive(Debug, Clone)]
37pub struct LoadConfig {
38    /// Maximum number of concurrent module loads
39    pub max_module_concurrency: usize,
40    /// Debug file search paths (for .gnu_debuglink)
41    pub debug_search_paths: Vec<String>,
42    /// Allow non-strict debug file matching (CRC/Build-ID)
43    pub allow_loose_debug_match: bool,
44    /// Optional user-provided debug file for one target module.
45    pub explicit_debug_file: Option<ExplicitDebugFile>,
46    /// Optional debuginfod client for build-id based debug file lookup.
47    pub debuginfod_client: Option<Arc<DebuginfodClient>>,
48}
49
50impl Default for LoadConfig {
51    fn default() -> Self {
52        Self {
53            max_module_concurrency: num_cpus::get(),
54            debug_search_paths: Vec::new(),
55            allow_loose_debug_match: false,
56            explicit_debug_file: None,
57            debuginfod_client: None,
58        }
59    }
60}
61
62impl LoadConfig {
63    /// Fast loading with maximum concurrency
64    pub fn fast() -> Self {
65        Self {
66            max_module_concurrency: num_cpus::get(),
67            debug_search_paths: Vec::new(),
68            allow_loose_debug_match: false,
69            explicit_debug_file: None,
70            debuginfod_client: None,
71        }
72    }
73}
74
75/// Builder for loading modules with flexible parallelism options
76pub struct ModuleLoader {
77    mappings: Vec<ModuleMapping>,
78    config: LoadConfig,
79}
80
81impl ModuleLoader {
82    /// Create a new loader with given module mappings
83    pub fn new(mappings: Vec<ModuleMapping>) -> Self {
84        Self {
85            mappings,
86            config: LoadConfig::default(),
87        }
88    }
89
90    /// Use predefined parallel configuration
91    pub fn parallel(mut self) -> Self {
92        self.config = LoadConfig::fast();
93        self
94    }
95
96    /// Set debug search paths for .gnu_debuglink files
97    pub fn with_debug_search_paths(mut self, paths: Vec<String>) -> Self {
98        self.config.debug_search_paths = paths;
99        self
100    }
101
102    /// Set loose debug match policy (CRC/Build-ID mismatches allowed)
103    pub fn with_loose_debug_match(mut self, allow: bool) -> Self {
104        self.config.allow_loose_debug_match = allow;
105        self
106    }
107
108    /// Set a user-provided debug file for one target module.
109    pub fn with_explicit_debug_file(mut self, debug_file: Option<ExplicitDebugFile>) -> Self {
110        self.config.explicit_debug_file = debug_file;
111        self
112    }
113
114    /// Set optional debuginfod client for build-id based debug file fallback.
115    pub fn with_debuginfod_client(mut self, client: Option<Arc<DebuginfodClient>>) -> Self {
116        self.config.debuginfod_client = client;
117        self
118    }
119
120    /// Load with progress callback - always parallel
121    pub async fn load_with_progress<F>(self, progress_callback: F) -> Result<Vec<LoadedObjfile>>
122    where
123        F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
124    {
125        self.load_modules_parallel_with_progress(progress_callback)
126            .await
127    }
128
129    /// Add progress callback (method chaining convenience)
130    pub fn with_progress_callback<F>(self, progress_callback: F) -> ModuleLoaderWithCallback<F>
131    where
132        F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
133    {
134        ModuleLoaderWithCallback {
135            loader: self,
136            callback: progress_callback,
137        }
138    }
139
140    /// Load modules in parallel with progress tracking
141    async fn load_modules_parallel_with_progress<F>(
142        self,
143        progress_callback: F,
144    ) -> Result<Vec<LoadedObjfile>>
145    where
146        F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
147    {
148        // Use semaphore to limit concurrency
149        let semaphore = Arc::new(tokio::sync::Semaphore::new(
150            self.config.max_module_concurrency,
151        ));
152
153        let total_modules = self.mappings.len();
154        if let Some(explicit) = self.config.explicit_debug_file.as_ref() {
155            validate_explicit_debug_file_target(&self.mappings, explicit)?;
156        }
157        let progress_callback = Arc::new(progress_callback);
158        let debug_search_paths = Arc::new(self.config.debug_search_paths.clone());
159        let allow_loose = self.config.allow_loose_debug_match;
160        let explicit_debug_file = self.config.explicit_debug_file.clone();
161        let debuginfod_client = self.config.debuginfod_client.clone();
162
163        let tasks: Vec<_> = self
164            .mappings
165            .into_iter()
166            .enumerate()
167            .map(|(index, mapping)| {
168                let semaphore = semaphore.clone();
169                let progress_callback = progress_callback.clone();
170                let debug_search_paths = debug_search_paths.clone();
171                let debuginfod_client = debuginfod_client.clone();
172                let explicit_debug_file_for_module =
173                    explicit_debug_file.as_ref().and_then(|explicit| {
174                        explicit
175                            .matches_module(&mapping.path)
176                            .then(|| explicit.debug_file.clone())
177                    });
178
179                task::spawn(async move {
180                    let _permit = semaphore.acquire().await.unwrap();
181
182                    let module_path = mapping.path.to_string_lossy().to_string();
183
184                    // Notify loading started
185                    progress_callback(ModuleLoadingEvent::LoadingStarted {
186                        module_path: module_path.clone(),
187                        current: index + 1,
188                        total: total_modules,
189                    });
190
191                    let start_time = std::time::Instant::now();
192
193                    let result = LoadedObjfile::load_parallel(
194                        mapping,
195                        &debug_search_paths,
196                        allow_loose,
197                        explicit_debug_file_for_module,
198                        debuginfod_client,
199                    )
200                    .await;
201
202                    let load_time_ms = start_time.elapsed().as_millis() as u64;
203
204                    match result {
205                        Ok(module) => {
206                            // Extract stats for progress reporting
207                            let (functions, variables, types) =
208                                module.get_lightweight_index().get_stats();
209                            let (parse_time_ms, index_time_ms, module_total_time_ms) =
210                                module.get_load_timing_ms();
211                            let stats = ModuleLoadingStats {
212                                functions,
213                                variables,
214                                types,
215                                debug_info_source: module.get_debug_info_source().clone(),
216                                load_time_ms,
217                                parse_time_ms,
218                                index_time_ms,
219                                module_total_time_ms,
220                            };
221
222                            progress_callback(ModuleLoadingEvent::LoadingCompleted {
223                                module_path,
224                                stats,
225                                current: index + 1,
226                                total: total_modules,
227                            });
228
229                            Ok(module)
230                        }
231                        Err(e) => {
232                            progress_callback(ModuleLoadingEvent::LoadingFailed {
233                                module_path,
234                                error: e.to_string(),
235                                current: index + 1,
236                                total: total_modules,
237                            });
238                            Err(e)
239                        }
240                    }
241                })
242            })
243            .collect();
244
245        let results = futures::future::try_join_all(tasks).await?;
246        let modules: Result<Vec<_>> = results.into_iter().collect();
247        modules
248    }
249}
250
251fn validate_explicit_debug_file_target(
252    mappings: &[ModuleMapping],
253    explicit: &ExplicitDebugFile,
254) -> Result<()> {
255    let matches: Vec<&ModuleMapping> = mappings
256        .iter()
257        .filter(|mapping| explicit.matches_module(&mapping.path))
258        .collect();
259
260    match matches.len() {
261        1 => Ok(()),
262        0 => {
263            let sample = mappings
264                .iter()
265                .take(8)
266                .map(|mapping| mapping.path.display().to_string())
267                .collect::<Vec<_>>()
268                .join("\n  - ");
269            Err(anyhow::anyhow!(
270                "Explicit debug file {} was provided for target module {}, but that module was not loaded. Loaded modules include:\n  - {}",
271                explicit.debug_file.display(),
272                explicit.target_module.display(),
273                sample
274            ))
275        }
276        _ => {
277            let sample = matches
278                .iter()
279                .take(8)
280                .map(|mapping| mapping.path.display().to_string())
281                .collect::<Vec<_>>()
282                .join("\n  - ");
283            Err(anyhow::anyhow!(
284                "Explicit debug file {} target {} matched multiple loaded modules:\n  - {}",
285                explicit.debug_file.display(),
286                explicit.target_module.display(),
287                sample
288            ))
289        }
290    }
291}
292
293fn paths_equivalent(left: &Path, right: &Path) -> bool {
294    if left == right {
295        return true;
296    }
297
298    if proc_root_paths_equivalent(left, right) {
299        return true;
300    }
301
302    if let (Ok(left), Ok(right)) = (left.canonicalize(), right.canonicalize()) {
303        if left == right {
304            return true;
305        }
306    }
307
308    match (std::fs::metadata(left), std::fs::metadata(right)) {
309        (Ok(left), Ok(right)) => left.dev() == right.dev() && left.ino() == right.ino(),
310        _ => false,
311    }
312}
313
314fn proc_root_paths_equivalent(left: &Path, right: &Path) -> bool {
315    match (strip_proc_root_prefix(left), strip_proc_root_prefix(right)) {
316        (Some(left), Some(right)) => left == right,
317        (Some(left), None) => left.as_path() == right,
318        (None, Some(right)) => left == right.as_path(),
319        (None, None) => false,
320    }
321}
322
323fn strip_proc_root_prefix(path: &Path) -> Option<PathBuf> {
324    let mut components = path.components();
325    if !matches!(components.next(), Some(Component::RootDir)) {
326        return None;
327    }
328    if !matches!(
329        components.next(),
330        Some(Component::Normal(component)) if component == OsStr::new("proc")
331    ) {
332        return None;
333    }
334    if !matches!(
335        components.next(),
336        Some(Component::Normal(pid)) if pid.to_string_lossy().parse::<u32>().is_ok()
337    ) {
338        return None;
339    }
340    if !matches!(
341        components.next(),
342        Some(Component::Normal(component)) if component == OsStr::new("root")
343    ) {
344        return None;
345    }
346
347    let remaining = components.as_path();
348    let mut stripped = PathBuf::from("/");
349    if !remaining.as_os_str().is_empty() {
350        stripped.push(remaining);
351    }
352    Some(stripped)
353}
354
355/// ModuleLoader with attached progress callback (for method chaining)
356pub struct ModuleLoaderWithCallback<F>
357where
358    F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
359{
360    loader: ModuleLoader,
361    callback: F,
362}
363
364impl<F> ModuleLoaderWithCallback<F>
365where
366    F: Fn(ModuleLoadingEvent) + Send + Sync + 'static,
367{
368    /// Load modules with attached progress callback
369    pub async fn load(self) -> Result<Vec<LoadedObjfile>> {
370        self.loader.load_with_progress(self.callback).await
371    }
372}
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377
378    #[test]
379    fn explicit_debug_file_matches_proc_root_rewritten_target_path() {
380        let mapping = ModuleMapping::from_path(PathBuf::from("/proc/123/root/usr/bin/app"));
381        let explicit = ExplicitDebugFile::new(
382            PathBuf::from("/usr/bin/app"),
383            PathBuf::from("/tmp/app.debug"),
384        );
385
386        assert!(explicit.matches_module(&mapping.path));
387        assert!(validate_explicit_debug_file_target(&[mapping], &explicit).is_ok());
388    }
389
390    #[test]
391    fn explicit_debug_file_rejects_unmatched_proc_root_target_path() {
392        let mapping = ModuleMapping::from_path(PathBuf::from("/proc/123/root/usr/bin/other"));
393        let explicit = ExplicitDebugFile::new(
394            PathBuf::from("/usr/bin/app"),
395            PathBuf::from("/tmp/app.debug"),
396        );
397
398        let error = validate_explicit_debug_file_target(&[mapping], &explicit)
399            .expect_err("unmatched explicit debug file should be rejected")
400            .to_string();
401
402        assert!(error.contains("/usr/bin/app"));
403    }
404
405    #[test]
406    fn proc_root_paths_equivalent_normalizes_either_side() {
407        assert!(proc_root_paths_equivalent(
408            Path::new("/proc/123/root/usr/bin/app"),
409            Path::new("/usr/bin/app")
410        ));
411        assert!(proc_root_paths_equivalent(
412            Path::new("/usr/lib/libfoo.so"),
413            Path::new("/proc/456/root/usr/lib/libfoo.so")
414        ));
415        assert!(!proc_root_paths_equivalent(
416            Path::new("/proc/123/root/usr/bin/app"),
417            Path::new("/usr/bin/other")
418        ));
419    }
420}