1use 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#[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#[derive(Debug, Clone)]
37pub struct LoadConfig {
38 pub max_module_concurrency: usize,
40 pub debug_search_paths: Vec<String>,
42 pub allow_loose_debug_match: bool,
44 pub explicit_debug_file: Option<ExplicitDebugFile>,
46 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 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
75pub struct ModuleLoader {
77 mappings: Vec<ModuleMapping>,
78 config: LoadConfig,
79}
80
81impl ModuleLoader {
82 pub fn new(mappings: Vec<ModuleMapping>) -> Self {
84 Self {
85 mappings,
86 config: LoadConfig::default(),
87 }
88 }
89
90 pub fn parallel(mut self) -> Self {
92 self.config = LoadConfig::fast();
93 self
94 }
95
96 pub fn with_debug_search_paths(mut self, paths: Vec<String>) -> Self {
98 self.config.debug_search_paths = paths;
99 self
100 }
101
102 pub fn with_loose_debug_match(mut self, allow: bool) -> Self {
104 self.config.allow_loose_debug_match = allow;
105 self
106 }
107
108 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 pub fn with_debuginfod_client(mut self, client: Option<Arc<DebuginfodClient>>) -> Self {
116 self.config.debuginfod_client = client;
117 self
118 }
119
120 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 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 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 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 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 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
355pub 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 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}