fastui_cosmic/
perf_lints.rs1pub mod lint_codes {
40 pub const EXPENSIVE_CLONE: &str = "cosmic_perf_001";
58
59 pub const BOX_DYN_IN_RENDER: &str = "cosmic_perf_002";
71
72 pub const ALLOCATION_IN_LOOP: &str = "cosmic_perf_003";
85
86 pub const STRING_ALLOCATION: &str = "cosmic_perf_004";
88
89 pub const MISSING_INLINE: &str = "cosmic_perf_005";
91
92 pub const SLOW_HASHER: &str = "cosmic_perf_006";
94
95 pub const STRING_CONCAT_LOOP: &str = "cosmic_perf_007";
97
98 pub const ARC_CLONE_UNNECESSARY: &str = "cosmic_perf_008";
100
101 pub const RC_IN_RENDER: &str = "cosmic_perf_009";
103
104 pub const LOCK_IN_RENDER: &str = "cosmic_perf_010";
106
107 pub const IO_IN_RENDER: &str = "cosmic_perf_011";
109
110 pub const REGEX_IN_HOT_PATH: &str = "cosmic_perf_012";
112
113 pub const COLLECT_THEN_ITER: &str = "cosmic_perf_013";
115
116 pub const CLONE_FULL_COLLECTION: &str = "cosmic_perf_014";
118
119 pub const FP_IN_TIGHT_LOOP: &str = "cosmic_perf_015";
121}
122
123pub mod check {
125 #[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 #[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 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#[derive(Debug)]
192pub struct PerformanceLinter {
193 enabled_rules: Vec<&'static str>,
194}
195
196impl PerformanceLinter {
197 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 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 pub fn disable(&mut self, rule: &'static str) {
229 self.enabled_rules.retain(|r| *r != rule);
230 }
231
232 pub fn enabled_rules(&self) -> &[&'static str] {
234 &self.enabled_rules
235 }
236
237 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#[derive(Debug, Clone)]
251pub struct LintIssue {
252 pub code: &'static str,
254 pub message: String,
256 pub line: usize,
258 pub column: usize,
260 pub suggestion: Option<String>,
262}
263
264impl PerformanceLinter {
265 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
385pub mod checklist {
387 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 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#[allow(non_snake_case)]
458pub mod macros {
459 #[macro_export]
462 #[doc(hidden)]
463 macro_rules! render_path {
464 ($($tt:tt)*) => {
465 #[inline]
466 $($tt)*
467 };
468 }
469
470 #[macro_export]
472 #[doc(hidden)]
473 macro_rules! render_type {
474 ($($tt:tt)*) => {
475 #[derive(Debug)]
476 $($tt)*
477 };
478 }
479
480 #[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}