1use crate::snapshot::{ActiveAllocation, MemorySnapshot, ThreadMemoryStats};
4use rayon::prelude::*;
5use serde_json::json;
6use std::{
7 collections::HashMap,
8 fs::File,
9 io::{BufWriter, Write},
10 path::Path,
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum OptimizationLevel {
16 Low,
18 #[default]
20 Medium,
21 High,
23 Maximum,
25}
26
27#[derive(Debug, Clone, Default)]
29pub struct SchemaValidator {
30 strict_mode: bool,
31}
32
33impl SchemaValidator {
34 pub fn new() -> Self {
35 Self { strict_mode: false }
36 }
37 pub fn with_strict_mode(mut self, strict: bool) -> Self {
38 self.strict_mode = strict;
39 self
40 }
41 pub fn validate(&self, data: &serde_json::Value) -> Result<(), String> {
42 if !data.is_object() {
43 return Err("Export data must be a JSON object".to_string());
44 }
45 let obj = data.as_object().ok_or("Invalid JSON object")?;
46 if self.strict_mode {
47 let required_fields = ["timestamp", "allocations", "stats"];
48 for field in &required_fields {
49 if !obj.contains_key(*field) {
50 return Err(format!("Missing required field: {}", field));
51 }
52 }
53 }
54 Ok(())
55 }
56}
57
58#[derive(Debug, Clone)]
59pub struct ExportJsonOptions {
60 pub parallel_processing: bool,
61 pub buffer_size: usize,
62 pub use_compact_format: Option<bool>,
63 pub enable_type_cache: bool,
64 pub batch_size: usize,
65 pub streaming_writer: bool,
66 pub schema_validation: bool,
67 pub adaptive_optimization: bool,
68 pub max_cache_size: usize,
69 pub security_analysis: bool,
70 pub include_low_severity: bool,
71 pub integrity_hashes: bool,
72 pub fast_export_mode: bool,
73 pub auto_fast_export_threshold: Option<usize>,
74 pub thread_count: Option<usize>,
75}
76
77impl Default for ExportJsonOptions {
78 fn default() -> Self {
79 Self {
80 parallel_processing: true,
81 buffer_size: 256 * 1024,
82 use_compact_format: None,
83 enable_type_cache: true,
84 batch_size: 1000,
85 streaming_writer: true,
86 schema_validation: false,
87 adaptive_optimization: true,
88 max_cache_size: 10_000,
89 security_analysis: false,
90 include_low_severity: false,
91 integrity_hashes: false,
92 fast_export_mode: false,
93 auto_fast_export_threshold: Some(10_000),
94 thread_count: None,
95 }
96 }
97}
98
99impl ExportJsonOptions {
100 pub fn fast_export_mode(mut self, enabled: bool) -> Self {
101 self.fast_export_mode = enabled;
102 self
103 }
104 pub fn security_analysis(mut self, enabled: bool) -> Self {
105 self.security_analysis = enabled;
106 self
107 }
108 pub fn streaming_writer(mut self, enabled: bool) -> Self {
109 self.streaming_writer = enabled;
110 self
111 }
112 pub fn schema_validation(mut self, enabled: bool) -> Self {
113 self.schema_validation = enabled;
114 self
115 }
116 pub fn integrity_hashes(mut self, enabled: bool) -> Self {
117 self.integrity_hashes = enabled;
118 self
119 }
120 pub fn batch_size(mut self, size: usize) -> Self {
121 self.batch_size = size;
122 self
123 }
124 pub fn adaptive_optimization(mut self, enabled: bool) -> Self {
125 self.adaptive_optimization = enabled;
126 self
127 }
128 pub fn max_cache_size(mut self, size: usize) -> Self {
129 self.max_cache_size = size;
130 self
131 }
132 pub fn include_low_severity(mut self, include: bool) -> Self {
133 self.include_low_severity = include;
134 self
135 }
136 pub fn thread_count(mut self, count: Option<usize>) -> Self {
137 self.thread_count = count;
138 self
139 }
140}
141
142pub fn export_snapshot_to_json(
143 snapshot: &MemorySnapshot,
144 output_path: &Path,
145 options: &ExportJsonOptions,
146) -> Result<(), Box<dyn std::error::Error>> {
147 export_snapshot_with_containers(snapshot, &[], output_path, options)
148}
149
150pub fn export_snapshot_with_containers(
156 snapshot: &MemorySnapshot,
157 containers: &[crate::capture::types::AllocationInfo],
158 output_path: &Path,
159 options: &ExportJsonOptions,
160) -> Result<(), Box<dyn std::error::Error>> {
161 if let Some(parent) = output_path.parent() {
163 if !parent.as_os_str().is_empty() {
164 std::fs::create_dir_all(parent)?;
165 }
166 }
167 let allocations: Vec<&ActiveAllocation> = snapshot.allocations().collect();
170 let mut processed = process_allocations(&allocations, snapshot.timestamp, options)?;
171 if let Some(bindings) = snapshot
174 .active_bindings
175 .as_ref()
176 .filter(|bindings| bindings.len() == processed.len())
177 {
178 for (entry, binding) in processed.iter_mut().zip(bindings.iter()) {
179 entry["generation_id"] = serde_json::json!(binding.generation_id);
180 entry["binding_serial"] = serde_json::json!(binding.binding_serial);
181 }
182 }
183 let output_dir = if output_path.extension().is_some() {
185 output_path.parent().unwrap_or(Path::new("."))
187 } else {
188 output_path
189 };
190 generate_memory_analysis_json(
191 output_dir,
192 &processed,
193 containers,
194 &snapshot.stats,
195 snapshot.active_bindings.is_some(),
196 options,
197 snapshot.timestamp,
198 )?;
199 generate_lifetime_json(output_dir, &processed, options, snapshot.timestamp)?;
200 generate_thread_analysis_json(
201 output_dir,
202 &snapshot.thread_stats,
203 options,
204 snapshot.timestamp,
205 )?;
206 Ok(())
207}
208
209fn process_allocations(
210 allocations: &[&ActiveAllocation],
211 captured_at: u64,
212 options: &ExportJsonOptions,
213) -> Result<Vec<serde_json::Value>, Box<dyn std::error::Error>> {
214 if options.parallel_processing && allocations.len() > options.batch_size {
215 let chunk_size = (allocations.len() / num_cpus::get()).max(1);
216 Ok(allocations
217 .par_chunks(chunk_size)
218 .flat_map(|batch| process_allocation_batch(batch, captured_at))
219 .collect())
220 } else {
221 Ok(process_allocation_batch(allocations, captured_at))
222 }
223}
224
225fn process_allocation_batch(
226 allocations: &[&ActiveAllocation],
227 captured_at: u64,
228) -> Vec<serde_json::Value> {
229 allocations
230 .iter()
231 .map(|alloc| {
232 let type_info = get_or_compute_type_info(
233 alloc.type_name.as_deref().unwrap_or("unknown"),
234 alloc.size,
235 );
236 let observed_age_ms = if alloc.allocated_at > 0 {
241 (captured_at.saturating_sub(alloc.allocated_at)) / 1_000_000
242 } else {
243 0
244 };
245 let lifetime_ms: Option<u64> = None;
250 let address = match alloc.ptr {
251 Some(ptr) => format!("0x{:x}", ptr),
252 None => "N/A".to_string(),
253 };
254 let mut entry = json!({
255 "address": address, "size": alloc.size, "type": type_info,
256 "timestamp": alloc.allocated_at, "thread_id": alloc.thread_id,
257 "lifetime_ms": lifetime_ms, "observed_age_ms": observed_age_ms,
258 "lifetime_complete": lifetime_ms.is_some(),
259 });
260 if let Some(ref var_name) = alloc.var_name {
261 entry["var_name"] = serde_json::json!(var_name);
262 }
263 if let Some(ref type_name) = alloc.type_name {
264 entry["type_name"] = serde_json::json!(type_name);
265 }
266 if let Some(ref module_path) = alloc.module_path {
267 if !is_library_module_path(module_path) {
269 entry["module_path"] = serde_json::json!(module_path);
270 }
271 }
272 entry
273 })
274 .collect()
275}
276
277fn is_library_module_path(module_path: &str) -> bool {
279 if module_path.starts_with("std::")
281 || module_path.starts_with("core::")
282 || module_path.starts_with("alloc::")
283 {
284 return true;
285 }
286 if module_path.starts_with("memscope_rs::") {
288 return true;
289 }
290 let library_prefixes = [
292 "tokio::",
293 "serde::",
294 "async_trait::",
295 "futures::",
296 "log::",
297 "tracing::",
298 "chrono::",
299 "indexmap::",
300 "rustc_hash::",
301 "parking_lot::",
302 "crossbeam::",
303 "rayon::",
304 "dashmap::",
305 "ahash::",
306 "hashbrown::",
307 ];
308 for prefix in library_prefixes.iter() {
309 if module_path.starts_with(prefix) {
310 return true;
311 }
312 }
313 false
314}
315
316pub(super) fn get_or_compute_type_info(type_name: &str, size: usize) -> String {
317 if (type_name.contains("Vec<") || type_name.contains("vec::Vec<"))
319 && !type_name.contains("VecDeque")
320 {
321 "dynamic_array".to_string()
322 } else if type_name == "str"
323 || type_name == "String"
324 || type_name.contains("&str")
325 || type_name.contains("alloc::string::String")
326 {
327 "string".to_string()
328 } else if type_name.contains("Box") || type_name.contains("Rc") || type_name.contains("Arc") {
329 "smart_pointer".to_string()
330 } else if type_name.contains("[") && type_name.contains("u8") {
331 "byte_array".to_string()
332 } else if size > 1024 * 1024 {
333 "large_buffer".to_string()
334 } else {
335 "custom".to_string()
336 }
337}
338
339fn generate_memory_analysis_json<P: AsRef<Path>>(
340 output_path: P,
341 allocations: &[serde_json::Value],
342 containers: &[crate::capture::types::AllocationInfo],
343 stats: &crate::snapshot::types::MemoryStats,
344 has_transition_history: bool,
345 options: &ExportJsonOptions,
346 captured_at: u64,
347) -> Result<(), Box<dyn std::error::Error>> {
348 let total_size: usize = allocations
349 .iter()
350 .filter_map(|a| a.get("size").and_then(|s| s.as_u64()))
351 .map(|s| s as usize)
352 .sum();
353 let type_distribution: HashMap<String, usize> = {
354 let mut dist = HashMap::new();
355 for alloc in allocations {
356 if let Some(t) = alloc.get("type").and_then(|t| t.as_str()) {
357 *dist.entry(t.to_string()).or_insert(0) += 1;
358 }
359 }
360 dist
361 };
362 let (cumulative_allocated, cumulative_deallocated, peak) = if has_transition_history {
371 (
372 serde_json::json!(stats.total_allocated),
373 serde_json::json!(stats.total_deallocated),
374 serde_json::json!(stats.peak_memory),
375 )
376 } else {
377 (
378 serde_json::Value::Null,
379 serde_json::Value::Null,
380 serde_json::Value::Null,
381 )
382 };
383 let counters_scope = if has_transition_history {
384 "live counters describe this cut, cumulative counters describe the whole capture"
385 } else {
386 "live only: this snapshot has no transition history, cumulative counters are unknown"
387 };
388 let data = json!({
389 "metadata": {
390 "export_version": "2.0", "export_timestamp": snapshot_timestamp(captured_at),
391 "snapshot_timestamp_ns": captured_at,
392 "specification": "memscope-rs memory analysis", "counters_scope": counters_scope,
393 "total_allocations": allocations.len(), "total_size_bytes": total_size,
394 "live_size_bytes": total_size, "degraded_releases": stats.degraded_releases,
395 "cumulative_allocated_bytes": cumulative_allocated, "cumulative_deallocated_bytes": cumulative_deallocated, "peak_bytes": peak
396 },
397 "allocations": allocations,
398 "containers": containers.iter().map(|container| json!({
400 "address": if container.ptr == 0 { "N/A".to_string() } else { format!("0x{:x}", container.ptr) },
401 "size": container.size,
402 "type_name": container.type_name.clone().unwrap_or_else(|| "unknown".to_string()),
403 "var_name": container.var_name.clone(), "thread_id": container.thread_id_u64, "counted_in_statistics": false,
404 })).collect::<Vec<_>>(),
405 "statistics": {
406 "total_allocations": allocations.len(), "total_size_bytes": total_size,
407 "average_size_bytes": if allocations.is_empty() { 0 } else { total_size / allocations.len() },
408 "live_size_bytes": total_size, "allocation_count": stats.total_allocations,
409 "deallocation_count": stats.total_deallocations, "degraded_releases": stats.degraded_releases,
410 "cumulative_allocated_bytes": cumulative_allocated, "cumulative_deallocated_bytes": cumulative_deallocated,
411 "peak_bytes": peak, "active_bindings": stats.active_allocations
412 },
413 "type_distribution": type_distribution
414 });
415 let path = output_path.as_ref().join("memory_analysis.json");
416 write_json_optimized(path, &data, options)?;
417 Ok(())
418}
419
420fn generate_lifetime_json<P: AsRef<Path>>(
421 output_path: P,
422 allocations: &[serde_json::Value],
423 options: &ExportJsonOptions,
424 captured_at: u64,
425) -> Result<(), Box<dyn std::error::Error>> {
426 let ownership_histories: Vec<serde_json::Value> = allocations.iter().map(|alloc| {
427 json!({
428 "address": alloc.get("address"), "var_name": alloc.get("var_name"),
429 "type_name": alloc.get("type_name"), "size": alloc.get("size"),
430 "timestamp_alloc": alloc.get("timestamp"), "timestamp_dealloc": null,
431 "lifetime_ms": null, "observed_age_ms": alloc.get("observed_age_ms"), "lifetime_complete": false,
434 "events": [{ "event_type": "Created", "timestamp": alloc.get("timestamp"), "context": "initial_allocation" }]
435 })
436 }).collect();
437 let lifetime_data = json!({
438 "metadata": {
439 "export_version": "2.0", "export_timestamp": snapshot_timestamp(captured_at),
440 "snapshot_timestamp_ns": captured_at,
441 "specification": "memscope-rs lifetime tracking", "total_tracked_allocations": ownership_histories.len()
442 },
443 "ownership_histories": ownership_histories
444 });
445 let lifetime_path = output_path.as_ref().join("lifetime.json");
446 write_json_optimized(lifetime_path, &lifetime_data, options)?;
447 Ok(())
448}
449
450fn generate_thread_analysis_json<P: AsRef<Path>>(
451 output_path: P,
452 thread_stats: &HashMap<u64, ThreadMemoryStats>,
453 options: &ExportJsonOptions,
454 captured_at: u64,
455) -> Result<(), Box<dyn std::error::Error>> {
456 let mut thread_analysis: Vec<serde_json::Value> = thread_stats.values().map(|stats| {
457 json!({
458 "thread_id": stats.thread_id, "allocation_count": stats.allocation_count,
459 "total_allocated": stats.total_allocated, "current_memory": stats.current_memory, "peak_memory": stats.peak_memory,
460 })
461 }).collect();
462 thread_analysis.sort_by_key(|row| row["thread_id"].as_u64());
463 let data = json!({
464 "metadata": {
465 "export_version": "2.0", "export_timestamp": snapshot_timestamp(captured_at),
466 "snapshot_timestamp_ns": captured_at,
467 "specification": "thread analysis", "total_threads": thread_analysis.len()
468 },
469 "thread_analysis": thread_analysis
470 });
471 let path = output_path.as_ref().join("thread_analysis.json");
472 write_json_optimized(path, &data, options)?;
473 Ok(())
474}
475
476fn snapshot_timestamp(captured_at: u64) -> String {
478 chrono::DateTime::from_timestamp(
479 (captured_at / 1_000_000_000) as i64,
480 (captured_at % 1_000_000_000) as u32,
481 )
482 .expect("a u64 nanosecond timestamp is within chrono's supported range")
483 .to_rfc3339()
484}
485
486fn write_json_optimized<P: AsRef<Path>>(
487 path: P,
488 data: &serde_json::Value,
489 options: &ExportJsonOptions,
490) -> Result<(), Box<dyn std::error::Error>> {
491 let path = path.as_ref();
492 let estimated_size = estimate_json_size(data);
493 let use_compact = options
494 .use_compact_format
495 .unwrap_or(estimated_size > 1_000_000);
496 if options.streaming_writer && estimated_size > 500_000 {
497 let file = File::create(path)?;
498 let mut writer = BufWriter::with_capacity(options.buffer_size, file);
499 if use_compact {
500 serde_json::to_writer(&mut writer, data)?;
501 } else {
502 serde_json::to_writer_pretty(&mut writer, data)?;
503 }
504 writer.flush()?;
505 } else {
506 let json_string = if use_compact {
507 serde_json::to_string(data)?
508 } else {
509 serde_json::to_string_pretty(data)?
510 };
511 std::fs::write(path, json_string)?;
512 }
513 Ok(())
514}
515
516pub(super) fn estimate_json_size(data: &serde_json::Value) -> usize {
517 match data {
518 serde_json::Value::Object(map) => {
519 map.values().map(estimate_json_size).sum::<usize>() + map.len() * 20
520 }
521 serde_json::Value::Array(arr) => {
522 arr.iter().map(estimate_json_size).sum::<usize>() + arr.len() * 10
523 }
524 serde_json::Value::String(s) => s.len(),
525 serde_json::Value::Number(n) => n.to_string().len(),
526 _ => 10,
527 }
528}
529
530#[derive(Debug, thiserror::Error)]
531pub enum ExportError {
532 #[error("IO error: {0}")]
533 Io(#[from] std::io::Error),
534 #[error("JSON error: {0}")]
535 Json(#[from] serde_json::Error),
536 #[error("Export failed: {0}")]
537 ExportFailed(String),
538}