//! Searching.

use std::borrow::Cow;
use std::cmp::min;
use std::ops::RangeInclusive;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, RwLock};
use std::thread;
use std::time;

use bit_set::BitSet;
use lazy_static::lazy_static;
use regex::bytes::{NoExpand, Regex};
use termwiz::cell::CellAttributes;
use termwiz::color::AnsiColor;
use termwiz::surface::change::Change;
use termwiz::surface::Position;
use unicode_width::UnicodeWidthStr;

use crate::error::Error;
use crate::event::{Event, EventSender};
use crate::file::{File, FileInfo};
use crate::overstrike;

const SEARCH_BATCH_SIZE: usize = 10000;

lazy_static! {
    /// Regex for detecting and removing escape sequences during search.
    pub(crate) static ref ESCAPE_SEQUENCE: Regex = Regex::new("\x1B\\[[0123456789:;\\[?!\"'#%()*+ ]{0,32}m").unwrap();
}

/// What kind of search to perform.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(crate) enum SearchKind {
    First,
    FirstAfter(usize),
    FirstBefore(usize),
}

/// Motion when changing search matches.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(crate) enum MatchMotion {
    First,
    Previous,
    PreviousLine,
    PreviousScreen,
    Next,
    NextLine,
    NextScreen,
    Last,
}

/// Internal struct for searching in a file.  This is protected by an Arc so
/// that it can be accessed from both the main screen thread and also the search
/// thread.
struct SearchInner {
    pattern: String,
    kind: SearchKind,
    regex: Regex,
    matches: RwLock<Vec<(usize, usize)>>,
    matching_lines: RwLock<BitSet>,
    current_match: RwLock<Option<usize>>,
    matching_line_count: AtomicUsize,
    search_line_count: AtomicUsize,
    finished: AtomicBool,
}

/// A search for a pattern within a file.
pub(crate) struct Search {
    inner: Arc<SearchInner>,
}

impl SearchInner {
    /// Create a new SearchInner for a search.
    fn new(
        file: &File,
        pattern: &str,
        kind: SearchKind,
        event_sender: EventSender,
    ) -> Result<Arc<SearchInner>, Error> {
        let regex = Regex::new(pattern)?;
        let search = Arc::new(SearchInner {
            pattern: pattern.to_string(),
            kind,
            regex: regex.clone(),
            matches: RwLock::new(Vec::new()),
            matching_lines: RwLock::new(BitSet::new()),
            current_match: RwLock::new(None),
            matching_line_count: AtomicUsize::new(0),
            search_line_count: AtomicUsize::new(0),
            finished: AtomicBool::new(false),
        });
        thread::Builder::new()
            .name(String::from("sp-search"))
            .spawn({
                let search = search.clone();
                let file = file.clone();
                move || {
                    let mut matched = false;
                    loop {
                        let loaded = file.loaded();
                        let lines = file.lines();
                        let search_line_count = search.search_line_count.load(Ordering::SeqCst);
                        let search_limit = min(
                            search_line_count + SEARCH_BATCH_SIZE,
                            if loaded { lines } else { lines - 1 },
                        );
                        for line in search_line_count..search_limit {
                            let count = file.with_line(line, |data| {
                                // Strip trailing LF or CRLF if it is there.
                                let len = trim_trailing_newline(&data[..]);
                                let data = overstrike::convert_overstrike(&data[..len]);
                                let data = ESCAPE_SEQUENCE.replace_all(&data[..], NoExpand(b""));
                                regex.find_iter(&data[..]).count()
                            });
                            if count.unwrap_or(0) > 0 {
                                let mut matching_lines = search.matching_lines.write().unwrap();
                                matching_lines.insert(line);
                                let mut matches = search.matches.write().unwrap();
                                let first_match_index = matches.len();
                                for i in 0..count.unwrap() {
                                    matches.push((line, i));
                                }
                                search.matching_line_count.fetch_add(1, Ordering::SeqCst);
                                if !matched {
                                    if let Some(index) = match search.kind {
                                        SearchKind::First => Some(first_match_index),
                                        SearchKind::FirstAfter(offset) => {
                                            if line >= offset {
                                                Some(first_match_index)
                                            } else {
                                                None
                                            }
                                        }
                                        SearchKind::FirstBefore(offset) => {
                                            if line >= offset
                                                && first_match_index > 0
                                                && matches[first_match_index - 1].0 < offset
                                            {
                                                Some(first_match_index - 1)
                                            } else {
                                                None
                                            }
                                        }
                                    } {
                                        *search.current_match.write().unwrap() = Some(index);
                                        event_sender
                                            .send(Event::SearchFirstMatch(file.index()))
                                            .unwrap();
                                        matched = true;
                                    }
                                }
                            }
                        }
                        search
                            .search_line_count
                            .store(search_limit, Ordering::SeqCst);
                        if loaded && search_limit == lines {
                            // Searched the whole file.
                            break;
                        }
                        if !loaded && search_limit >= lines - 1 {
                            // Searched the whole file so far.  Wait for more data.
                            thread::sleep(time::Duration::from_millis(100));
                        }
                    }
                    if !matched {
                        let matches = search.matches.read().unwrap();
                        if matches.len() > 0 {
                            let index = match search.kind {
                                SearchKind::First | SearchKind::FirstAfter(_) => 0,
                                SearchKind::FirstBefore(_) => matches.len() - 1,
                            };
                            *search.current_match.write().unwrap() = Some(index);
                            event_sender
                                .send(Event::SearchFirstMatch(file.index()))
                                .unwrap();
                        }
                    }
                    search.finished.store(true, Ordering::SeqCst);
                    event_sender
                        .send(Event::SearchFinished(file.index()))
                        .unwrap();
                }
            })
            .unwrap();
        Ok(search)
    }
}

impl Search {
    /// Create a new search for a pattern.
    pub(crate) fn new(
        file: &File,
        pattern: &str,
        kind: SearchKind,
        event_sender: EventSender,
    ) -> Result<Search, Error> {
        Ok(Search {
            inner: SearchInner::new(file, pattern, kind, event_sender)?,
        })
    }

    /// Returns true if the search has finished searching the whole file.
    pub(crate) fn finished(&self) -> bool {
        self.inner.finished.load(Ordering::SeqCst)
    }

    /// Renders the search overlay line.
    pub(crate) fn render(&mut self, changes: &mut Vec<Change>, line: usize, width: usize) {
        let mut width = width;
        changes.push(Change::CursorPosition {
            x: Position::Absolute(0),
            y: Position::Absolute(line),
        });
        changes.push(Change::AllAttributes(
            CellAttributes::default()
                .set_foreground(AnsiColor::Black)
                .set_background(AnsiColor::Silver)
                .clone(),
        ));
        if width < 8 {
            // The screen is too small to write anything, just write a blank bar.
            changes.push(Change::ClearToEndOfLine(AnsiColor::Silver.into()));
            return;
        }
        changes.push(Change::Text("  ".into()));
        width -= 2;

        let matches = self.inner.matches.read().unwrap();
        let match_info = match *self.inner.current_match.read().unwrap() {
            Some(index) => Cow::Owned(format!(
                "{} of {} matches on {} lines",
                index + 1,
                matches.len(),
                self.inner.matching_line_count.load(Ordering::SeqCst),
            )),
            _ if self.inner.finished.load(Ordering::SeqCst) => Cow::Borrowed("No matches"),
            _ => Cow::Owned(format!(
                "Searched {} lines",
                self.inner.search_line_count.load(Ordering::SeqCst),
            )),
        };

        // The right-hand side is shown only if it can fit.
        let right_width = match_info.width() + 2;
        let mut left_width = width;
        if width >= right_width {
            left_width -= right_width;
        }

        // Write the left-hand side if it fits.
        match left_width {
            0 => {}
            1 => changes.push(Change::Text(" ".into())),
            _ => changes.push(Change::Text(format!(
                "{1:0$.0$} ",
                left_width - 1,
                self.inner.pattern
            ))),
        }

        // Write the right-hand side if it fits.
        if width >= right_width {
            changes.push(Change::Text(match_info.into()));
            changes.push(Change::ClearToEndOfLine(AnsiColor::Silver.into()));
        }
    }

    /// Returns the line number and match index of the current match.
    pub(crate) fn current_match(&self) -> Option<(usize, usize)> {
        let matches = self.inner.matches.read().unwrap();
        let current_match_index = self.inner.current_match.read().unwrap();
        current_match_index.map(|index| matches[index])
    }

    /// Moves to another match if there is one.
    ///
    /// `scope` describes visible lines of the file on screen.
    /// It is used for `*Screen` movements.
    pub(crate) fn move_match(&mut self, motion: MatchMotion, scope: RangeInclusive<usize>) {
        let matches = self.inner.matches.read().unwrap();
        if matches.len() > 0 {
            let mut current_match_index = self.inner.current_match.write().unwrap();
            if let Some(ref mut index) = *current_match_index {
                // If the current match is within `line_scope`, then `*Screen` is just `*` movement.
                let need_seek = matches!(
                    motion,
                    MatchMotion::NextScreen | MatchMotion::PreviousScreen
                ) && !scope.contains(&matches[*index].0);
                match motion {
                    MatchMotion::First => *index = 0,
                    MatchMotion::PreviousLine => {
                        let match_index = matches[*index].1;
                        if match_index < *index {
                            *index -= match_index + 1;
                        }
                    }
                    MatchMotion::Previous | MatchMotion::PreviousScreen if *index > 0 => {
                        *index -= 1
                    }
                    MatchMotion::Next | MatchMotion::NextScreen if *index < matches.len() - 1 => {
                        *index += 1
                    }
                    MatchMotion::NextLine => {
                        let line_index = matches[*index].0;
                        let mut new_index = *index;
                        while new_index < matches.len() - 1 && matches[new_index].0 == line_index {
                            new_index += 1;
                        }
                        if matches[new_index].0 != line_index {
                            *index = new_index;
                        }
                    }
                    MatchMotion::Last => *index = matches.len() - 1,
                    _ => {}
                }

                // Attempt to satisfy the scope limit.
                if need_seek {
                    match motion {
                        MatchMotion::NextScreen => {
                            let mut candidate_index = *index;
                            if matches[candidate_index].0 > *scope.end() {
                                // Re-search from the beginning.
                                candidate_index = 0;
                            }
                            // Search forward.
                            while candidate_index < matches.len() - 1 {
                                if matches[candidate_index].0 >= *scope.start() {
                                    *index = candidate_index;
                                    break;
                                }
                                candidate_index += 1;
                            }
                        }
                        MatchMotion::PreviousScreen => {
                            let mut candidate_index = *index;
                            if matches[candidate_index].0 < *scope.start() {
                                // Re-search from the end.
                                candidate_index = matches.len() - 1;
                            }
                            // Search backward.
                            while candidate_index > 0 {
                                if matches[candidate_index].0 <= *scope.end() {
                                    *index = candidate_index;
                                    break;
                                }
                                candidate_index -= 1;
                            }
                        }
                        _ => {}
                    }
                }
            }
        }
    }

    /// Returns the lines in the given range that match.
    pub(crate) fn matching_lines(&self, start: usize, end: usize) -> Vec<usize> {
        let mut lines = Vec::new();
        let matching_lines = self.inner.matching_lines.read().unwrap();
        for line in start..end {
            if matching_lines.contains(line) {
                lines.push(line);
            }
        }
        lines
    }

    /// Returns the number of searched lines.
    pub(crate) fn searched_lines(&self) -> usize {
        self.inner.search_line_count.load(Ordering::SeqCst)
    }

    /// Returns the Regex used for this search.
    pub(crate) fn regex(&self) -> &Regex {
        &self.inner.regex
    }

    /// Returns true if the line index matches the search
    pub(crate) fn line_matches(&self, line_index: usize) -> bool {
        self.inner
            .matching_lines
            .read()
            .unwrap()
            .contains(line_index)
    }
}

pub(crate) fn trim_trailing_newline(data: impl AsRef<[u8]>) -> usize {
    let data = data.as_ref();
    let mut len = data.len();
    if len > 0 && data[len - 1] == b'\n' {
        len -= 1;
        if len > 0 && data[len - 1] == b'\r' {
            len -= 1;
        }
    }
    len
}


//! Utilities.

use std::borrow::Cow;

use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;

/// Returns the maximum width in characters of a number.
pub(crate) fn number_width(number: usize) -> usize {
    let mut width = 1;
    let mut limit = 10;
    while limit <= number {
        limit *= 10;
        width += 1;
    }
    width
}

/// Truncates a string to a column offset and width.
pub(crate) fn truncate_string<'a>(
    text: impl Into<Cow<'a, str>>,
    offset: usize,
    width: usize,
) -> String {
    let text = text.into();
    if offset > 0 || width < text.width() {
        let mut column = 0;
        let mut maybe_start_index = None;
        let mut maybe_end_index = None;
        let mut start_pad = 0;
        let mut end_pad = 0;
        for (i, g) in text.grapheme_indices(true) {
            let w = g.width();
            if w != 0 {
                if column >= offset && maybe_start_index.is_none() {
                    maybe_start_index = Some(i);
                    start_pad = column - offset;
                }
                if column + w > offset + width && maybe_end_index.is_none() {
                    maybe_end_index = Some(i);
                    end_pad = offset + width - column;
                    break;
                }
                column += w;
            }
        }
        let start_index = maybe_start_index.unwrap_or_else(|| text.len());
        let end_index = maybe_end_index.unwrap_or_else(|| text.len());
        format!(
            "{0:1$.1$}{3}{0:2$.2$}",
            "",
            start_pad,
            end_pad,
            &text[start_index..end_index]
        )
    } else {
        text.into_owned()
    }
}


//! Default keymap

keymap! {
    CTRL 'C', 'q', ('Q') => Quit;
    Escape => Cancel;
    CTRL 'L', 'r' => Refresh;
    CTRL 'R' => ToggleRuler;
    UpArrow, 'k', (CTRL 'K'), (CTRL 'P') => ScrollUpLines(1);
    DownArrow, 'j', (CTRL 'N'), Enter => ScrollDownLines(1);
    SHIFT UpArrow, (ApplicationUpArrow) => ScrollUpScreenFraction(4);
    SHIFT DownArrow, (ApplicationDownArrow) => ScrollDownScreenFraction(4);
    CTRL UpArrow, 'u', CTRL 'U' => ScrollUpScreenFraction(2);
    CTRL DownArrow, 'd', CTRL 'D' => ScrollDownScreenFraction(2);
    PageUp, Backspace, 'b', CTRL 'B', ALT 'v' => ScrollUpScreenFraction(1);
    PageDown, ' ', 'f', CTRL 'F', CTRL 'V' => ScrollDownScreenFraction(1);
    Home, 'g', '<' => ScrollToTop;
    End, 'F', 'G', '>' => ScrollToBottom;
    LeftArrow => ScrollLeftColumns(4);
    RightArrow => ScrollRightColumns(4);
    SHIFT LeftArrow => ScrollLeftScreenFraction(4);
    SHIFT RightArrow => ScrollRightScreenFraction(4);
    '[', SHIFT Tab => PreviousFile;
    ']', Tab => NextFile;
    'h', F 1 => Help;
    '#' => ToggleLineNumbers;
    '\\' => ToggleLineWrapping;
    ':', '%' => PromptGoToLine;
    '/' => PromptSearchForwards;
    '?' => PromptSearchBackwards;
    ',' => PreviousMatch;
    '.' => NextMatch;
    'p', ('N') => PreviousMatchScreen;
    'n' => NextMatchScreen;
    '(' => FirstMatch;
    ')' => LastMatch;
    '0' => AppendDigitToRepeatCount(0);
    '1' => AppendDigitToRepeatCount(1);
    '2' => AppendDigitToRepeatCount(2);
    '3' => AppendDigitToRepeatCount(3);
    '4' => AppendDigitToRepeatCount(4);
    '5' => AppendDigitToRepeatCount(5);
    '6' => AppendDigitToRepeatCount(6);
    '7' => AppendDigitToRepeatCount(7);
    '8' => AppendDigitToRepeatCount(8);
    '9' => AppendDigitToRepeatCount(9);
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

// Buck target names are different from ".thrift" file names.
// Re-export
pub use anyhow;
pub use edenfs;
pub use edenfs_config;
pub use fb303_core;
pub use fbthrift;
pub use futures;
pub use hgclient_conf;
pub use thiserror;


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! Analyze tracing data for edenscm
//!
//! This is edenscm application specific. It's not a general purposed library.

mod tables;
pub use tables::extract_tables;


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! Analyze tracing data for edenscm
//!
//! This is edenscm application specific. It's not a general purposed library.

// use std::borrow::Cow;
use std::collections::BTreeMap as Map;

use serde_json::Value;
use tracing_collector::model::IndexMap;
use tracing_collector::model::TreeSpan;
use tracing_collector::model::TreeSpans;

type Row = Map<String, Value>;
type Rows = Vec<Row>;
type Tables = Map<String, Rows>;
type TidSpans<'a> = IndexMap<(u64, u64), TreeSpans<&'a str>>;

// TODO: Make things more configurable.

/// Extract rows from tracing data. Output format is similar to NoSQL tables:
///
/// ```plain,ignore
/// {table_name: [{column_name: column_data}]}
/// ```
pub fn extract_tables(tid_spans: &TidSpans) -> Tables {
    let mut tables = Map::new();
    extract_dev_command_timers(&mut tables, tid_spans);
    extract_other_tables(&mut tables, tid_spans);
    tables
}

fn extract_dev_command_timers<'a>(tables: &mut Tables, tid_spans: &TidSpans) {
    let mut row = Row::new();
    let toint = |value: &str| -> Value { value.parse::<i64>().unwrap_or_default().into() };

    for spans in tid_spans.values() {
        for span in spans.walk() {
            match span.meta.get("name").cloned().unwrap_or("") {
                // By hgcommands, run.rs
                "Run Command" => {
                    let duration = span.duration_millis().unwrap_or(0);
                    row.insert("command_duration".into(), duration.into());
                    row.insert("elapsed".into(), duration.into());

                    for (&name, &value) in span.meta.iter() {
                        match name {
                            "nice" => {
                                row.insert("nice".into(), toint(value));
                            }
                            "version" => {
                                // Truncate the "version" string. This matches the old telemetry behavior.
                                row.insert("version".into(), value[..34.min(value.len())].into());
                            }
                            "max_rss" => {
                                row.insert("maxrss".into(), toint(value));
                            }
                            "exit_code" => {
                                row.insert("errorcode".into(), toint(value));
                            }
                            "parent_names" => {
                                if let Ok(names) = serde_json::from_str::<Vec<String>>(value) {
                                    let name = names.get(0).cloned().unwrap_or_default();
                                    row.insert("parent".into(), name.into());
                                }
                            }
                            "args" => {
                                if let Ok(args) = serde_json::from_str::<Vec<String>>(value) {
                                    // Normalize the first argument to "hg".
                                    let mut full = "hg".to_string();
                                    for arg in args.into_iter().skip(1) {
                                        // Keep the length bounded.
                                        if full.len() + arg.len() >= 256 {
                                            full += " (truncated)";
                                            break;
                                        }
                                        full += &" ";
                                        // TODO: Use shell_escape once in tp2.
                                        // full += &shell_escape::unix::escape(Cow::Owned(arg));
                                        full += &arg;
                                    }
                                    row.insert("fullcommand".into(), full.into());
                                }
                            }
                            _ => {}
                        }
                    }
                }

                // The "log:command-row" event is used by code that wants to
                // log to columns of the main command row easily.
                "log:command-row" if span.is_event => {
                    extract_span(&span, &mut row);
                }

                _ => {}
            }
        }
    }

    tables.insert("dev_command_timers".into(), vec![row]);
}

fn extract_other_tables<'a>(tables: &mut Tables, tid_spans: &TidSpans) {
    for spans in tid_spans.values() {
        for span in spans.walk() {
            match span.meta.get("name").cloned().unwrap_or("") {
                // The "log:create-row" event is used by code that wants to log
                // to a entire new column in a specified table.
                //
                // The event is expected to have "table", and the rest of the
                // metadata will be logged as-is.
                "log:create-row" => {
                    let table_name = match span.meta.get("table") {
                        Some(&name) => name,
                        None => continue,
                    };
                    let mut row = Row::new();
                    extract_span(span, &mut row);
                    tables.entry(table_name.into()).or_default().push(row);
                }
                _ => {}
            }
        }
    }
}

/// Parse a span, extract its metadata to a row.
fn extract_span(span: &TreeSpan<&str>, row: &mut Row) {
    for (&name, &value) in span.meta.iter() {
        match name {
            // Those keys are likely generated. Skip them.
            "module_path" | "cat" | "line" | "name" => {}

            // Attempt to convert it to an integer (since tracing data is
            // string only).
            _ => match value.parse::<i64>() {
                Ok(i) => {
                    row.insert(name.into(), i.into());
                }
                _ => {
                    row.insert(name.into(), value.into());
                }
            },
        }
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! See [`TracingCollector`] for the main structure.

#![allow(unused_variables)]
#![allow(dead_code)]

pub mod model;
use std::sync::Arc;

pub use model::TracingData;
use parking_lot::Mutex;
use tracing::span::Attributes;
use tracing::span::Record;
use tracing::Event;
use tracing::Id;
use tracing::Subscriber;
use tracing_subscriber::layer::Context;
use tracing_subscriber::layer::Layer;

/// A `tokio/tracing` subscriber that collects tracing data to [`TracingData`].
/// [`TracingData`] is independent from `tokio/tracing`. See its docstring for
/// more details.
pub struct TracingCollector {
    data: Arc<Mutex<TracingData>>,
}

impl TracingCollector {
    pub fn new(data: Arc<Mutex<TracingData>>) -> Self {
        Self { data }
    }
}

impl<S: Subscriber> Layer<S> for TracingCollector {
    fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
        let callsite_id = attrs.metadata().callsite();
        let mut data = self.data.lock();
        let count = data.callsite_entered.entry(callsite_id).or_default();
        *count += 1;
        if *count < data.max_span_ref_count {
            let espan_id = data.new_span(attrs);
            data.insert_id_mapping(id, espan_id);
        }
    }

    fn on_record(&self, span_id: &Id, values: &Record<'_>, _ctx: Context<'_, S>) {
        let mut data = self.data.lock();
        if let Some(espan_id) = data.get_espan_id_from_trace(span_id) {
            data.record(&espan_id.into(), values);
        }
    }

    fn on_follows_from(&self, span_id: &Id, follows: &Id, _ctx: Context<'_, S>) {
        let mut data = self.data.lock();
        if let Some(espan_id) = data.get_espan_id_from_trace(span_id) {
            if let Some(follows_espan_id) = data.get_espan_id_from_trace(follows) {
                data.record_follows_from(&espan_id.into(), &follows_espan_id.into());
            }
        }
    }

    fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
        let callsite_id = event.metadata().callsite();
        let mut data = self.data.lock();
        let count = data.callsite_entered.entry(callsite_id).or_default();
        *count += 1;
        if *count < data.max_span_ref_count {
            data.event(event)
        }
    }

    fn on_enter(&self, span_id: &Id, _ctx: Context<'_, S>) {
        let mut data = self.data.lock();
        if let Some(espan_id) = data.get_espan_id_from_trace(span_id) {
            data.enter(&espan_id.into());
        }
    }

    fn on_exit(&self, span_id: &Id, _ctx: Context<'_, S>) {
        let mut data = self.data.lock();
        if let Some(espan_id) = data.get_espan_id_from_trace(span_id) {
            data.exit(&espan_id.into());
        }
    }
}

#[cfg(test)]
mod tests {
    use tracing::instrument;
    use tracing_subscriber::layer::SubscriberExt;
    use tracing_subscriber::Registry;

    use super::*;

    #[instrument]
    fn fib(x: u32) -> u32 {
        match x {
            0 | 1 => 1,
            2 => 2,
            _ => fib(x - 1) + fib(x - 2),
        }
    }

    #[test]
    fn test_instrument() {
        let data = TracingData::new_for_test();
        let data = Arc::new(Mutex::new(data));
        let collector = TracingCollector::new(data.clone());

        tracing::subscriber::with_default(Registry::default().with(collector), || fib(5));

        let mut data = data.lock();
        data.fixup_module_lines_for_tests();

        assert_eq!(
            data.ascii(&Default::default()),
            r#"Process _ Thread _:
Start Dur.ms | Name               Source
    2    +34 | fib                <mod> line <line>
             | - x = 5            :
    4    +18  \ fib               <mod> line <line>
               | - x = 4          :
    6    +10   \ fib              <mod> line <line>
                | - x = 3         :
    8     +2    \ fib             <mod> line <line>
                 | - x = 2        :
   12     +2    \ fib             <mod> line <line>
                 | - x = 1        :
   18     +2   \ fib              <mod> line <line>
                | - x = 2         :
   24    +10  \ fib               <mod> line <line>
               | - x = 3          :
   26     +2   \ fib              <mod> line <line>
                | - x = 2         :
   30     +2   \ fib              <mod> line <line>
                | - x = 1         :

"#
        );
    }

    #[test]
    fn test_multi_threads() {
        let data = TracingData::new_for_test();
        let data = Arc::new(Mutex::new(data));
        let collector = TracingCollector::new(data.clone());

        tracing::subscriber::with_default(Registry::default().with(collector), || fib(0));
        let cloned = data.clone();
        let thread = std::thread::spawn(|| {
            let collector = TracingCollector::new(cloned);
            tracing::subscriber::with_default(Registry::default().with(collector), || fib(3));
        });
        thread.join().unwrap();

        let cloned = data.clone();
        let thread = std::thread::spawn(|| {
            let collector = TracingCollector::new(cloned);
            tracing::subscriber::with_default(Registry::default().with(collector), || fib(2));
        });
        thread.join().unwrap();
        data.lock().fixup_module_lines_for_tests();

        assert_eq!(
            data.lock().ascii(&Default::default()),
            r#"Process _ Thread _:
Start Dur.ms | Name               Source
    2     +2 | fib                <mod> line <line>
             | - x = 0            :

Process _ Thread _:
Start Dur.ms | Name               Source
    6    +10 | fib                <mod> line <line>
             | - x = 3            :
    8     +2  \ fib               <mod> line <line>
               | - x = 2          :
   12     +2  \ fib               <mod> line <line>
               | - x = 1          :

Process _ Thread _:
Start Dur.ms | Name               Source
   18     +2 | fib                <mod> line <line>
             | - x = 2            :

"#
        );
    }

    #[test]
    fn test_span_count_limit() {
        let mut data = TracingData::new_for_test();
        data.max_span_ref_count = 5;
        let data = Arc::new(Mutex::new(data));
        let collector = TracingCollector::new(data.clone());

        tracing::subscriber::with_default(Registry::default().with(collector), || fib(10));
        data.lock().fixup_module_lines_for_tests();

        // fib(6) ... are not logged.
        assert_eq!(
            data.lock().ascii(&Default::default()),
            r#"Process _ Thread _:
Start Dur.ms | Name               Source
    2    +14 | fib                <mod> line <line>
             | - x = 10           :
    4    +10 | fib                <mod> line <line>
             | - x = 9            :
    6     +6 | fib                <mod> line <line>
             | - x = 8            :
    8     +2 | fib                <mod> line <line>
             | - x = 7            :

"#
        );
    }

    #[test]
    fn test_log_count_limit() {
        let mut data = TracingData::new_for_test();
        data.max_span_ref_count = 5;
        let data = Arc::new(Mutex::new(data));
        let collector = TracingCollector::new(data.clone());

        let counts = tracing::subscriber::with_default(Registry::default().with(collector), || {
            (0..10)
                .map(|_| {
                    tracing::info!("log something");
                    data.lock().eventus_len_for_tests()
                })
                .collect::<Vec<usize>>()
        });

        // Repetitive logs are ignored.
        assert_eq!(counts, [1, 2, 3, 4, 4, 4, 4, 4, 4, 4]);
    }
}