1use serde::{Deserialize, Serialize};
7use silicera::hardware::HardwareInfo;
8use silicera::hnep::{Confidence, HnepProfile};
9use silicera::measure::{MeasurementConfig, MeasurementEngine, MeasurementSummary};
10use silicera::Result;
11
12use crate::bench::{AlignmentBench, CacheTarget, ConcurrencyBench, FloatBench, IntegerBench, MemOpBench};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct AlignmentSample {
19 pub offset: usize,
21 pub scalar: MeasurementSummary,
23 pub chunked: MeasurementSummary,
25 pub winner: String,
27 pub improvement: f64,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct AlignmentReport {
34 pub host_brand: String,
36 pub fingerprint: Option<String>,
38 pub len: usize,
40 pub samples: Vec<AlignmentSample>,
42 pub alignment_matters: bool,
44 pub recommendation: String,
46}
47
48pub fn measure_alignment(
50 info: &HardwareInfo,
51 len: usize,
52 cfg: &MeasurementConfig,
53 min_improvement: f64,
54) -> Result<AlignmentReport> {
55 let eng = MeasurementEngine::new(cfg.clone());
56 let offsets = [0usize, 1, 7, 15, 63];
57 let mut samples = Vec::new();
58 for &offset in &offsets {
59 let b = AlignmentBench::new(len, offset);
60 let scalar = eng.measure(|| {
61 let _ = b.run_scalar();
62 })?;
63 let chunked = eng.measure(|| {
64 let _ = b.run_chunked();
65 })?;
66 let (winner, improvement) = if chunked.median_ns < scalar.median_ns * (1.0 - min_improvement)
67 {
68 (
69 "chunked".into(),
70 (scalar.median_ns - chunked.median_ns) / scalar.median_ns,
71 )
72 } else if scalar.median_ns < chunked.median_ns * (1.0 - min_improvement) {
73 (
74 "scalar".into(),
75 (chunked.median_ns - scalar.median_ns) / chunked.median_ns,
76 )
77 } else {
78 ("no_meaningful_difference".into(), 0.0)
79 };
80 samples.push(AlignmentSample {
81 offset,
82 scalar,
83 chunked,
84 winner,
85 improvement,
86 });
87 }
88 let meaningful: Vec<&str> = samples
89 .iter()
90 .filter(|s| s.winner != "no_meaningful_difference")
91 .map(|s| s.winner.as_str())
92 .collect();
93 let alignment_matters = meaningful.len() >= 2
94 && meaningful.windows(2).any(|w| w[0] != w[1])
95 || samples.iter().any(|s| {
96 s.offset != 0
97 && s.winner != "no_meaningful_difference"
98 && samples[0].winner != s.winner
99 && samples[0].winner != "no_meaningful_difference"
100 });
101 let base = samples[0].scalar.median_ns;
103 let timing_shift = samples.iter().any(|s| {
104 s.offset != 0 && (s.scalar.median_ns - base).abs() / base.max(1.0) > 0.10
105 });
106 let alignment_matters = alignment_matters || timing_shift;
107 let recommendation = if alignment_matters {
108 "Alignment materially affected measurements on this host — consider alignment in dispatch for this workload class.".into()
109 } else {
110 "No meaningful alignment-driven strategy change under these knobs — do not add dispatch complexity.".into()
111 };
112 Ok(AlignmentReport {
113 host_brand: info.brand.clone(),
114 fingerprint: info.fingerprint.as_ref().map(|f| f.value.clone()),
115 len,
116 samples,
117 alignment_matters,
118 recommendation,
119 })
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
126#[serde(rename_all = "snake_case")]
127pub enum PlacementStrategy {
128 OsDefault,
130 Compact,
132 Spread,
134}
135
136impl PlacementStrategy {
137 pub fn label(self) -> &'static str {
139 match self {
140 PlacementStrategy::OsDefault => "os_default",
141 PlacementStrategy::Compact => "compact",
142 PlacementStrategy::Spread => "spread",
143 }
144 }
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct PlacementSample {
150 pub strategy: String,
152 pub threads: usize,
154 pub summary: MeasurementSummary,
156 pub affinity_applied: bool,
158 pub notes: String,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct PlacementReport {
165 pub host_brand: String,
167 pub fingerprint: Option<String>,
169 pub logical_cpus: usize,
171 pub samples: Vec<PlacementSample>,
173 pub fastest: String,
175 pub conclusion: String,
177}
178
179pub fn measure_placement(
181 info: &HardwareInfo,
182 threads: usize,
183 iters_per_thread: u64,
184 cfg: &MeasurementConfig,
185) -> Result<PlacementReport> {
186 let eng = MeasurementEngine::new(cfg.clone());
187 let logical = info.topology.thread_count().max(1);
188 let threads = threads.clamp(1, logical);
189 let strategies = [
190 PlacementStrategy::OsDefault,
191 PlacementStrategy::Compact,
192 PlacementStrategy::Spread,
193 ];
194 let mut samples = Vec::new();
195 for strat in strategies {
196 let (masks, notes) = affinity_masks(strat, threads, logical);
197 let summary = eng.measure(|| {
198 run_placed(threads, iters_per_thread, &masks);
199 })?;
200 samples.push(PlacementSample {
201 strategy: strat.label().into(),
202 threads,
203 summary,
204 affinity_applied: masks.iter().any(|m| m.is_some()),
205 notes,
206 });
207 }
208
209 let mut fastest = "os_default".to_string();
210 let mut best = f64::INFINITY;
211 for s in &samples {
212 if s.summary.median_ns < best {
213 best = s.summary.median_ns;
214 fastest = s.strategy.clone();
215 }
216 }
217
218 let conclusion = format!(
219 "Fastest under these knobs: {fastest}. Placement changes global OS policy: no. \
220 Affinity is Silicera-owned and temporary. Do not assume compact > spread."
221 );
222 Ok(PlacementReport {
223 host_brand: info.brand.clone(),
224 fingerprint: info.fingerprint.as_ref().map(|f| f.value.clone()),
225 logical_cpus: logical,
226 samples,
227 fastest,
228 conclusion,
229 })
230}
231
232fn affinity_masks(
233 strat: PlacementStrategy,
234 threads: usize,
235 logical: usize,
236) -> (Vec<Option<usize>>, String) {
237 match strat {
238 PlacementStrategy::OsDefault => (
239 vec![None; threads],
240 "no affinity; OS schedules freely".into(),
241 ),
242 PlacementStrategy::Compact => {
243 let masks: Vec<Option<usize>> = (0..threads).map(|i| Some(i % logical)).collect();
244 (
245 masks,
246 format!("compact: logical CPUs 0..{}", threads.min(logical)),
247 )
248 }
249 PlacementStrategy::Spread => {
250 let step = (logical / threads.max(1)).max(1);
251 let masks: Vec<Option<usize>> = (0..threads)
252 .map(|i| Some((i * step) % logical))
253 .collect();
254 (
255 masks,
256 format!("spread: step={step} across {logical} logical CPUs"),
257 )
258 }
259 }
260}
261
262fn run_placed(threads: usize, iters: u64, masks: &[Option<usize>]) {
263 let mut handles = Vec::new();
264 for t in 0..threads {
265 let mask = masks.get(t).copied().flatten();
266 handles.push(std::thread::spawn(move || {
267 if let Some(cpu) = mask {
268 set_current_thread_affinity(cpu);
269 }
270 let mut x = t as u64;
271 for i in 0..iters {
272 x = x
273 .wrapping_mul(1664525)
274 .wrapping_add(1013904223)
275 .wrapping_add(i);
276 }
277 std::hint::black_box(x)
278 }));
279 }
280 for h in handles {
281 let _ = h.join();
282 }
283}
284
285#[cfg(windows)]
286mod win_affinity {
287 use std::ffi::c_void;
288
289 type Handle = *mut c_void;
290
291 #[link(name = "kernel32")]
292 extern "system" {
293 fn GetCurrentThread() -> Handle;
294 fn SetThreadAffinityMask(thread: Handle, mask: usize) -> usize;
295 }
296
297 pub fn set(logical_cpu: usize) {
298 if logical_cpu < usize::BITS as usize {
299 let mask = 1usize << logical_cpu;
300 unsafe {
302 let _ = SetThreadAffinityMask(GetCurrentThread(), mask);
303 }
304 }
305 }
306}
307
308fn set_current_thread_affinity(logical_cpu: usize) {
309 #[cfg(windows)]
310 {
311 win_affinity::set(logical_cpu);
312 }
313 #[cfg(not(windows))]
314 {
315 let _ = logical_cpu;
316 }
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct SpotCheckItem {
324 pub workload: String,
326 pub profile_winner: String,
328 pub profile_confidence: String,
330 pub observation: String,
332 pub ok: bool,
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize)]
338pub struct SpotCheckReport {
339 pub profile_label: String,
341 pub host_brand: String,
343 pub items: Vec<SpotCheckItem>,
345 pub ok_count: usize,
347 pub fail_count: usize,
349 pub summary: String,
351}
352
353pub fn spot_check_profile(
355 info: &HardwareInfo,
356 profile: &HnepProfile,
357 limit: usize,
358 cfg: &MeasurementConfig,
359) -> Result<SpotCheckReport> {
360 let eng = MeasurementEngine::new(cfg.clone());
361 let mut items = Vec::new();
362 for w in profile
363 .workloads
364 .iter()
365 .filter(|w| w.confidence != Confidence::Inconclusive)
366 .take(limit)
367 {
368 items.push(check_one(info, &eng, &w.name, &w.winner, w.confidence)?);
369 }
370 if let Some(sc) = profile.size_classes.first() {
371 if items.len() < limit {
372 let name = format!("memscan-{}", sc.class.to_ascii_lowercase());
373 items.push(check_one(
374 info,
375 &eng,
376 &name,
377 &sc.winner,
378 sc.confidence,
379 )?);
380 }
381 }
382 let ok_count = items.iter().filter(|i| i.ok).count();
383 let fail_count = items.len() - ok_count;
384 let summary = if items.is_empty() {
385 "No non-inconclusive workloads to spot-check.".into()
386 } else if fail_count == 0 {
387 format!("All {ok_count} spot-checks consistent under tiny sample.")
388 } else {
389 format!(
390 "{fail_count}/{} spot-checks unstable or disagreed — consider partial retrain.",
391 items.len()
392 )
393 };
394 Ok(SpotCheckReport {
395 profile_label: profile.header.label.clone(),
396 host_brand: info.brand.clone(),
397 items,
398 ok_count,
399 fail_count,
400 summary,
401 })
402}
403
404fn check_one(
405 info: &HardwareInfo,
406 eng: &MeasurementEngine,
407 name: &str,
408 winner: &str,
409 confidence: Confidence,
410) -> Result<SpotCheckItem> {
411 let (observation, ok) = if name.starts_with("memscan") || name.starts_with("memory") {
412 let mut op = MemOpBench::for_target(&info.topology, CacheTarget::L2);
413 let base = eng.measure(|| {
414 let _ = op.run_scan_stride();
415 })?;
416 let cand = eng.measure(|| {
417 let _ = op.run_copy();
418 })?;
419 let stable =
420 base.stability.label() != "UNSTABLE" && cand.stability.label() != "UNSTABLE";
421 (
422 format!(
423 "fresh L2 scan={:.0}ns copy={:.0}ns [{}|{}]",
424 base.median_ns,
425 cand.median_ns,
426 base.stability.label(),
427 cand.stability.label()
428 ),
429 stable,
430 )
431 } else if name.starts_with("float") {
432 let b = FloatBench::new(2048);
433 let s = eng.measure(|| {
434 let _ = b.run_baseline();
435 })?;
436 (
437 format!(
438 "float baseline median={:.0}ns {}",
439 s.median_ns,
440 s.stability.label()
441 ),
442 s.stability.label() != "UNSTABLE",
443 )
444 } else if name.starts_with("concurrency") {
445 let b = ConcurrencyBench {
446 iters: 5_000,
447 threads: 4.min(info.topology.thread_count().max(1)),
448 };
449 let base = eng.measure(|| {
450 let _ = b.run_baseline();
451 })?;
452 let cand = eng.measure(|| {
453 let _ = b.run_candidate();
454 })?;
455 let stable =
456 base.stability.label() != "UNSTABLE" && cand.stability.label() != "UNSTABLE";
457 (
458 format!(
459 "concurrency base={:.0} cand={:.0} profile_winner={winner}",
460 base.median_ns, cand.median_ns
461 ),
462 stable,
463 )
464 } else {
465 let b = IntegerBench { n: 42 };
466 let s = eng.measure(|| {
467 let _ = b.run_baseline();
468 })?;
469 (
470 format!(
471 "integer median={:.0}ns {} (profile_winner={winner})",
472 s.median_ns,
473 s.stability.label()
474 ),
475 s.stability.label() != "UNSTABLE",
476 )
477 };
478 Ok(SpotCheckItem {
479 workload: name.into(),
480 profile_winner: winner.into(),
481 profile_confidence: confidence.label().into(),
482 observation,
483 ok,
484 })
485}