Skip to main content

fastui_cosmic/
perf_lints.rs

1//! Performance lints for GUI/TUI applications
2//!
3//! This module provides lint rules to help developers identify
4//! common performance issues in GUI/TUI applications.
5//!
6//! ## Quick Reference
7//!
8//! | Lint Code | Description | Fix |
9//! |-----------|-------------|-----|
10//! | `cosmic_perf_001` | Clone in render method | Use `&` borrow instead |
11//! | `cosmic_perf_002` | `Box<dyn>` in hot path | Use generics |
12//! | `cosmic_perf_003` | `Vec::new()` in loop | Pre-allocate with capacity |
13//! | `cosmic_perf_004` | `to_string()` on `&str` | Use `&str` directly |
14//! | `cosmic_perf_005` | Hot fn without inline | Add `#[inline]` |
15//! | `cosmic_perf_006` | `HashMap` in render path | Use `FxHashMap` |
16//! | `cosmic_perf_007` | `s + &x` in loop | Use `join()` or `write!()` |
17//! | `cosmic_perf_008` | Unnecessary `Arc::clone()` | Use `&` instead |
18//! | `cosmic_perf_009` | `Rc` in render path | Use borrow |
19//! | `cosmic_perf_010` | Mutex in render loop | Double-buffer |
20//! | `cosmic_perf_011` | `println!` in render | Gate with `cfg` |
21//! | `cosmic_perf_012` | Regex in render | Pre-compile |
22//! | `cosmic_perf_013` | Unnecessary `.collect()` | Chain iterators |
23//! | `cosmic_perf_014` | Clone full collection | Borrow instead |
24//! | `cosmic_perf_015` | FP in tight loops | Use integer math |
25//!
26//! ## Usage
27//!
28//! Add this to your crate root:
29//!
30//! ```ignore
31//! // Enable all performance lints
32//! #![warn(
33//!     fastui_cosmic::perf_lints::EXPENSIVE_CLONE,
34//!     fastui_cosmic::perf_lints::ALLOCATION_IN_LOOP,
35//! use fastui_cosmic::perf_lints::{check_render_fn, lint_codes::*, PerformanceLinter};
36//! ```
37
38/// Lint error codes for GUI/TUI performance
39pub mod lint_codes {
40    //! Lint error codes that can be used with `#[warn(lint_code)]`
41
42    /// Detects expensive `.clone()` calls in render/update paths
43    ///
44    /// **BAD:**
45    /// ```ignore
46    /// fn render(&self) {
47    ///     let data = self.buffer.clone();
48    /// }
49    /// ```
50    ///
51    /// **GOOD:**
52    /// ```ignore
53    /// fn render(&self) {
54    ///     let data = &self.buffer;
55    /// }
56    /// ```
57    pub const EXPENSIVE_CLONE: &str = "cosmic_perf_001";
58
59    /// Detects `Box<dyn Trait>` in render-critical types
60    ///
61    /// **BAD:**
62    /// ```ignore
63    /// struct Renderer { drawer: Box<dyn Drawer> }
64    /// ```
65    ///
66    /// **GOOD:**
67    /// ```ignore
68    /// struct Renderer<D: Drawer> { drawer: D }
69    /// ```
70    pub const BOX_DYN_IN_RENDER: &str = "cosmic_perf_002";
71
72    /// Detects allocations inside loops
73    ///
74    /// **BAD:**
75    /// ```ignore
76    /// for _ in 0..1000 { let mut s = String::new(); }
77    /// ```
78    ///
79    /// **GOOD:**
80    /// ```ignore
81    /// let mut s = String::with_capacity(1024);
82    /// for _ in 0..1000 { s.clear(); }
83    /// ```
84    pub const ALLOCATION_IN_LOOP: &str = "cosmic_perf_003";
85
86    /// Detects unnecessary String allocations from &str
87    pub const STRING_ALLOCATION: &str = "cosmic_perf_004";
88
89    /// Detects missing #[inline] on hot path functions
90    pub const MISSING_INLINE: &str = "cosmic_perf_005";
91
92    /// Detects slow HashMap/HashSet in hot paths
93    pub const SLOW_HASHER: &str = "cosmic_perf_006";
94
95    /// Detects string concatenation in loops
96    pub const STRING_CONCAT_LOOP: &str = "cosmic_perf_007";
97
98    /// Detects unnecessary Arc::clone() calls
99    pub const ARC_CLONE_UNNECESSARY: &str = "cosmic_perf_008";
100
101    /// Detects Rc in render-critical code
102    pub const RC_IN_RENDER: &str = "cosmic_perf_009";
103
104    /// Detects locking in render paths
105    pub const LOCK_IN_RENDER: &str = "cosmic_perf_010";
106
107    /// Detects I/O in render paths
108    pub const IO_IN_RENDER: &str = "cosmic_perf_011";
109
110    /// Detects regex compilation in hot paths
111    pub const REGEX_IN_HOT_PATH: &str = "cosmic_perf_012";
112
113    /// Detects unnecessary .collect() followed by iteration
114    pub const COLLECT_THEN_ITER: &str = "cosmic_perf_013";
115
116    /// Detects cloning full collections when partial borrow suffices
117    pub const CLONE_FULL_COLLECTION: &str = "cosmic_perf_014";
118
119    /// Detects floating-point operations in tight loops
120    pub const FP_IN_TIGHT_LOOP: &str = "cosmic_perf_015";
121}
122
123/// Helper functions for performance checking
124pub mod check {
125    //! Runtime helpers to check for performance issues
126
127    /// Check if a function name suggests it's in the render path
128    ///
129    /// # Example
130    /// ```ignore
131    /// fn render_frame() { }  // returns true
132    /// fn draw_widget() { }    // returns true
133    /// fn process_data() { }   // returns false
134    /// ```
135    #[inline]
136    pub fn is_render_fn(name: &str) -> bool {
137        name.contains("render")
138            || name.contains("draw")
139            || name.contains("update")
140            || name.contains("paint")
141            || name.contains("layout")
142            || name.contains("shape")
143    }
144
145    #[inline]
146    pub fn check_render_fn(name: &str) -> bool {
147        is_render_fn(name)
148    }
149
150    /// Check if a type name suggests it's render-related
151    #[inline]
152    pub fn is_render_type(name: &str) -> bool {
153        name.contains("Buffer")
154            || name.contains("Layout")
155            || name.contains("Glyph")
156            || name.contains("Widget")
157            || name.contains("Renderer")
158    }
159
160    /// Recommend fast hasher based on context
161    pub fn recommended_hasher(use_case: &str) -> &'static str {
162        match use_case {
163            "cache" | "render" | "layout" => "FxHashMap / FxHashSet",
164            "crypto" | "security" => "Default (SipHash)",
165            "benchmark" | "one_shot" => "RandomState",
166            _ => "FxHashMap / FxHashSet",
167        }
168    }
169}
170
171/// AST-based performance linter for GUI/TUI applications
172///
173/// This linter analyzes code patterns that commonly cause
174/// performance issues in render/update paths.
175///
176/// # Usage
177///
178/// ```ignore
179/// use fastui_cosmic::perf_lints::{PerformanceLinter, lint_codes::*};
180///
181/// let mut linter = PerformanceLinter::new();
182/// linter.enable(EXPENSIVE_CLONE);
183/// linter.enable(ALLOCATION_IN_LOOP);
184/// linter.enable(SLOW_HASHER);
185///
186/// let issues = linter.lint_source(source_code);
187/// for issue in issues {
188///     println!("{}: {}", issue.code, issue.message);
189/// }
190/// ```
191#[derive(Debug)]
192pub struct PerformanceLinter {
193    enabled_rules: Vec<&'static str>,
194}
195
196impl PerformanceLinter {
197    /// Create a new PerformanceLinter with all rules enabled
198    pub fn new() -> Self {
199        Self {
200            enabled_rules: vec![
201                lint_codes::EXPENSIVE_CLONE,
202                lint_codes::BOX_DYN_IN_RENDER,
203                lint_codes::ALLOCATION_IN_LOOP,
204                lint_codes::STRING_ALLOCATION,
205                lint_codes::MISSING_INLINE,
206                lint_codes::SLOW_HASHER,
207                lint_codes::STRING_CONCAT_LOOP,
208                lint_codes::ARC_CLONE_UNNECESSARY,
209                lint_codes::RC_IN_RENDER,
210                lint_codes::LOCK_IN_RENDER,
211                lint_codes::IO_IN_RENDER,
212                lint_codes::REGEX_IN_HOT_PATH,
213                lint_codes::COLLECT_THEN_ITER,
214                lint_codes::CLONE_FULL_COLLECTION,
215                lint_codes::FP_IN_TIGHT_LOOP,
216            ],
217        }
218    }
219
220    /// Enable a specific lint rule
221    pub fn enable(&mut self, rule: &'static str) {
222        if !self.enabled_rules.contains(&rule) {
223            self.enabled_rules.push(rule);
224        }
225    }
226
227    /// Disable a specific lint rule
228    pub fn disable(&mut self, rule: &'static str) {
229        self.enabled_rules.retain(|r| *r != rule);
230    }
231
232    /// Get list of enabled rules
233    pub fn enabled_rules(&self) -> &[&'static str] {
234        &self.enabled_rules
235    }
236
237    /// Check if a rule is enabled
238    pub fn is_enabled(&self, rule: &str) -> bool {
239        self.enabled_rules.iter().any(|r| *r == rule)
240    }
241}
242
243impl Default for PerformanceLinter {
244    fn default() -> Self {
245        Self::new()
246    }
247}
248
249/// Represents a performance issue found by the linter
250#[derive(Debug, Clone)]
251pub struct LintIssue {
252    /// The lint code (e.g., "cosmic_perf_001")
253    pub code: &'static str,
254    /// Human-readable message describing the issue
255    pub message: String,
256    /// Line number where the issue was found
257    pub line: usize,
258    /// Column number
259    pub column: usize,
260    /// Suggested fix (if available)
261    pub suggestion: Option<String>,
262}
263
264impl PerformanceLinter {
265    /// Lint source code for performance issues
266    ///
267    /// This performs simple pattern matching on the source code.
268    /// For full AST-based linting, use a clippy integration.
269    pub fn lint_source(&self, source: &str) -> Vec<LintIssue> {
270        let mut issues = Vec::new();
271
272        for (line_num, line) in source.lines().enumerate() {
273            if self.is_enabled(lint_codes::EXPENSIVE_CLONE) {
274                if line.contains(".clone()")
275                    && (line.contains("render") || line.contains("draw") || line.contains("update"))
276                {
277                    issues.push(LintIssue {
278                        code: lint_codes::EXPENSIVE_CLONE,
279                        message: "Expensive clone in render path".to_string(),
280                        line: line_num + 1,
281                        column: line.find(".clone()").unwrap_or(0) + 1,
282                        suggestion: Some("Use & borrow instead".to_string()),
283                    });
284                }
285            }
286
287            if self.is_enabled(lint_codes::ALLOCATION_IN_LOOP) {
288                if (line.contains("String::new()")
289                    || line.contains("Vec::new()")
290                    || line.contains("Box::new("))
291                    && (source
292                        .lines()
293                        .nth(line_num.saturating_sub(1))
294                        .map_or(false, |l| l.contains("for"))
295                        || source
296                            .lines()
297                            .nth(line_num.saturating_sub(2))
298                            .map_or(false, |l| l.contains("for")))
299                {
300                    issues.push(LintIssue {
301                        code: lint_codes::ALLOCATION_IN_LOOP,
302                        message: "Allocation in loop".to_string(),
303                        line: line_num + 1,
304                        column: 1,
305                        suggestion: Some("Pre-allocate with capacity".to_string()),
306                    });
307                }
308            }
309
310            if self.is_enabled(lint_codes::SLOW_HASHER) {
311                if line.contains("HashMap") || line.contains("HashSet") {
312                    if !line.contains("FxHash") && !line.contains("FxHasher") {
313                        issues.push(LintIssue {
314                            code: lint_codes::SLOW_HASHER,
315                            message: "Slow default hasher in potential hot path".to_string(),
316                            line: line_num + 1,
317                            column: 1,
318                            suggestion: Some("Use FxHashMap or FxHashSet".to_string()),
319                        });
320                    }
321                }
322            }
323
324            if self.is_enabled(lint_codes::STRING_ALLOCATION) {
325                if line.contains("to_string()") && !line.contains("&str") {
326                    issues.push(LintIssue {
327                        code: lint_codes::STRING_ALLOCATION,
328                        message: "Unnecessary String allocation".to_string(),
329                        line: line_num + 1,
330                        column: line.find(".to_string()").unwrap_or(0) + 1,
331                        suggestion: Some("Use &str or &String directly".to_string()),
332                    });
333                }
334            }
335
336            if self.is_enabled(lint_codes::MISSING_INLINE) {
337                if line.contains("fn ")
338                    && !line.contains("#[inline]")
339                    && !line.contains("fn render")
340                    && !line.contains("fn draw")
341                {
342                    let fn_name = line
343                        .split("fn ")
344                        .nth(1)
345                        .unwrap_or("")
346                        .split('(')
347                        .next()
348                        .unwrap_or("");
349                    if check::is_render_fn(fn_name) {
350                        issues.push(LintIssue {
351                            code: lint_codes::MISSING_INLINE,
352                            message: format!("Hot path function '{}' missing #[inline]", fn_name),
353                            line: line_num + 1,
354                            column: 1,
355                            suggestion: Some("Add #[inline] attribute".to_string()),
356                        });
357                    }
358                }
359            }
360
361            if self.is_enabled(lint_codes::IO_IN_RENDER) {
362                if (line.contains("println!")
363                    || line.contains("eprintln!")
364                    || line.contains("write!"))
365                    && (line.contains("render")
366                        || line.contains("draw")
367                        || line.contains("update")
368                        || line.contains("paint"))
369                {
370                    issues.push(LintIssue {
371                        code: lint_codes::IO_IN_RENDER,
372                        message: "I/O operation in render path".to_string(),
373                        line: line_num + 1,
374                        column: 1,
375                        suggestion: Some("Gate with cfg(debug_assertions)".to_string()),
376                    });
377                }
378            }
379        }
380
381        issues
382    }
383}
384
385/// Performance checklist for GUI/TUI applications
386pub mod checklist {
387    /// Render loop performance checklist
388    pub const RENDER_LOOP: &str = r#"
389    GUI/TUI Performance Checklist:
390    
391    [ ] Avoid allocations in loops
392        - Use Vec::with_capacity()
393        - Reuse buffers with clear()
394    
395    [ ] Minimize cloning
396        - Borrow instead of clone
397        - Use Arc only when sharing necessary
398    
399    [ ] Use fast hash maps
400        - FxHashMap instead of HashMap
401        - FxHashSet instead of HashSet
402    
403    [ ] Enable optimizations
404        - #[inline] on hot functions
405        - LTO in release profile
406        - opt-level = 3
407    
408    [ ] Profile first
409        - cargo-flamegraph
410        - Measure frame times
411    
412    [ ] Threading
413        - Shape on background threads
414        - Double-buffer layouts
415        - Avoid locks in render
416    
417    [ ] Memory
418        - Cache-friendly layouts
419        - Minimize pointer chains
420        - Stack over heap when possible
421    "#;
422
423    /// cosmic-text specific optimizations
424    pub const COSMIC_TEXT: &str = r#"
425    cosmic-text Optimization Tips:
426    
427    [ ] Cache ShapeLine results
428        - Don't re-shape unchanged text
429    
430    [ ] Reuse Buffer objects
431        - Don't create new Buffer per frame
432    
433    [ ] Batch glyph updates
434        - Collect changes before shaping
435    
436    [ ] Use appropriate Wrap mode
437        - Wrap::None for fixed layouts
438        - Wrap::WordOrGlyph for dynamic
439    
440    [ ] Pre-load fonts
441        - Load fonts at startup
442        - Don't load in render loop
443    
444    [ ] Use FxHashMap for glyph cache
445        - Faster than default hasher
446    "#;
447}
448
449/// Manual lint implementation using procedural macros
450///
451/// Add to your code to mark functions for lint checking:
452///
453/// ```ignore
454/// #[render_path]
455/// fn draw(&mut self) { ... }
456/// ```
457#[allow(non_snake_case)]
458pub mod macros {
459    /// Marks a function as in the render path
460    /// This is a documentation marker - actual linting requires clippy
461    #[macro_export]
462    #[doc(hidden)]
463    macro_rules! render_path {
464        ($($tt:tt)*) => {
465            #[inline]
466            $($tt)*
467        };
468    }
469
470    /// Marks a type as render-critical
471    #[macro_export]
472    #[doc(hidden)]
473    macro_rules! render_type {
474        ($($tt:tt)*) => {
475            #[derive(Debug)]
476            $($tt)*
477        };
478    }
479
480    /// Helper to detect render methods
481    #[macro_export]
482    #[doc(hidden)]
483    macro_rules! is_render_method {
484        (render) => {
485            true
486        };
487        (draw) => {
488            true
489        };
490        (update) => {
491            true
492        };
493        (paint) => {
494            true
495        };
496        (layout) => {
497            true
498        };
499        ($other:ident) => {
500            false
501        };
502    }
503}
504
505#[cfg(test)]
506mod tests {
507    use super::check::{check_render_fn, is_render_fn, is_render_type, recommended_hasher};
508    use super::{lint_codes::*, LintIssue, PerformanceLinter};
509
510    #[test]
511    fn test_is_render_fn() {
512        assert!(is_render_fn("render_frame"));
513        assert!(is_render_fn("draw_widget"));
514        assert!(is_render_fn("update_layout"));
515        assert!(is_render_fn("paint"));
516        assert!(is_render_fn("shape_text"));
517        assert!(!is_render_fn("process_data"));
518    }
519
520    #[test]
521    fn test_check_render_fn() {
522        assert!(check_render_fn("render_frame"));
523        assert!(check_render_fn("draw_widget"));
524        assert!(!check_render_fn("process_data"));
525    }
526
527    #[test]
528    fn test_is_render_type() {
529        assert!(is_render_type("Buffer"));
530        assert!(is_render_type("LayoutGlyph"));
531        assert!(is_render_type("Widget"));
532        assert!(is_render_type("Renderer"));
533        assert!(!is_render_type("Config"));
534    }
535
536    #[test]
537    fn test_recommended_hasher() {
538        assert_eq!(recommended_hasher("cache"), "FxHashMap / FxHashSet");
539        assert_eq!(recommended_hasher("render"), "FxHashMap / FxHashSet");
540        assert_eq!(recommended_hasher("crypto"), "Default (SipHash)");
541    }
542
543    #[test]
544    fn test_performance_linter_default() {
545        let linter = PerformanceLinter::default();
546        assert!(linter.is_enabled(EXPENSIVE_CLONE));
547        assert!(linter.is_enabled(ALLOCATION_IN_LOOP));
548        assert!(linter.is_enabled(SLOW_HASHER));
549    }
550
551    #[test]
552    fn test_performance_linter_enable_disable() {
553        let mut linter = PerformanceLinter::new();
554        linter.disable(EXPENSIVE_CLONE);
555        assert!(!linter.is_enabled(EXPENSIVE_CLONE));
556
557        linter.enable(EXPENSIVE_CLONE);
558        assert!(linter.is_enabled(EXPENSIVE_CLONE));
559    }
560
561    #[test]
562    fn test_lint_source_clone() {
563        let linter = PerformanceLinter::new();
564        let source = r#"fn render(&self) { let data = self.buffer.clone(); }"#;
565        let issues = linter.lint_source(source);
566        assert!(!issues.is_empty());
567        assert_eq!(issues[0].code, EXPENSIVE_CLONE);
568    }
569
570    #[test]
571    fn test_lint_source_hashmap() {
572        let linter = PerformanceLinter::new();
573        let source = r#"
574fn draw() {
575    let map = HashMap::new();
576}
577"#;
578        let issues = linter.lint_source(source);
579        let hasher_issues: Vec<_> = issues.iter().filter(|i| i.code == SLOW_HASHER).collect();
580        assert!(!hasher_issues.is_empty());
581    }
582
583    #[test]
584    fn test_lint_source_to_string() {
585        let linter = PerformanceLinter::new();
586        let source = r#"
587fn process(s: &str) {
588    let x = s.to_string();
589}
590"#;
591        let issues = linter.lint_source(source);
592        let alloc_issues: Vec<_> = issues
593            .iter()
594            .filter(|i| i.code == STRING_ALLOCATION)
595            .collect();
596        assert!(!alloc_issues.is_empty());
597    }
598}