1use 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22pub struct WorkflowTiming {
23 pub schema: String,
25 pub schema_version: u32,
27 pub workflow: String,
29 pub path_count: usize,
31 pub language_count: usize,
33 pub row_count: usize,
35 pub scan_ms: u128,
37 pub model_ms: u128,
39 pub receipt_ms: u128,
41 pub total_ms: u128,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct TimedWorkflow<T> {
48 pub receipt: T,
49 pub timing: WorkflowTiming,
50}
51
52pub 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
93pub 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
140pub 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}