Initial Commit
This commit is contained in:
@@ -0,0 +1,573 @@
|
||||
use anyhow::Result;
|
||||
use docx_mcp::docx_handler::{DocxHandler, DocxStyle, TableData};
|
||||
use docx_mcp::pure_converter::PureRustConverter;
|
||||
use docx_mcp::docx_tools::DocxToolsProvider;
|
||||
use docx_mcp::security::SecurityConfig;
|
||||
use mcp_core::{ToolProvider, ToolResult};
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
const PERFORMANCE_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
const STRESS_TEST_ITERATIONS: usize = 100;
|
||||
|
||||
#[test]
|
||||
fn test_large_document_performance() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let mut handler = DocxHandler::new_with_temp_dir(temp_dir.path()).unwrap();
|
||||
|
||||
let start = Instant::now();
|
||||
let doc_id = handler.create_document().unwrap();
|
||||
let creation_time = start.elapsed();
|
||||
|
||||
println!("Document creation took: {:?}", creation_time);
|
||||
assert!(creation_time < Duration::from_millis(500), "Document creation should be fast");
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
// Add substantial content
|
||||
for i in 0..1000 {
|
||||
if i % 50 == 0 {
|
||||
handler.add_heading(&doc_id, &format!("Section {}", i / 50 + 1), 2)?;
|
||||
}
|
||||
|
||||
let content = format!(
|
||||
"This is paragraph number {} in our performance test. It contains enough text to make the test meaningful and simulate real-world usage patterns. The paragraph includes various punctuation marks, numbers like {}, and other elements that might affect processing performance.",
|
||||
i, i * 7
|
||||
);
|
||||
handler.add_paragraph(&doc_id, &content, None)?;
|
||||
|
||||
// Add a table every 100 paragraphs
|
||||
if i % 100 == 99 {
|
||||
let table_data = TableData {
|
||||
rows: vec![
|
||||
vec!["Item".to_string(), "Value".to_string(), "Status".to_string()],
|
||||
vec![format!("Item {}", i), format!("${}.00", i * 10), "Active".to_string()],
|
||||
],
|
||||
headers: Some(vec!["Item".to_string(), "Value".to_string(), "Status".to_string()]),
|
||||
border_style: Some("single".to_string()),
|
||||
};
|
||||
handler.add_table(&doc_id, table_data)?;
|
||||
}
|
||||
}
|
||||
|
||||
let content_addition_time = start.elapsed();
|
||||
println!("Adding 1000 paragraphs took: {:?}", content_addition_time);
|
||||
assert!(content_addition_time < PERFORMANCE_TIMEOUT, "Content addition took too long");
|
||||
|
||||
// Test text extraction performance
|
||||
let start = Instant::now();
|
||||
let text = handler.extract_text(&doc_id)?;
|
||||
let extraction_time = start.elapsed();
|
||||
|
||||
println!("Text extraction took: {:?}", extraction_time);
|
||||
println!("Extracted text length: {} characters", text.len());
|
||||
assert!(extraction_time < Duration::from_secs(10), "Text extraction should be reasonably fast");
|
||||
assert!(text.len() > 100000, "Should extract substantial amount of text");
|
||||
|
||||
// Test PDF conversion performance
|
||||
let metadata = handler.get_metadata(&doc_id)?;
|
||||
let converter = PureRustConverter::new();
|
||||
let pdf_path = temp_dir.path().join("large_performance_test.pdf");
|
||||
|
||||
let start = Instant::now();
|
||||
converter.convert_docx_to_pdf(&metadata.path, &pdf_path)?;
|
||||
let conversion_time = start.elapsed();
|
||||
|
||||
println!("PDF conversion took: {:?}", conversion_time);
|
||||
assert!(conversion_time < PERFORMANCE_TIMEOUT, "PDF conversion took too long");
|
||||
assert!(pdf_path.exists(), "PDF should be created");
|
||||
|
||||
let pdf_size = std::fs::metadata(&pdf_path)?.len();
|
||||
println!("Generated PDF size: {} bytes", pdf_size);
|
||||
assert!(pdf_size > 50000, "PDF should have substantial size");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_concurrent_document_stress() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let temp_path = Arc::new(temp_dir.path().to_path_buf());
|
||||
let results = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
let thread_count = 8;
|
||||
let operations_per_thread = 20;
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
let handles: Vec<_> = (0..thread_count).map(|thread_id| {
|
||||
let temp_path = Arc::clone(&temp_path);
|
||||
let results = Arc::clone(&results);
|
||||
|
||||
thread::spawn(move || -> Result<()> {
|
||||
let mut handler = DocxHandler::new_with_temp_dir(&temp_path)?;
|
||||
let mut local_results = Vec::new();
|
||||
|
||||
for op_id in 0..operations_per_thread {
|
||||
let doc_start = Instant::now();
|
||||
|
||||
// Create document
|
||||
let doc_id = handler.create_document()?;
|
||||
|
||||
// Add varied content
|
||||
handler.add_heading(&doc_id, &format!("Thread {} Document {}", thread_id, op_id), 1)?;
|
||||
|
||||
for i in 0..10 {
|
||||
let content = format!("Thread {} operation {} paragraph {}", thread_id, op_id, i);
|
||||
handler.add_paragraph(&doc_id, &content, None)?;
|
||||
}
|
||||
|
||||
// Add a small table
|
||||
let table_data = TableData {
|
||||
rows: vec![
|
||||
vec!["Col1".to_string(), "Col2".to_string()],
|
||||
vec![format!("T{}", thread_id), format!("O{}", op_id)],
|
||||
],
|
||||
headers: None,
|
||||
border_style: Some("single".to_string()),
|
||||
};
|
||||
handler.add_table(&doc_id, table_data)?;
|
||||
|
||||
// Extract text
|
||||
let text = handler.extract_text(&doc_id)?;
|
||||
assert!(text.contains(&format!("Thread {} Document {}", thread_id, op_id)));
|
||||
|
||||
let doc_duration = doc_start.elapsed();
|
||||
local_results.push((thread_id, op_id, doc_duration));
|
||||
|
||||
// Cleanup
|
||||
handler.close_document(&doc_id)?;
|
||||
}
|
||||
|
||||
// Store results
|
||||
{
|
||||
let mut results_guard = results.lock().unwrap();
|
||||
results_guard.extend(local_results);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}).collect();
|
||||
|
||||
// Wait for all threads
|
||||
for handle in handles {
|
||||
handle.join().unwrap()?;
|
||||
}
|
||||
|
||||
let total_duration = start.elapsed();
|
||||
let results_guard = results.lock().unwrap();
|
||||
|
||||
println!("Concurrent stress test completed in: {:?}", total_duration);
|
||||
println!("Total operations: {}", results_guard.len());
|
||||
|
||||
let avg_duration = results_guard.iter()
|
||||
.map(|(_, _, duration)| duration.as_millis())
|
||||
.sum::<u128>() as f64 / results_guard.len() as f64;
|
||||
|
||||
println!("Average operation duration: {:.2}ms", avg_duration);
|
||||
|
||||
// Verify all operations completed
|
||||
assert_eq!(results_guard.len(), thread_count * operations_per_thread);
|
||||
assert!(total_duration < Duration::from_secs(60), "Stress test took too long");
|
||||
assert!(avg_duration < 1000.0, "Average operation should be under 1 second");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_intensive_operations() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let mut handler = DocxHandler::new_with_temp_dir(temp_dir.path()).unwrap();
|
||||
|
||||
let mut doc_ids = Vec::new();
|
||||
|
||||
// Create many documents simultaneously
|
||||
for i in 0..50 {
|
||||
let doc_id = handler.create_document()?;
|
||||
|
||||
// Add substantial content to each
|
||||
handler.add_heading(&doc_id, &format!("Memory Test Document {}", i), 1)?;
|
||||
|
||||
for j in 0..100 {
|
||||
let content = format!(
|
||||
"Document {} paragraph {}. This paragraph contains substantial text content to test memory usage patterns. It includes various data that might accumulate in memory during processing and needs to be handled efficiently by the system.",
|
||||
i, j
|
||||
);
|
||||
handler.add_paragraph(&doc_id, &content, None)?;
|
||||
}
|
||||
|
||||
// Add a large table
|
||||
let mut table_rows = vec![vec!["ID".to_string(), "Name".to_string(), "Description".to_string()]];
|
||||
for k in 0..20 {
|
||||
table_rows.push(vec![
|
||||
format!("ID-{}", k),
|
||||
format!("Item-{}", k),
|
||||
format!("Description for item {} in document {}", k, i),
|
||||
]);
|
||||
}
|
||||
|
||||
let table_data = TableData {
|
||||
rows: table_rows,
|
||||
headers: Some(vec!["ID".to_string(), "Name".to_string(), "Description".to_string()]),
|
||||
border_style: Some("single".to_string()),
|
||||
};
|
||||
handler.add_table(&doc_id, table_data)?;
|
||||
|
||||
doc_ids.push(doc_id);
|
||||
}
|
||||
|
||||
println!("Created {} documents with substantial content", doc_ids.len());
|
||||
|
||||
// Test that all documents are accessible
|
||||
for (i, doc_id) in doc_ids.iter().enumerate() {
|
||||
let text = handler.extract_text(doc_id)?;
|
||||
assert!(text.contains(&format!("Memory Test Document {}", i)));
|
||||
assert!(text.len() > 10000, "Document should have substantial text");
|
||||
}
|
||||
|
||||
// Test batch operations
|
||||
let start = Instant::now();
|
||||
let mut total_text_length = 0;
|
||||
|
||||
for doc_id in &doc_ids {
|
||||
let text = handler.extract_text(doc_id)?;
|
||||
total_text_length += text.len();
|
||||
}
|
||||
|
||||
let batch_extraction_time = start.elapsed();
|
||||
println!("Batch text extraction took: {:?}", batch_extraction_time);
|
||||
println!("Total extracted text: {} characters", total_text_length);
|
||||
|
||||
assert!(batch_extraction_time < Duration::from_secs(30), "Batch extraction should be reasonable");
|
||||
assert!(total_text_length > 500000, "Should extract substantial total text");
|
||||
|
||||
// Cleanup all documents
|
||||
for doc_id in doc_ids {
|
||||
handler.close_document(&doc_id)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mcp_tool_performance() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
std::env::set_var("TMPDIR", temp_dir.path());
|
||||
|
||||
let provider = DocxToolsProvider::new();
|
||||
let mut operation_times = Vec::new();
|
||||
|
||||
// Test document creation performance
|
||||
let start = Instant::now();
|
||||
let create_result = tokio_test::block_on(async {
|
||||
provider.call_tool("create_document", json!({})).await
|
||||
});
|
||||
let creation_time = start.elapsed();
|
||||
operation_times.push(("create_document", creation_time));
|
||||
|
||||
let doc_id = match create_result {
|
||||
ToolResult::Success(value) => value["document_id"].as_str().unwrap().to_string(),
|
||||
_ => panic!("Failed to create document"),
|
||||
};
|
||||
|
||||
// Test paragraph addition performance
|
||||
let start = Instant::now();
|
||||
for i in 0..100 {
|
||||
let args = json!({
|
||||
"document_id": doc_id,
|
||||
"text": format!("Performance test paragraph {} with substantial content for timing measurements", i)
|
||||
});
|
||||
|
||||
let result = tokio_test::block_on(async {
|
||||
provider.call_tool("add_paragraph", args).await
|
||||
});
|
||||
|
||||
match result {
|
||||
ToolResult::Success(_) => {},
|
||||
ToolResult::Error(e) => panic!("Failed to add paragraph {}: {}", i, e),
|
||||
}
|
||||
}
|
||||
let paragraph_addition_time = start.elapsed();
|
||||
operation_times.push(("add_100_paragraphs", paragraph_addition_time));
|
||||
|
||||
// Test heading performance
|
||||
let start = Instant::now();
|
||||
for level in 1..=6 {
|
||||
let args = json!({
|
||||
"document_id": doc_id,
|
||||
"text": format!("Heading Level {}", level),
|
||||
"level": level
|
||||
});
|
||||
|
||||
tokio_test::block_on(async {
|
||||
provider.call_tool("add_heading", args).await
|
||||
});
|
||||
}
|
||||
let heading_time = start.elapsed();
|
||||
operation_times.push(("add_headings", heading_time));
|
||||
|
||||
// Test table performance
|
||||
let start = Instant::now();
|
||||
let table_args = json!({
|
||||
"document_id": doc_id,
|
||||
"rows": [
|
||||
["Product", "Price", "Quantity", "Total"],
|
||||
["Item 1", "$10.00", "5", "$50.00"],
|
||||
["Item 2", "$15.00", "3", "$45.00"],
|
||||
["Item 3", "$12.00", "7", "$84.00"],
|
||||
["Item 4", "$8.00", "10", "$80.00"]
|
||||
]
|
||||
});
|
||||
|
||||
tokio_test::block_on(async {
|
||||
provider.call_tool("add_table", table_args).await
|
||||
});
|
||||
let table_time = start.elapsed();
|
||||
operation_times.push(("add_table", table_time));
|
||||
|
||||
// Test text extraction performance
|
||||
let start = Instant::now();
|
||||
let extract_args = json!({"document_id": doc_id});
|
||||
let extract_result = tokio_test::block_on(async {
|
||||
provider.call_tool("extract_text", extract_args).await
|
||||
});
|
||||
let extraction_time = start.elapsed();
|
||||
operation_times.push(("extract_text", extraction_time));
|
||||
|
||||
match extract_result {
|
||||
ToolResult::Success(value) => {
|
||||
let text = value["text"].as_str().unwrap();
|
||||
println!("Extracted text length: {} characters", text.len());
|
||||
assert!(text.len() > 5000, "Should extract substantial text");
|
||||
},
|
||||
ToolResult::Error(e) => panic!("Text extraction failed: {}", e),
|
||||
}
|
||||
|
||||
// Test metadata retrieval performance
|
||||
let start = Instant::now();
|
||||
let metadata_args = json!({"document_id": doc_id});
|
||||
tokio_test::block_on(async {
|
||||
provider.call_tool("get_metadata", metadata_args).await
|
||||
});
|
||||
let metadata_time = start.elapsed();
|
||||
operation_times.push(("get_metadata", metadata_time));
|
||||
|
||||
// Print performance results
|
||||
println!("\nMCP Tool Performance Results:");
|
||||
for (operation, duration) in &operation_times {
|
||||
println!("{}: {:?}", operation, duration);
|
||||
}
|
||||
|
||||
// Verify reasonable performance
|
||||
for (operation, duration) in &operation_times {
|
||||
match operation.as_ref() {
|
||||
"create_document" => assert!(duration < &Duration::from_millis(500), "Document creation too slow"),
|
||||
"add_100_paragraphs" => assert!(duration < &Duration::from_secs(10), "Paragraph addition too slow"),
|
||||
"extract_text" => assert!(duration < &Duration::from_secs(5), "Text extraction too slow"),
|
||||
_ => assert!(duration < &Duration::from_secs(2), "Operation {} too slow", operation),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_security_overhead_performance() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
std::env::set_var("TMPDIR", temp_dir.path());
|
||||
|
||||
// Test with default (permissive) security
|
||||
let default_provider = DocxToolsProvider::new();
|
||||
|
||||
// Test with restrictive security
|
||||
let restrictive_config = SecurityConfig {
|
||||
readonly_mode: true,
|
||||
sandbox_mode: true,
|
||||
max_document_size: 1024 * 1024, // 1MB
|
||||
max_open_documents: 10,
|
||||
allow_external_tools: false,
|
||||
allow_network: false,
|
||||
..Default::default()
|
||||
};
|
||||
let restrictive_provider = DocxToolsProvider::new_with_security(restrictive_config);
|
||||
|
||||
let operations = vec![
|
||||
("list_documents", json!({})),
|
||||
("get_security_info", json!({})),
|
||||
];
|
||||
|
||||
for (operation, args) in operations {
|
||||
// Test default provider
|
||||
let start = Instant::now();
|
||||
let _result = tokio_test::block_on(async {
|
||||
default_provider.call_tool(operation, args.clone()).await
|
||||
});
|
||||
let default_time = start.elapsed();
|
||||
|
||||
// Test restrictive provider
|
||||
let start = Instant::now();
|
||||
let _result = tokio_test::block_on(async {
|
||||
restrictive_provider.call_tool(operation, args.clone()).await
|
||||
});
|
||||
let restrictive_time = start.elapsed();
|
||||
|
||||
println!("Operation {}: Default={:?}, Restrictive={:?}",
|
||||
operation, default_time, restrictive_time);
|
||||
|
||||
// Security overhead should be minimal
|
||||
let overhead_ratio = restrictive_time.as_nanos() as f64 / default_time.as_nanos() as f64;
|
||||
assert!(overhead_ratio < 3.0, "Security overhead too high for {}: {}x", operation, overhead_ratio);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conversion_performance_scaling() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let converter = PureRustConverter::new();
|
||||
|
||||
let document_sizes = vec![10, 50, 100, 250];
|
||||
let mut performance_data = Vec::new();
|
||||
|
||||
for &size in &document_sizes {
|
||||
let mut handler = DocxHandler::new_with_temp_dir(temp_dir.path())?;
|
||||
let doc_id = handler.create_document()?;
|
||||
|
||||
// Create document with specified number of paragraphs
|
||||
handler.add_heading(&doc_id, &format!("Test Document - {} paragraphs", size), 1)?;
|
||||
|
||||
for i in 0..size {
|
||||
let content = format!("Paragraph {} content for performance scaling test. This paragraph contains enough text to make the performance test meaningful and realistic.", i);
|
||||
handler.add_paragraph(&doc_id, &content, None)?;
|
||||
|
||||
if i % 20 == 19 {
|
||||
handler.add_heading(&doc_id, &format!("Section {}", i / 20 + 1), 2)?;
|
||||
}
|
||||
}
|
||||
|
||||
let metadata = handler.get_metadata(&doc_id)?;
|
||||
|
||||
// Test text extraction scaling
|
||||
let start = Instant::now();
|
||||
let text = handler.extract_text(&doc_id)?;
|
||||
let extraction_time = start.elapsed();
|
||||
|
||||
// Test PDF conversion scaling
|
||||
let pdf_path = temp_dir.path().join(format!("scale_test_{}.pdf", size));
|
||||
let start = Instant::now();
|
||||
converter.convert_docx_to_pdf(&metadata.path, &pdf_path)?;
|
||||
let conversion_time = start.elapsed();
|
||||
|
||||
performance_data.push((size, text.len(), extraction_time, conversion_time));
|
||||
|
||||
println!("Size: {} paragraphs, Text: {} chars, Extract: {:?}, Convert: {:?}",
|
||||
size, text.len(), extraction_time, conversion_time);
|
||||
|
||||
handler.close_document(&doc_id)?;
|
||||
}
|
||||
|
||||
// Analyze scaling behavior
|
||||
for i in 1..performance_data.len() {
|
||||
let (prev_size, _, prev_extract, prev_convert) = performance_data[i-1];
|
||||
let (curr_size, _, curr_extract, curr_convert) = performance_data[i];
|
||||
|
||||
let size_ratio = curr_size as f64 / prev_size as f64;
|
||||
let extract_ratio = curr_extract.as_nanos() as f64 / prev_extract.as_nanos() as f64;
|
||||
let convert_ratio = curr_convert.as_nanos() as f64 / prev_convert.as_nanos() as f64;
|
||||
|
||||
println!("Size {}→{}: Extract scaling {:.2}, Convert scaling {:.2}",
|
||||
prev_size, curr_size, extract_ratio / size_ratio, convert_ratio / size_ratio);
|
||||
|
||||
// Performance should scale reasonably (not exponentially)
|
||||
assert!(extract_ratio / size_ratio < 3.0, "Text extraction scaling too poor");
|
||||
assert!(convert_ratio / size_ratio < 5.0, "PDF conversion scaling too poor");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_handling_performance() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
std::env::set_var("TMPDIR", temp_dir.path());
|
||||
|
||||
let provider = DocxToolsProvider::new();
|
||||
let error_operations = vec![
|
||||
("extract_text", json!({"document_id": "nonexistent"})),
|
||||
("add_paragraph", json!({"document_id": "fake", "text": "test"})),
|
||||
("get_metadata", json!({"document_id": "invalid"})),
|
||||
("unknown_tool", json!({})),
|
||||
];
|
||||
|
||||
for (operation, args) in error_operations {
|
||||
let start = Instant::now();
|
||||
|
||||
let result = tokio_test::block_on(async {
|
||||
provider.call_tool(operation, args).await
|
||||
});
|
||||
|
||||
let error_time = start.elapsed();
|
||||
println!("Error handling for {}: {:?}", operation, error_time);
|
||||
|
||||
// Error handling should be fast
|
||||
assert!(error_time < Duration::from_millis(100),
|
||||
"Error handling for {} too slow: {:?}", operation, error_time);
|
||||
|
||||
// Should return appropriate error
|
||||
match result {
|
||||
ToolResult::Error(_) | ToolResult::Success(_) => {}, // Both are acceptable for error cases
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resource_cleanup_performance() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let mut handler = DocxHandler::new_with_temp_dir(temp_dir.path())?;
|
||||
|
||||
let document_count = 50;
|
||||
let mut doc_ids = Vec::new();
|
||||
|
||||
// Create many documents
|
||||
let creation_start = Instant::now();
|
||||
for i in 0..document_count {
|
||||
let doc_id = handler.create_document()?;
|
||||
handler.add_paragraph(&doc_id, &format!("Document {} content", i), None)?;
|
||||
doc_ids.push(doc_id);
|
||||
}
|
||||
let creation_time = creation_start.elapsed();
|
||||
|
||||
println!("Created {} documents in {:?}", document_count, creation_time);
|
||||
|
||||
// Verify all documents exist
|
||||
let initial_count = handler.list_documents().len();
|
||||
assert_eq!(initial_count, document_count);
|
||||
|
||||
// Test cleanup performance
|
||||
let cleanup_start = Instant::now();
|
||||
for doc_id in doc_ids {
|
||||
handler.close_document(&doc_id)?;
|
||||
}
|
||||
let cleanup_time = cleanup_start.elapsed();
|
||||
|
||||
println!("Cleaned up {} documents in {:?}", document_count, cleanup_time);
|
||||
|
||||
// Verify cleanup worked
|
||||
let final_count = handler.list_documents().len();
|
||||
assert_eq!(final_count, 0);
|
||||
|
||||
// Cleanup should be reasonably fast
|
||||
assert!(cleanup_time < Duration::from_secs(5), "Cleanup took too long");
|
||||
|
||||
let avg_cleanup_time = cleanup_time.as_nanos() / document_count as u128;
|
||||
println!("Average cleanup time per document: {}ns", avg_cleanup_time);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user