1pub mod changes;
12pub mod panels;
13
14use std::cell::RefCell;
15
16use crate::trace::{self, Point};
17
18pub use crate::trace::is_observed;
19
20fn render_threshold() -> u64 {
23 static MICROSECONDS: std::sync::OnceLock<u64> = std::sync::OnceLock::new();
24 *MICROSECONDS.get_or_init(|| {
25 std::env::var("GUINEA_TRACE_RENDER_MS")
26 .ok()
27 .and_then(|given| given.parse::<f64>().ok())
28 .map_or(2_000, |milliseconds| (milliseconds * 1000.0) as u64)
29 })
30}
31
32thread_local! {
33 static EVERY_RENDER: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
34}
35
36pub fn record_every_render() -> EveryRender {
44 EveryRender(EVERY_RENDER.replace(true))
45}
46
47pub struct EveryRender(bool);
49
50impl Drop for EveryRender {
51 fn drop(&mut self) {
52 let _gone = EVERY_RENDER.try_with(|every| every.set(self.0));
53 }
54}
55
56pub struct Rendering {
62 segment: &'static str,
63 started: std::time::Instant,
64 #[cfg(feature = "profiling")]
66 _zone: Option<puffin::ProfilerScope>,
67}
68
69impl Rendering {
70 pub fn of(segment: &'static str) -> Option<Rendering> {
71 #[cfg(feature = "profiling")]
72 {
73 let zone = profiling::zone(segment);
74 if !trace::is_observed_anywhere() && zone.is_none() {
75 return None;
76 }
77
78 return Some(Rendering {
79 segment,
80 started: std::time::Instant::now(),
81 _zone: zone,
82 });
83 }
84
85 #[cfg(not(feature = "profiling"))]
86 trace::is_observed_anywhere().then(|| Rendering {
87 segment,
88 started: std::time::Instant::now(),
89 })
90 }
91}
92
93pub mod profiling {
98 pub fn on() -> bool {
101 #[cfg(feature = "profiling")]
102 {
103 puffin::are_scopes_on()
104 }
105 #[cfg(not(feature = "profiling"))]
106 {
107 false
108 }
109 }
110
111 pub fn record(on: bool) {
113 #[cfg(feature = "profiling")]
114 puffin::set_scopes_on(on);
115 #[cfg(not(feature = "profiling"))]
116 let _ = on;
117 }
118
119 pub fn frame_done() {
122 #[cfg(feature = "profiling")]
123 puffin::GlobalProfiler::lock().new_frame();
124 }
125
126 #[cfg(feature = "profiling")]
129 pub(super) fn zone(segment: &'static str) -> Option<puffin::ProfilerScope> {
130 if !puffin::are_scopes_on() {
131 return None;
132 }
133
134 static SCOPE: std::sync::OnceLock<puffin::ScopeId> = std::sync::OnceLock::new();
135 let scope = *SCOPE.get_or_init(|| {
136 puffin::ThreadProfiler::call(|profiler| {
137 profiler.register_named_scope("render", "guinea", file!(), line!())
138 })
139 });
140
141 Some(puffin::ProfilerScope::new(scope, segment))
142 }
143
144}
145
146impl Drop for Rendering {
147 fn drop(&mut self) {
148 let took_us = self.started.elapsed().as_micros() as u64;
149 if took_us < render_threshold() && !EVERY_RENDER.get() {
150 return;
151 }
152
153 let segment = self.segment;
154 trace::mark(|| Point::Render { segment, took_us });
155 }
156}
157
158pub fn mark_anywhere(point: impl FnOnce() -> Point + Send + 'static) {
164 if trace::is_observed() || !trace::is_observed_anywhere() {
165 trace::mark(point);
166 return;
167 }
168 let cause = trace::current();
169 let marked = crate::actor::try_invoke_on_ui(move || {
170 trace::mark_under(cause, point);
171 });
172 if let Err(unsent) = marked {
173 unsent();
174 }
175}
176
177pub(crate) fn begin_anywhere(
180 id: trace::Cause,
181 parent: Option<trace::Cause>,
182 point: impl FnOnce() -> Point + Send + 'static,
183) {
184 if trace::is_observed() || !trace::is_observed_anywhere() {
185 trace::begin_as(id, parent, point);
186 return;
187 }
188 let begun = crate::actor::try_invoke_on_ui(move || trace::begin_as(id, parent, point));
189 if let Err(unsent) = begun {
190 unsent();
191 }
192}
193
194pub(crate) fn end_anywhere(id: trace::Cause, took: std::time::Duration) {
196 if trace::is_observed() || !trace::is_observed_anywhere() {
197 trace::end(id, took);
198 return;
199 }
200 let ended = crate::actor::try_invoke_on_ui(move || trace::end(id, took));
201 if let Err(unsent) = ended {
202 unsent();
203 }
204}
205
206pub fn layer() -> LogLayer {
229 LogLayer { all: false }
230}
231
232pub struct LogLayer {
233 all: bool,
234}
235
236impl LogLayer {
237 pub fn all(self) -> Self {
239 Self { all: true }
240 }
241}
242
243fn written_here(file: Option<&str>) -> bool {
250 file.is_some_and(|file| std::path::Path::new(file).is_relative())
251}
252
253thread_local! {
254 static LOGGING: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
255
256 static ENTERED: RefCell<Vec<(tracing::span::Id, trace::Resumed, std::time::Instant)>> =
259 const { RefCell::new(Vec::new()) };
260}
261
262struct Opened {
265 id: trace::Cause,
266 busy: std::time::Duration,
267}
268
269impl LogLayer {
270 fn wants(&self, meta: &tracing::Metadata<'_>) -> bool {
271 !trace::is_point_target(meta.target()) && (self.all || written_here(meta.file()))
272 }
273}
274
275impl<S> tracing_subscriber::Layer<S> for LogLayer
276where
277 S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
278{
279 fn on_new_span(
280 &self,
281 attrs: &tracing::span::Attributes<'_>,
282 id: &tracing::span::Id,
283 cx: tracing_subscriber::layer::Context<'_, S>,
284 ) {
285 let meta = attrs.metadata();
286 if !self.wants(meta) || !trace::is_recorded_anywhere() {
287 return;
288 }
289 let Some(span) = cx.span(id) else {
290 return;
291 };
292
293 let parent = span
294 .parent()
295 .and_then(|above| above.extensions().get::<Opened>().map(|opened| opened.id))
296 .or_else(trace::current);
297
298 let mut fields = Text::default();
299 attrs.record(&mut fields);
300
301 let opened = trace::reserve();
302 let (name, level, target) = (meta.name(), *meta.level(), meta.target());
303 let (file, line, module) = (meta.file(), meta.line(), meta.module_path());
304 begin_anywhere(opened, parent, move || Point::Span {
305 name,
306 level,
307 target,
308 file,
309 line,
310 module,
311 fields: fields.finish(),
312 });
313
314 span.extensions_mut().insert(Opened {
315 id: opened,
316 busy: std::time::Duration::ZERO,
317 });
318 }
319
320 fn on_enter(&self, id: &tracing::span::Id, cx: tracing_subscriber::layer::Context<'_, S>) {
321 let Some(opened) = cx
322 .span(id)
323 .and_then(|span| span.extensions().get::<Opened>().map(|opened| opened.id))
324 else {
325 return;
326 };
327
328 let current = trace::resume(Some(opened));
329 ENTERED.with(|entered| {
330 entered
331 .borrow_mut()
332 .push((id.clone(), current, std::time::Instant::now()));
333 });
334 }
335
336 fn on_exit(&self, id: &tracing::span::Id, cx: tracing_subscriber::layer::Context<'_, S>) {
337 let left = ENTERED.with(|entered| {
338 let mut entered = entered.borrow_mut();
339 let at = entered.iter().rposition(|(span, ..)| span == id)?;
340 Some(entered.remove(at))
341 });
342 let Some((_, current, since)) = left else {
343 return;
344 };
345 drop(current);
346
347 if let Some(span) = cx.span(id)
348 && let Some(opened) = span.extensions_mut().get_mut::<Opened>()
349 {
350 opened.busy += since.elapsed();
351 }
352 }
353
354 fn on_close(&self, id: tracing::span::Id, cx: tracing_subscriber::layer::Context<'_, S>) {
355 let Some(opened) = cx
356 .span(&id)
357 .and_then(|span| span.extensions_mut().remove::<Opened>())
358 else {
359 return;
360 };
361
362 end_anywhere(opened.id, opened.busy);
363 }
364
365 fn on_event(&self, event: &tracing::Event<'_>, _: tracing_subscriber::layer::Context<'_, S>) {
366 let meta = event.metadata();
367 if !self.wants(meta) || !trace::is_observed_anywhere() || LOGGING.get() {
368 return;
369 }
370
371 LOGGING.set(true);
372 let mut text = Text::default();
373 event.record(&mut text);
374
375 let (level, target) = (*meta.level(), meta.target());
376 let (file, line, module) = (meta.file(), meta.line(), meta.module_path());
377 mark_anywhere(move || Point::Log {
378 level,
379 target,
380 file,
381 line,
382 module,
383 text: text.finish(),
384 });
385 LOGGING.set(false);
386 }
387}
388
389#[derive(Default)]
390struct Text {
391 message: String,
392 fields: Vec<String>,
393}
394
395impl Text {
396 fn finish(self) -> String {
397 let mut text = self.message;
398 for field in self.fields {
399 if !text.is_empty() {
400 text.push(' ');
401 }
402 text.push_str(&field);
403 }
404 text
405 }
406}
407
408impl tracing::field::Visit for Text {
409 fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
410 if field.name() == "message" {
411 self.message = value.to_string();
412 } else {
413 self.fields.push(format!("{}={value}", field.name()));
414 }
415 }
416
417 fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
418 if field.name() == "message" {
419 self.message = format!("{value:?}");
420 } else {
421 self.fields.push(format!("{}={value:?}", field.name()));
422 }
423 }
424}
425
426#[cfg(test)]
427mod tests {
428 use std::rc::Rc;
429
430 use super::changes::{Change, changed};
431 use super::panels::{Panel, contribute, contribute_to_app, for_app, for_root};
432 use super::*;
433
434 fn test_panel() -> Option<Panel> {
435 Some(Panel {
436 id: "test",
437 title: "Test",
438 nodes: Vec::new(),
439 })
440 }
441
442 #[test]
443 fn a_panel_is_offered_for_its_root_until_the_guard_goes() {
444 let guard = contribute(7, test_panel);
445 assert_eq!(for_root(7).len(), 1);
446 assert!(for_root(8).is_empty());
447 assert!(for_app().is_empty());
448
449 drop(guard);
450 assert!(for_root(7).is_empty());
451 }
452
453 #[cfg(not(feature = "test-utils"))]
454 struct Queue(std::sync::mpsc::Sender<crate::actor::UiTask>);
455
456 #[cfg(not(feature = "test-utils"))]
457 impl crate::actor::UiDispatcher for Queue {
458 fn init(&self) {}
459
460 fn dispatch(&self, task: crate::actor::UiTask) {
461 let _ = self.0.send(task);
462 }
463 }
464
465 #[test]
466 #[cfg(not(feature = "test-utils"))]
467 fn a_mark_from_another_thread_lands_on_the_watched_one_under_its_cause() {
468 let (tx, rx) = std::sync::mpsc::channel();
469 crate::actor::set_ui_dispatcher(Queue(tx));
470 let seen = Rc::new(RefCell::new(Vec::new()));
471 let sink = seen.clone();
472 trace::observe(move |record| {
473 if let trace::Trace::Mark(record) = record {
474 sink.borrow_mut().push((record.parent, record.point.kind()));
475 }
476 });
477
478 let action = trace::mark(|| Point::Action { message: "Save" });
479 std::thread::spawn(move || {
480 let _resumed = trace::resume(Some(action));
481 mark_anywhere(|| Point::Note("written".into()));
482 })
483 .join()
484 .expect("writer");
485 for task in rx.try_iter() {
486 task();
487 }
488 trace::stop_observing();
489
490 assert_eq!(
491 *seen.borrow(),
492 [(None, "action"), (Some(action), "note")],
493 "the write is marked here, under the action that caused it"
494 );
495 }
496
497 #[test]
498 fn an_ordinary_event_is_traced_under_what_caused_it() {
499 use tracing_subscriber::layer::SubscriberExt;
500
501 let seen = Rc::new(RefCell::new(Vec::new()));
502 let sink = seen.clone();
503 trace::observe(move |record| {
504 if let trace::Trace::Mark(record) = record
505 && let Point::Log { text, .. } = &record.point
506 {
507 sink.borrow_mut().push((record.parent, text.clone()));
508 }
509 });
510
511 let subscriber = tracing_subscriber::registry().with(layer());
512 let action = trace::mark(|| Point::Action { message: "Kill" });
513 tracing::subscriber::with_default(subscriber, || {
514 let _resumed = trace::resume(Some(action));
515 tracing::info!(pid = 42, "process killed");
516 tracing::debug!(target: "guinea::note", "a guinea point, logged: not traced twice");
517 });
518 trace::stop_observing();
519
520 assert_eq!(
521 *seen.borrow(),
522 [(Some(action), "process killed pid=42".to_string())]
523 );
524 }
525
526 #[test]
527 fn a_logged_event_says_where_it_was_written() {
528 use tracing_subscriber::layer::SubscriberExt;
529
530 let seen = Rc::new(RefCell::new(Vec::new()));
531 let sink = seen.clone();
532 trace::observe(move |record| {
533 if let trace::Trace::Mark(record) = record
534 && let Point::Log {
535 file, line, module, ..
536 } = &record.point
537 {
538 sink.borrow_mut().push((*file, line.is_some(), *module));
539 }
540 });
541
542 let subscriber = tracing_subscriber::registry().with(layer());
543 tracing::subscriber::with_default(subscriber, || tracing::info!("here"));
544 trace::stop_observing();
545
546 assert_eq!(
547 *seen.borrow(),
548 [(Some(file!()), true, Some(module_path!()))]
549 );
550 }
551
552 #[test]
553 fn a_span_is_open_under_what_caused_it_and_current_while_entered() {
554 use tracing_subscriber::layer::SubscriberExt;
555
556 let seen = Rc::new(RefCell::new(Vec::new()));
557 let sink = seen.clone();
558 trace::observe(move |trace| sink.borrow_mut().push(trace.clone()));
559
560 let subscriber = tracing_subscriber::registry().with(layer());
561 let action = trace::mark(|| Point::Action { message: "Refresh" });
562 tracing::subscriber::with_default(subscriber, || {
563 let _resumed = trace::resume(Some(action));
564 let span = tracing::info_span!("rows_from_report", rows = 3);
565 {
566 let _entered = span.enter();
567 trace::mark(|| Point::Push { reducer: "Rows" });
568 }
569 trace::mark(|| Point::Note("after the span".into()));
570 });
571 trace::stop_observing();
572
573 let seen = seen.borrow();
574 let span = seen
575 .iter()
576 .find_map(|trace| match trace {
577 trace::Trace::Begin(record) => Some(record),
578 _ => None,
579 })
580 .expect("the span was opened");
581 assert_eq!(span.parent, Some(action));
582 assert!(
583 matches!(
584 &span.point,
585 Point::Span { name: "rows_from_report", fields, file: Some(file), .. }
586 if fields == "rows=3" && *file == file!()
587 ),
588 "{:?}",
589 span.point
590 );
591
592 let parent_of = |kind: &str| {
593 seen.iter().find_map(|trace| match trace {
594 trace::Trace::Mark(record) if record.point.kind() == kind => Some(record.parent),
595 _ => None,
596 })
597 };
598 assert_eq!(parent_of("push"), Some(Some(span.id)), "inside it, under it");
599 assert_eq!(parent_of("note"), Some(Some(action)), "after it, under what was there");
600 assert!(
601 matches!(seen.last(), Some(trace::Trace::End { id, .. }) if *id == span.id),
602 "{seen:?}"
603 );
604 }
605
606 #[test]
607 fn nothing_is_built_for_a_change_nobody_watches() {
608 let built = std::cell::Cell::new(false);
609 changed(|| {
610 built.set(true);
611 Change::TimerStarted { id: 0 }
612 });
613
614 assert!(!built.get());
615 }
616
617 #[test]
618 fn a_span_is_at_the_level_it_was_opened_at() {
619 use tracing_subscriber::layer::SubscriberExt;
620
621 let seen = Rc::new(RefCell::new(Vec::new()));
622 let sink = seen.clone();
623 trace::observe(move |trace| {
624 if let trace::Trace::Begin(record) = trace
625 && let Point::Span { name, level, .. } = &record.point
626 {
627 sink.borrow_mut().push((*name, *level));
628 }
629 });
630
631 let subscriber = tracing_subscriber::registry().with(layer());
632 tracing::subscriber::with_default(subscriber, || {
633 let _warned = tracing::warn_span!("retrying").entered();
634 let _debugged = tracing::debug_span!("parsing").entered();
635 });
636 trace::stop_observing();
637
638 assert_eq!(
639 *seen.borrow(),
640 [("retrying", tracing::Level::WARN), ("parsing", tracing::Level::DEBUG)]
641 );
642 }
643
644 #[test]
645 fn a_span_took_the_time_it_was_entered_not_the_time_between() {
646 use tracing_subscriber::layer::SubscriberExt;
647
648 let took = Rc::new(RefCell::new(Vec::new()));
649 let sink = took.clone();
650 trace::observe(move |trace| {
651 if let trace::Trace::End { took, .. } = trace {
652 sink.borrow_mut().push(*took);
653 }
654 });
655
656 let subscriber = tracing_subscriber::registry().with(layer());
657 tracing::subscriber::with_default(subscriber, || {
658 let span = tracing::info_span!("polled");
659 for _ in 0..2 {
660 let _entered = span.enter();
661 std::thread::sleep(std::time::Duration::from_millis(5));
662 }
663 std::thread::sleep(std::time::Duration::from_millis(40));
664 });
665 trace::stop_observing();
666
667 let took = *took.borrow().first().expect("the span closed");
668 assert!(
669 took >= std::time::Duration::from_millis(10) && took < std::time::Duration::from_millis(40),
670 "two entries of five milliseconds, forty idle after: {took:?}"
671 );
672 }
673
674 #[test]
675 fn a_span_written_elsewhere_is_not_traced() {
676 use tracing_subscriber::layer::SubscriberExt;
677
678 let seen = Rc::new(RefCell::new(0));
679 let sink = seen.clone();
680 trace::observe(move |_| *sink.borrow_mut() += 1);
681
682 let subscriber = tracing_subscriber::registry().with(layer());
683 tracing::subscriber::with_default(subscriber, || {
684 let span = tracing::span!(target: "guinea::handle", tracing::Level::INFO, "a guinea point");
685 let _entered = span.enter();
686 });
687 trace::stop_observing();
688
689 assert_eq!(*seen.borrow(), 0);
690 }
691
692 #[test]
693 fn only_a_file_of_the_workspace_is_the_applications() {
694 assert!(written_here(Some(file!())), "{}", file!());
695 assert!(!written_here(Some(concat!(env!("CARGO_MANIFEST_DIR"), "/src/lib.rs"))));
696 assert!(!written_here(None));
697 }
698
699 #[test]
700 fn an_application_panel_belongs_to_no_root() {
701 let guard = contribute_to_app(test_panel);
702 assert_eq!(for_app().len(), 1);
703 assert!(for_root(0).is_empty());
704
705 drop(guard);
706 assert!(for_app().is_empty());
707 }
708}