cu_profiler_core/baseline/
mod.rs1mod compare;
7mod fingerprint;
8
9pub use compare::BaselineComparison;
10pub use fingerprint::{Fingerprint, hash_bytes, hash_str};
11
12use std::collections::BTreeMap;
13
14use serde::{Deserialize, Serialize};
15
16use crate::confidence::ConfidenceLevel;
17use crate::metadata::InstrumentationMode;
18
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21pub struct BaselineRecord {
22 pub scenario: String,
24 pub actual_units: u64,
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub budget: Option<u64>,
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub timestamp: Option<String>,
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub git_commit: Option<String>,
35 pub fingerprint: Fingerprint,
37 #[serde(default, skip_serializing_if = "Vec::is_empty")]
39 pub solana_versions: Vec<String>,
40 pub profiler_version: String,
42 pub instrumentation: InstrumentationMode,
44 pub confidence: ConfidenceLevel,
46 #[serde(default)]
48 pub approved: bool,
49}
50
51#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
54pub struct BaselineStore {
55 #[serde(default = "default_version")]
57 pub version: u32,
58 #[serde(default)]
60 pub records: BTreeMap<String, BaselineRecord>,
61}
62
63fn default_version() -> u32 {
64 1
65}
66
67impl BaselineStore {
68 #[must_use]
70 pub fn new() -> Self {
71 Self {
72 version: default_version(),
73 records: BTreeMap::new(),
74 }
75 }
76
77 pub fn insert(&mut self, record: BaselineRecord) {
79 self.records.insert(record.scenario.clone(), record);
80 }
81
82 #[must_use]
84 pub fn get(&self, scenario: &str) -> Option<&BaselineRecord> {
85 self.records.get(scenario)
86 }
87
88 pub fn approve(&mut self, scenario: &str) -> bool {
90 match self.records.get_mut(scenario) {
91 Some(r) => {
92 r.approved = true;
93 true
94 }
95 None => false,
96 }
97 }
98
99 #[cfg(feature = "json")]
101 pub fn to_json(&self) -> crate::Result<String> {
102 Ok(serde_json::to_string_pretty(self)?)
103 }
104
105 #[cfg(feature = "json")]
107 pub fn from_json(s: &str) -> crate::Result<Self> {
108 Ok(serde_json::from_str(s)?)
109 }
110
111 #[cfg(feature = "json")]
113 pub fn load(path: &std::path::Path) -> crate::Result<Self> {
114 match std::fs::read_to_string(path) {
115 Ok(s) => Self::from_json(&s),
116 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Self::new()),
117 Err(e) => Err(e.into()),
118 }
119 }
120
121 #[cfg(feature = "json")]
123 pub fn save(&self, path: &std::path::Path) -> crate::Result<()> {
124 if let Some(parent) = path.parent() {
125 std::fs::create_dir_all(parent)?;
126 }
127 std::fs::write(path, self.to_json()?)?;
128 Ok(())
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 fn record(name: &str, cu: u64) -> BaselineRecord {
137 BaselineRecord {
138 scenario: name.into(),
139 actual_units: cu,
140 budget: Some(100_000),
141 timestamp: None,
142 git_commit: None,
143 fingerprint: Fingerprint::new(name, "fix", "cfg", None),
144 solana_versions: Vec::new(),
145 profiler_version: "0.1.0".into(),
146 instrumentation: InstrumentationMode::Off,
147 confidence: ConfidenceLevel::High,
148 approved: false,
149 }
150 }
151
152 #[test]
153 fn insert_get_approve() {
154 let mut store = BaselineStore::new();
155 store.insert(record("swap", 95_000));
156 assert_eq!(store.get("swap").map(|r| r.actual_units), Some(95_000));
157 assert!(store.approve("swap"));
158 assert!(store.get("swap").unwrap().approved);
159 assert!(!store.approve("missing"));
160 }
161
162 #[cfg(feature = "json")]
163 #[test]
164 fn json_round_trip_is_stable() {
165 let mut store = BaselineStore::new();
166 store.insert(record("b", 2));
167 store.insert(record("a", 1));
168 let json = store.to_json().unwrap();
169 assert!(json.find("\"a\"").unwrap() < json.find("\"b\"").unwrap());
171 let back = BaselineStore::from_json(&json).unwrap();
172 assert_eq!(store, back);
173 }
174}