inspector_gguf/gui/loader.rs
1//! Asynchronous GGUF file loading with progress tracking.
2//!
3//! This module provides background file loading capabilities for GGUF files with
4//! real-time progress reporting and thread-safe result handling. The loading system
5//! is designed to keep the UI responsive during potentially long-running file
6//! operations while providing detailed progress feedback to users.
7//!
8//! # Architecture
9//!
10//! The loading system uses a multi-threaded approach:
11//!
12//! - **Main Thread**: Handles UI updates and progress display
13//! - **Worker Thread**: Performs file I/O and GGUF parsing
14//! - **Shared State**: Thread-safe progress and result communication
15//!
16//! # Progress Tracking
17//!
18//! Progress is reported through several phases:
19//!
20//! 1. **File Opening** (0-5%): Initial file access and validation
21//! 2. **Reading** (5-80%): Chunked file reading with real-time updates
22//! 3. **Parsing** (80-95%): GGUF format parsing and validation
23//! 4. **Processing** (95-100%): Metadata extraction and formatting
24//!
25//! # Usage
26//!
27//! ## Basic Async Loading
28//!
29//! ```rust
30//! use inspector_gguf::gui::loader::{load_gguf_metadata_async, LoadingResult};
31//! use std::sync::{Arc, Mutex};
32//! use std::path::PathBuf;
33//!
34//! let progress = Arc::new(Mutex::new(0.0f32));
35//! let result: LoadingResult = Arc::new(Mutex::new(None));
36//! let path = PathBuf::from("model.gguf");
37//!
38//! // Start async loading (non-blocking)
39//! load_gguf_metadata_async(path, progress.clone(), result.clone());
40//!
41//! // Check progress in UI loop
42//! let current_progress = *progress.lock().unwrap();
43//! if current_progress >= 1.0 {
44//! if let Some(load_result) = result.lock().unwrap().take() {
45//! match load_result {
46//! Ok(metadata) => println!("Loaded {} entries", metadata.len()),
47//! Err(e) => eprintln!("Loading failed: {}", e),
48//! }
49//! }
50//! }
51//! ```
52
53use crate::format::{get_full_tokenizer_content, readable_value_for_key};
54use std::fs::File;
55use std::io::Read;
56use std::sync::{Arc, Mutex};
57use std::thread;
58use std::time::{Duration, Instant};
59
60/// Type alias for thread-safe loading result container.
61///
62/// This type represents a shared, thread-safe container for loading results that can
63/// be accessed from both the worker thread (for writing results) and the main thread
64/// (for reading results). The nested structure provides:
65///
66/// - **Arc<Mutex<...>>**: Thread-safe shared ownership
67/// - **Option<...>**: Indicates whether a result is available
68/// - **Result<Vec<...>, String>**: Success with metadata or error with message
69/// - **Vec<(String, String, `Option<String>`)>**: Metadata entries with key, display value, and optional full content
70pub type LoadingResult = Arc<Mutex<Option<Result<Vec<(String, String, Option<String>)>, String>>>>;
71
72/// Represents a single metadata entry from a GGUF file.
73///
74/// This structure contains both the display-optimized and full content versions
75/// of metadata values, allowing the UI to show abbreviated content while preserving
76/// access to complete data for detailed viewing or export operations.
77///
78/// # Fields
79///
80/// * `key` - The metadata key identifier (e.g., "model.name", "tokenizer.chat_template")
81/// * `display_value` - Formatted value optimized for UI display (may be truncated or summarized)
82/// * `full_value` - Complete original value for detailed viewing (None if same as display_value)
83///
84/// # Examples
85///
86/// ```rust
87/// use inspector_gguf::gui::loader::MetadataEntry;
88///
89/// // Simple metadata entry
90/// let entry = MetadataEntry {
91/// key: "model.name".to_string(),
92/// display_value: "llama-7b-chat".to_string(),
93/// full_value: None, // Same as display value
94/// };
95///
96/// // Large content with separate display and full values
97/// let large_entry = MetadataEntry {
98/// key: "tokenizer.chat_template".to_string(),
99/// display_value: "Large template content...".to_string(),
100/// full_value: Some("Full template content here...".to_string()),
101/// };
102/// ```
103#[derive(Clone)]
104pub struct MetadataEntry {
105 /// The metadata key identifier (e.g., "model.name", "tokenizer.chat_template").
106 pub key: String,
107 /// Formatted value optimized for UI display (may be truncated or summarized).
108 pub display_value: String,
109 /// Complete original value for detailed viewing (None if same as display_value).
110 pub full_value: Option<String>,
111}
112
113/// Loads GGUF metadata asynchronously with progress tracking.
114///
115/// This function initiates background loading of a GGUF file, providing real-time
116/// progress updates and thread-safe result delivery. The operation is non-blocking,
117/// allowing the UI to remain responsive during file processing.
118///
119/// # Loading Process
120///
121/// 1. **File Validation** (0-5%): Opens and validates file access
122/// 2. **Chunked Reading** (5-80%): Reads file in 256KB chunks with progress updates
123/// 3. **GGUF Parsing** (80-95%): Parses GGUF format using Candle library
124/// 4. **Metadata Processing** (95-100%): Extracts and formats metadata entries
125///
126/// # Progress Reporting
127///
128/// Progress values have special meanings:
129/// - **0.0 to 1.0**: Normal progress from start to completion
130/// - **Negative values**: Indicate errors occurred during loading
131/// - **1.0**: Loading completed successfully
132///
133/// # Parameters
134///
135/// * `path` - Path to the GGUF file to load
136/// * `progress` - Shared progress indicator (0.0 to 1.0, negative for errors)
137/// * `result` - Shared result container for metadata or error messages
138///
139/// # Thread Safety
140///
141/// This function spawns a new thread for file operations. The progress and result
142/// parameters use Arc<Mutex<>> for safe cross-thread communication.
143///
144/// The function integrates with [`crate::format::load_gguf_metadata_with_full_content_sync`]
145/// for file parsing and works with [`crate::gui::GgufApp`] for UI integration.
146///
147/// # Examples
148///
149/// ## Basic Usage
150///
151/// ```rust
152/// use inspector_gguf::gui::loader::{load_gguf_metadata_async, LoadingResult};
153/// use std::sync::{Arc, Mutex};
154/// use std::path::PathBuf;
155///
156/// let progress = Arc::new(Mutex::new(0.0f32));
157/// let result: LoadingResult = Arc::new(Mutex::new(None));
158/// let path = PathBuf::from("model.gguf");
159///
160/// // Start loading (returns immediately)
161/// load_gguf_metadata_async(path, progress.clone(), result.clone());
162///
163/// // Monitor progress in your UI loop
164/// loop {
165/// let current_progress = *progress.lock().unwrap();
166///
167/// if current_progress < 0.0 {
168/// println!("Loading failed");
169/// break;
170/// } else if current_progress >= 1.0 {
171/// if let Some(load_result) = result.lock().unwrap().take() {
172/// match load_result {
173/// Ok(metadata) => println!("Loaded {} entries", metadata.len()),
174/// Err(e) => println!("Error: {}", e),
175/// }
176/// }
177/// break;
178/// } else {
179/// println!("Progress: {:.1}%", current_progress * 100.0);
180/// }
181///
182/// std::thread::sleep(std::time::Duration::from_millis(100));
183/// }
184/// ```
185///
186/// # Error Handling
187///
188/// Errors are communicated through both the progress indicator (negative values)
189/// and the result container (Err variant). Common error scenarios include:
190///
191/// - File not found or inaccessible
192/// - Invalid GGUF format
193/// - Insufficient memory for large files
194/// - I/O errors during reading
195pub fn load_gguf_metadata_async(
196 path: std::path::PathBuf,
197 progress: Arc<Mutex<f32>>,
198 result: LoadingResult,
199) {
200 puffin::profile_scope!("load_gguf_metadata_async");
201
202 thread::spawn(move || {
203 puffin::profile_scope!("file_loading_thread");
204 // Start loading
205 *progress.lock().unwrap() = 0.0;
206
207 // Try to open file
208 let mut f = {
209 puffin::profile_scope!("file_open");
210 match File::open(&path) {
211 Ok(file) => file,
212 Err(e) => {
213 *progress.lock().unwrap() = -1.0;
214 *result.lock().unwrap() = Some(Err(format!("Не удалось открыть файл: {}", e)));
215 return;
216 }
217 }
218 };
219
220 // Get file size for progress calculation
221 let file_size = {
222 puffin::profile_scope!("file_metadata");
223 match f.metadata() {
224 Ok(metadata) => metadata.len(),
225 Err(e) => {
226 *progress.lock().unwrap() = -1.0;
227 *result.lock().unwrap() =
228 Some(Err(format!("Не удалось получить размер файла: {}", e)));
229 return;
230 }
231 }
232 };
233
234 *progress.lock().unwrap() = 0.05;
235
236 // Read file into memory in chunks to show real progress
237 let mut buf = Vec::new();
238 let mut bytes_read = 0u64;
239 let chunk_size = 256 * 1024; // 256KB chunks for better performance
240 let mut chunk = vec![0u8; chunk_size];
241 let mut last_progress_update = Instant::now();
242 let mut last_progress_value = 0.05;
243
244 {
245 puffin::profile_scope!("file_reading");
246 loop {
247 match f.read(&mut chunk) {
248 Ok(0) => break, // EOF
249 Ok(n) => {
250 buf.extend_from_slice(&chunk[..n]);
251 bytes_read += n as u64;
252
253 // Update reading progress (from 5% to 80%), but not more often than once per 50ms
254 let read_progress = (bytes_read as f32 / file_size as f32) * 0.75 + 0.05;
255 let current_progress = read_progress.min(0.8);
256
257 // Update progress only if enough time has passed or change is significant
258 if last_progress_update.elapsed() > Duration::from_millis(50)
259 || (current_progress - last_progress_value).abs() > 0.01
260 {
261 *progress.lock().unwrap() = current_progress;
262 last_progress_value = current_progress;
263 last_progress_update = Instant::now();
264 }
265 }
266 Err(e) => {
267 *progress.lock().unwrap() = -1.0;
268 *result.lock().unwrap() = Some(Err(format!("Ошибка чтения файла: {}", e)));
269 return;
270 }
271 }
272 }
273 }
274
275 *progress.lock().unwrap() = 0.85;
276
277 // GGUF parsing
278 let content = {
279 puffin::profile_scope!("gguf_parsing");
280 let mut cursor = std::io::Cursor::new(&buf);
281 match candle::quantized::gguf_file::Content::read(&mut cursor) {
282 Ok(content) => content,
283 Err(e) => {
284 *progress.lock().unwrap() = -1.0;
285 *result.lock().unwrap() = Some(Err(format!("Ошибка парсинга GGUF: {}", e)));
286 return;
287 }
288 }
289 };
290
291 *progress.lock().unwrap() = 0.95;
292
293 // Process metadata
294 let mut out = Vec::new();
295 {
296 puffin::profile_scope!("metadata_processing");
297 for (k, v) in content.metadata.iter() {
298 let s = readable_value_for_key(k, v);
299 let full_content = get_full_tokenizer_content(k, v);
300 out.push((k.clone(), s, full_content));
301 }
302 }
303
304 *progress.lock().unwrap() = 1.0;
305 *result.lock().unwrap() = Some(Ok(out));
306 });
307}