Per-command timings for script runs and batches (#1868)

Thanks @DuBearte.
This commit is contained in:
DuBearte authored and GitHub committed 2026-10-17 07:26:20 -04:00
1 parent d1b96100d3
commit b04e0801d6
17 files changed
+526 -37

No files matched your search

+27 -8
View File
@@ -1,7 +1,7 @@
//! SolidCraft headless command line.
//!
//! ```text
//! solidcraft-cli run <script.json|design.solidcraft|part.step|mesh.3mf> [--in design|part.step|mesh.3mf] [--out FILE]... [--save FILE] [--quiet] [--results] [--json-out FILE] [--allow-errors]
//! solidcraft-cli run <script.json|design.solidcraft|part.step|mesh.3mf> [--in design|part.step|mesh.3mf] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]
//! solidcraft-cli eval <script.json|design.solidcraft|part.step> [--json-out FILE] (alias: inspect) measurements as JSON
//! solidcraft-cli script <design.solidcraft> [--out script.json] the design's history as a command script
//! solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] [--display LEVEL [--edit NAME EXPR]] loading timings as JSON lines
@@ -37,9 +37,11 @@ use solidcraft_engine::Session;
use solidcraft_engine::view::Guides;
const USAGE: &str = "usage:
solidcraft-cli run <script.json|design.solidcraft|part.step> [--in design.solidcraft|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--json-out FILE] [--allow-errors]
solidcraft-cli run <script.json|design.solidcraft|part.step> [--in design.solidcraft|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]
solidcraft-cli run --in <design.solidcraft|part.step> [--out FILE]...
(a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)
(--timings prints {timings: [{index, command, ms}], total_ms} for a script, with --results its results too;
--profile adds each command's profile: {boolean_ms, tessellate_ms, other_ms}, the kernel's share)
solidcraft-cli eval <script.json|design.solidcraft|part.step> [--json-out FILE] (alias: inspect; or --in FILE)
(--json-out FILE writes the JSON that run (with --results, else the measurement) and eval print to FILE instead of stdout,
as plain UTF-8: a shell's > can re-encode it, e.g. Windows PowerShell adds a byte-order mark)
@@ -180,7 +182,11 @@ fn positional(args: &[String]) -> Option<&String> {
let mut i = 0;
while let Some(a) = args.get(i) {
if a.starts_with("--") {
i += if matches!(a.as_str(), "--quiet" | "--results" | "--allow-errors" | "--no-grid" | "--no-axes") { 1 } else { 2 };
i += if matches!(a.as_str(), "--quiet" | "--results" | "--timings" | "--profile" | "--allow-errors" | "--no-grid" | "--no-axes") {
1
} else {
2
};
continue;
}
return Some(a);
@@ -224,15 +230,28 @@ fn cmd_run(args: &[String]) -> Result<(), String> {
if path.is_none() && base.is_none() {
return Err(USAGE.to_string());
}
let (mut s, results) = if args.iter().any(|a| a == "--results") {
// Collect every command's result (scripts only), then process requested files too.
let want_results = args.iter().any(|a| a == "--results");
// `--profile` times each command and splits out its kernel time; it implies `--timings`.
let profile = args.iter().any(|a| a == "--profile");
let timings = profile || args.iter().any(|a| a == "--timings");
let (mut s, results) = if want_results || timings {
// Collect every command's result or time (scripts only), then process requested files too.
let mut s = match base {
Some(b) => load(b)?,
None => Session::default(),
};
let v = read_json(path.ok_or("--results needs a script")?)?;
let r = s.run_script(&v).map_err(|e| e.to_string())?;
(s, Some(Value::Array(r)))
let needs = if want_results { "--results needs a script" } else { "--timings needs a script" };
let v = read_json(path.ok_or(needs)?)?;
let out = if timings {
let (r, mut t) = s.run_script_timed(&v, profile).map_err(|e| e.to_string())?;
if want_results && let Some(o) = t.as_object_mut() {
o.insert("results".into(), Value::Array(r));
}
t
} else {
Value::Array(s.run_script(&v).map_err(|e| e.to_string())?)
};
(s, Some(out))
} else {
(open(base, path)?, None)
};
+30
View File
@@ -179,3 +179,33 @@ fn unknown_command_suggests_the_closest_id() {
assert_eq!(output.status.code(), Some(1));
assert!(String::from_utf8_lossy(&output.stderr).contains("did you mean `doc.open`?"), "{}", String::from_utf8_lossy(&output.stderr));
}
/// #1833: `--timings` reports each command's time instead of the measurement, `--profile` adds
/// the kernel's share, `--results` puts the results beside them, and files are still written.
#[test]
fn timings_and_profile_report_each_command() {
let dir = Scratch::new();
let saved = dir.0.join("timed.solidcraft");
let t = json_output(cli().arg("run").arg(dir.script()).arg("--timings").arg("--save").arg(&saved).output().unwrap());
let rows = t["timings"].as_array().unwrap();
assert_eq!(rows.len(), 1);
assert_eq!((rows[0]["index"].as_u64(), rows[0]["command"].as_str()), (Some(0), Some("solid.box")));
assert!(rows[0]["ms"].as_f64().is_some() && t["total_ms"].as_f64().is_some(), "{t}");
assert!(rows[0].get("profile").is_none() && t.get("results").is_none(), "{t}");
assert_eq!(body_count(&saved), 1);
// --profile alone implies --timings; a flag before the script doesn't eat it.
let t = json_output(cli().arg("run").arg("--profile").arg(dir.script()).arg("--results").output().unwrap());
let p = &t["timings"][0]["profile"];
for k in ["boolean_ms", "tessellate_ms", "other_ms"] {
assert!(p[k].as_f64().is_some_and(|ms| ms >= 0.0), "{k}: {t}");
}
assert_eq!(t["results"].as_array().map(Vec::len), Some(1), "{t}");
// --results alone stays a plain array.
assert!(json_output(cli().arg("run").arg(dir.script()).arg("--results").output().unwrap()).is_array());
let output = cli().arg("run").arg("--in").arg(&saved).arg("--timings").output().unwrap();
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("--timings needs a script"));
}