Skip to main content

tokmd_core/workflows/
timing.rs

1//! Phase timing helpers for core inventory workflows.
2//!
3//! These helpers are opt-in measurement surfaces. They use the same scan,
4//! model, and receipt builders as the normal workflows and return the same
5//! receipts plus phase timings, without changing default receipt schemas or CLI
6//! output.
7
8use std::path::Path;
9use std::time::Instant;
10
11use anyhow::Result;
12use serde::{Deserialize, Serialize};
13use tokmd_types::{ExportReceipt, LangReceipt, ModuleReceipt};
14
15use crate::settings::{ExportSettings, LangSettings, ModuleSettings, ScanSettings};
16use crate::{build_export_receipt, build_lang_receipt, build_module_receipt};
17
18use super::{scan_paths_or_current_dir, settings_to_scan_options};
19
20/// Timing evidence for one core workflow run.
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22pub struct WorkflowTiming {
23    /// Schema identifier for downstream tools.
24    pub schema: String,
25    /// Schema version for this timing record.
26    pub schema_version: u32,
27    /// `lang`, `module`, or `export`.
28    pub workflow: String,
29    /// Number of caller-provided scan roots. Paths are deliberately omitted.
30    pub path_count: usize,
31    /// Number of language entries returned by `tokei`.
32    pub language_count: usize,
33    /// Number of rows in the workflow's final report/data section.
34    pub row_count: usize,
35    /// Time spent in `tokmd-scan`.
36    pub scan_ms: u128,
37    /// Time spent in `tokmd-model` aggregation/selection.
38    pub model_ms: u128,
39    /// Time spent constructing the final receipt object.
40    pub receipt_ms: u128,
41    /// End-to-end time measured around the workflow body.
42    pub total_ms: u128,
43}
44
45/// A normal workflow receipt plus opt-in timing evidence.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct TimedWorkflow<T> {
48    pub receipt: T,
49    pub timing: WorkflowTiming,
50}
51
52/// Runs the language workflow and records scan/model/receipt phase timings.
53///
54/// # Errors
55/// Returns any error produced by the underlying scan or model workflow.
56pub fn timed_lang_workflow(
57    scan: &ScanSettings,
58    lang: &LangSettings,
59) -> Result<TimedWorkflow<LangReceipt>> {
60    let total_start = Instant::now();
61    let scan_opts = settings_to_scan_options(scan);
62    let paths = scan_paths_or_current_dir(scan);
63
64    let scan_start = Instant::now();
65    let languages = tokmd_scan::scan(&paths, &scan_opts)?;
66    let scan_ms = elapsed_ms(scan_start);
67    let language_count = languages.len();
68
69    let model_start = Instant::now();
70    let report = tokmd_model::create_lang_report(&languages, lang.top, lang.files, lang.children);
71    let row_count = report.rows.len();
72    let model_ms = elapsed_ms(model_start);
73
74    let receipt_start = Instant::now();
75    let receipt = build_lang_receipt(&paths, &scan_opts, lang, report);
76    let receipt_ms = elapsed_ms(receipt_start);
77
78    Ok(TimedWorkflow {
79        receipt,
80        timing: timing_record(TimingRecordInput {
81            workflow: "lang",
82            path_count: paths.len(),
83            language_count,
84            row_count,
85            scan_ms,
86            model_ms,
87            receipt_ms,
88            total_ms: elapsed_ms(total_start),
89        }),
90    })
91}
92
93/// Runs the module workflow and records scan/model/receipt phase timings.
94///
95/// # Errors
96/// Returns any error produced by the underlying scan or model workflow.
97pub fn timed_module_workflow(
98    scan: &ScanSettings,
99    module: &ModuleSettings,
100) -> Result<TimedWorkflow<ModuleReceipt>> {
101    let total_start = Instant::now();
102    let scan_opts = settings_to_scan_options(scan);
103    let paths = scan_paths_or_current_dir(scan);
104
105    let scan_start = Instant::now();
106    let languages = tokmd_scan::scan(&paths, &scan_opts)?;
107    let scan_ms = elapsed_ms(scan_start);
108    let language_count = languages.len();
109
110    let model_start = Instant::now();
111    let report = tokmd_model::create_module_report(
112        &languages,
113        &module.module_roots,
114        module.module_depth,
115        module.children,
116        module.top,
117    );
118    let row_count = report.rows.len();
119    let model_ms = elapsed_ms(model_start);
120
121    let receipt_start = Instant::now();
122    let receipt = build_module_receipt(&paths, &scan_opts, module, report);
123    let receipt_ms = elapsed_ms(receipt_start);
124
125    Ok(TimedWorkflow {
126        receipt,
127        timing: timing_record(TimingRecordInput {
128            workflow: "module",
129            path_count: paths.len(),
130            language_count,
131            row_count,
132            scan_ms,
133            model_ms,
134            receipt_ms,
135            total_ms: elapsed_ms(total_start),
136        }),
137    })
138}
139
140/// Runs the export workflow and records scan/model/receipt phase timings.
141///
142/// # Errors
143/// Returns any error produced by the underlying scan or model workflow.
144pub fn timed_export_workflow(
145    scan: &ScanSettings,
146    export: &ExportSettings,
147) -> Result<TimedWorkflow<ExportReceipt>> {
148    let total_start = Instant::now();
149    let scan_opts = settings_to_scan_options(scan);
150    let paths = scan_paths_or_current_dir(scan);
151    let strip_prefix = export.strip_prefix.as_deref();
152
153    let scan_start = Instant::now();
154    let languages = tokmd_scan::scan(&paths, &scan_opts)?;
155    let scan_ms = elapsed_ms(scan_start);
156    let language_count = languages.len();
157
158    let model_start = Instant::now();
159    let data = tokmd_model::create_export_data(
160        &languages,
161        &export.module_roots,
162        export.module_depth,
163        export.children,
164        strip_prefix.map(Path::new),
165        export.min_code,
166        export.max_rows,
167    );
168    let row_count = data.rows.len();
169    let model_ms = elapsed_ms(model_start);
170
171    let receipt_start = Instant::now();
172    let receipt = build_export_receipt(&paths, &scan_opts, export, data);
173    let receipt_ms = elapsed_ms(receipt_start);
174
175    Ok(TimedWorkflow {
176        receipt,
177        timing: timing_record(TimingRecordInput {
178            workflow: "export",
179            path_count: paths.len(),
180            language_count,
181            row_count,
182            scan_ms,
183            model_ms,
184            receipt_ms,
185            total_ms: elapsed_ms(total_start),
186        }),
187    })
188}
189
190struct TimingRecordInput {
191    workflow: &'static str,
192    path_count: usize,
193    language_count: usize,
194    row_count: usize,
195    scan_ms: u128,
196    model_ms: u128,
197    receipt_ms: u128,
198    total_ms: u128,
199}
200
201fn timing_record(input: TimingRecordInput) -> WorkflowTiming {
202    WorkflowTiming {
203        schema: "tokmd.workflow_timing.v1".to_string(),
204        schema_version: 1,
205        workflow: input.workflow.to_string(),
206        path_count: input.path_count,
207        language_count: input.language_count,
208        row_count: input.row_count,
209        scan_ms: input.scan_ms,
210        model_ms: input.model_ms,
211        receipt_ms: input.receipt_ms,
212        total_ms: input.total_ms,
213    }
214}
215
216fn elapsed_ms(start: Instant) -> u128 {
217    start.elapsed().as_millis()
218}
219
220#[cfg(test)]
221mod tests {
222    use std::fs;
223
224    use anyhow::Result;
225
226    use super::*;
227
228    #[test]
229    fn timed_lang_workflow_preserves_receipt_and_records_phases() -> Result<()> {
230        let temp = tempfile::tempdir()?;
231        fs::write(temp.path().join("main.rs"), "fn main() {}\n")?;
232        let scan = ScanSettings::for_paths(vec![temp.path().to_string_lossy().into_owned()]);
233        let result = timed_lang_workflow(&scan, &LangSettings::default())?;
234
235        assert_eq!(result.receipt.mode, "lang");
236        assert_eq!(result.timing.workflow, "lang");
237        assert_eq!(result.timing.path_count, 1);
238        assert_eq!(result.timing.language_count, 1);
239        assert_eq!(result.timing.row_count, result.receipt.report.rows.len());
240        assert_eq!(result.timing.schema, "tokmd.workflow_timing.v1");
241        Ok(())
242    }
243
244    #[test]
245    fn timed_module_and_export_workflows_record_rows_without_paths() -> Result<()> {
246        let temp = tempfile::tempdir()?;
247        fs::create_dir_all(temp.path().join("src"))?;
248        fs::write(temp.path().join("src").join("lib.rs"), "pub fn lib() {}\n")?;
249        let scan = ScanSettings::for_paths(vec![temp.path().to_string_lossy().into_owned()]);
250
251        let module = timed_module_workflow(&scan, &ModuleSettings::default())?;
252        let export = timed_export_workflow(&scan, &ExportSettings::default())?;
253
254        assert_eq!(module.timing.workflow, "module");
255        assert_eq!(module.timing.row_count, module.receipt.report.rows.len());
256        assert_eq!(export.timing.workflow, "export");
257        assert_eq!(export.timing.row_count, export.receipt.data.rows.len());
258        assert_eq!(export.timing.path_count, 1);
259        Ok(())
260    }
261}