Skip to main content

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}