1use crate::util;
4use anyhow::{Context, Result, ensure};
5use serde_json::{Value, json};
6use sha2::{Digest, Sha256};
7use std::{
8 ffi::OsStr,
9 path::{Path, PathBuf},
10 process::Command,
11};
12
13pub(super) struct Model {
15 pub reference: String,
16 pub directory: PathBuf,
17 pub precisions: Vec<u8>,
18 pub source_directory: Option<PathBuf>,
20}
21
22impl Model {
23 pub(super) fn inspect(binary: &Path, selector: &OsStr, root: Option<&Path>) -> Result<Self> {
25 let mut command = Command::new(binary);
26
27 command.arg("inspect").arg(selector).arg("--json");
28
29 if let Some(root) = root {
30 command.arg("--root").arg(root);
31 }
32
33 let output = command.output().context("inspecting benchmark model")?;
34
35 ensure!(
36 output.status.success(),
37 "model inspection failed: {}",
38 String::from_utf8_lossy(&output.stderr).trim()
39 );
40
41 Self::from_inspection(
42 &serde_json::from_slice(&output.stdout).context("decoding model inspection")?,
43 )
44 }
45
46 fn from_inspection(value: &Value) -> Result<Self> {
48 let index = &value["index"];
49 let reference = index["id"]
50 .as_str()
51 .context("inspection missing model ID")?
52 .to_owned();
53 let artifact = index["artifacts"]
54 .as_array()
55 .context("inspection missing artifacts")?
56 .iter()
57 .find(|artifact| artifact["kind"] == "prepared" && artifact["available"] == true)
58 .context(
59 "model must be prepared before benchmarking; run `cherenkov prepare SOURCE`",
60 )?;
61 let directory = artifact["path"]
62 .as_str()
63 .context("prepared artifact missing path")?
64 .into();
65 let precisions = serde_json::from_value(index["precisions"].clone())
66 .context("inspection missing expert precisions")?;
67
68 Ok(Self {
69 reference,
70 directory,
71 precisions,
72 source_directory: index["source"]["path"].as_str().map(PathBuf::from),
73 })
74 }
75
76 pub(super) fn migrate_signature(&self, signature: &mut Value) {
78 if signature["model_id"].is_string() {
79 return;
80 }
81
82 let matches_id = signature["model"].as_str() == Some(&self.reference);
83 let matches_path = self.source_directory.as_ref().is_some_and(|source| {
84 let digest: String = Sha256::digest(source.as_os_str().as_encoded_bytes())
85 .iter()
86 .map(|byte| format!("{byte:02x}"))
87 .collect();
88
89 signature["model"]
90 .as_str()
91 .is_some_and(|saved| Some(saved) == source.to_str())
92 || signature["model_path_sha256"].as_str() == Some(&digest)
93 });
94
95 if !matches_id && !matches_path {
96 return;
97 }
98
99 signature["model"] = json!("<model>");
100 signature["model_id"] = json!(self.reference);
101
102 signature
103 .as_object_mut()
104 .unwrap()
105 .remove("model_path_sha256");
106
107 if let Some(metadata) = signature["model_metadata_sha256"].as_object_mut()
108 && let Some(manifest) = metadata.remove("packed/manifest.json")
109 {
110 metadata.entry("manifest.json").or_insert(manifest);
111 }
112 }
113
114 pub(super) fn metadata(&self) -> Result<Value> {
116 let mut metadata = json!({});
117
118 for name in ["config.json", "tokenizer.json"] {
119 let direct = self.directory.join(name);
120 let path = if direct.is_file() {
121 direct
122 } else {
123 self.directory
124 .parent()
125 .context("model metadata missing")?
126 .join(name)
127 };
128 metadata[name] = json!(util::digest(&path)?);
129 }
130
131 metadata["manifest.json"] = json!(util::digest(&self.directory.join("manifest.json"))?);
132
133 Ok(metadata)
134 }
135}
136
137#[cfg(test)]
138#[path = "../../tests/unit/bench_model.rs"]
139mod tests;