1use crate::process::PerfMemRecord;
7use crate::sched_util::cmd_extract_sched_util;
8use anyhow::{Context as _, Result};
9use clap::{ArgAction, Parser, Subcommand};
10use regex::Regex;
11use serde::Serialize;
12use std::collections::{BTreeSet, HashMap};
13use std::fs::File;
14use std::io::{BufRead, BufReader};
15use std::path::PathBuf;
16
17const DEFAULT_WORKLOAD_CGROUP_REGEX: &str = "workload.slice";
18const DEFAULT_WORKLOAD_ALLOTMENT_CGROUP_REGEX: &str = r"workload-tw-[^/]+\.allotment\.slice";
19
20#[derive(Debug, Parser)]
21pub struct ExtractMemOpts {
22 #[clap(short = 'f', long)]
24 pub file: PathBuf,
25
26 #[clap(long, default_value = DEFAULT_WORKLOAD_CGROUP_REGEX)]
28 pub workload_cgroup_regex: String,
29
30 #[clap(long, default_value = DEFAULT_WORKLOAD_ALLOTMENT_CGROUP_REGEX)]
32 pub workload_allotment_cgroup_regex: String,
33
34 #[clap(long)]
36 pub use_hints: bool,
37
38 #[clap(short, long, action = ArgAction::Count)]
40 pub verbose: u8,
41}
42
43#[derive(Debug, Parser)]
44pub struct ExtractOpts {
45 #[clap(subcommand)]
46 pub command: ExtractCommand,
47}
48
49#[derive(Debug, Subcommand)]
50pub enum ExtractCommand {
51 Mem(ExtractMemOpts),
53 Sched(ExtractSchedOpts),
55}
56
57#[derive(Debug, Parser)]
58pub struct ExtractSchedOpts {
59 #[clap(subcommand)]
60 pub command: ExtractSchedCommand,
61}
62
63#[derive(Debug, Subcommand)]
64pub enum ExtractSchedCommand {
65 Util(ExtractSchedUtilOpts),
67}
68
69#[derive(Debug, Parser)]
70pub struct ExtractSchedUtilOpts {
71 #[clap(short = 'f', long)]
73 pub file: PathBuf,
74
75 #[clap(long, default_value = "1")]
77 pub window_ms: u64,
78
79 #[clap(long, default_value = "")]
81 pub categories: String,
82
83 #[clap(short, long)]
85 pub verbose: bool,
86}
87
88fn classify_cgroup<'a>(cgroup: &'a str, workload_cgroup: &'a str, allotment_re: &Regex) -> &'a str {
89 if !cgroup.contains(workload_cgroup) {
90 return "rest";
91 }
92
93 if let Some(m) = allotment_re.find(cgroup) {
94 return m.as_str();
95 }
96
97 workload_cgroup
98}
99
100struct GroupData {
102 samples: Vec<PerfMemRecord>,
103}
104
105impl GroupData {
106 fn new() -> Self {
107 Self {
108 samples: Vec::new(),
109 }
110 }
111
112 fn push(&mut self, sample: PerfMemRecord) {
113 self.samples.push(sample);
114 }
115
116 fn samples(&self) -> &[PerfMemRecord] {
117 &self.samples
118 }
119
120 fn print(&self, group_name: &str, global_total: u64, verbosity: u8, use_hints: bool) {
121 let group_pct = (self.samples.len() as f64 / global_total as f64) * 100.0;
122 eprintln!(
123 "\n{}: {} samples ({:.2}%)",
124 group_name,
125 self.samples.len(),
126 group_pct
127 );
128
129 let sample_refs: Vec<_> = self.samples.iter().collect();
130 let counts = summarize_comm_groups(&sample_refs);
131 let mut printed_aggregated_marker_note = false;
132
133 let mut skipped = 0;
134 for entry in counts {
135 let pct = (entry.count as f64 / self.samples.len() as f64) * 100.0;
136 if verbosity >= 2 || pct > 1.0 {
137 let display_name = if entry.aggregated_numeric_suffixes {
138 printed_aggregated_marker_note = true;
139 format!("{}*", entry.name)
140 } else {
141 entry.name.clone()
142 };
143 eprintln!(" {}: {} ({:.2}%)", display_name, entry.count, pct);
144 if use_hints && entry.hint_counts.len() > 1 {
145 let mut skipped_hints = 0;
146 for (hint, count) in entry.hint_counts {
147 let hint_pct = (count as f64 / entry.count as f64) * 100.0;
148 if verbosity >= 2 || hint_pct > 1.0 {
149 eprintln!(" hint={}: {} ({:.2}%)", hint, count, hint_pct);
150 } else {
151 skipped_hints += 1;
152 }
153 }
154 if skipped_hints > 0 {
155 eprintln!(" ... {} more hints below 1%", skipped_hints);
156 }
157 }
158 if verbosity >= 2 && entry.concrete_counts.len() > 1 {
159 for (comm, count) in entry.concrete_counts {
160 let comm_pct = (count as f64 / entry.count as f64) * 100.0;
161 eprintln!(" {}: {} ({:.2}%)", comm, count, comm_pct);
162 }
163 }
164 } else {
165 skipped += 1;
166 }
167 }
168 if skipped > 0 {
169 eprintln!(" ... {} more below 1%", skipped);
170 }
171 if printed_aggregated_marker_note {
172 eprintln!(" * trailing numeric suffixes merged");
173 }
174 }
175}
176
177struct ClusterResult {
179 significant_comms: Vec<CommCluster>,
181}
182
183struct CommCluster {
185 name: String,
186 match_comms: Vec<String>,
187 significant_hints: Vec<u64>,
188}
189
190#[derive(Debug, Clone, PartialEq, Eq)]
191struct CommSummary {
192 name: String,
193 count: u64,
194 aggregated_numeric_suffixes: bool,
195 hint_counts: Vec<(u64, u64)>,
196 concrete_counts: Vec<(String, u64)>,
197}
198
199#[derive(Debug, Default)]
200struct GroupedCommSamples<'a> {
201 samples: Vec<&'a PerfMemRecord>,
202 hint_counts: HashMap<u64, u64>,
203 concrete_counts: HashMap<String, u64>,
204}
205
206#[derive(Debug, Clone, Copy, PartialEq)]
208enum GroupType {
209 Allotment,
210 Workload,
211 Rest,
212}
213
214fn compute_clusters(
217 group_type: GroupType,
218 samples: &[&PerfMemRecord],
219 threshold_pct: f64,
220 use_hints: bool,
221) -> ClusterResult {
222 if group_type != GroupType::Allotment {
224 return ClusterResult {
225 significant_comms: Vec::new(),
226 };
227 }
228
229 let total = samples.len();
230 let grouped_samples = group_samples_by_normalized_comm(samples);
231
232 let mut significant_comms = Vec::new();
233 if total > 0 {
234 for (cluster_name, group) in grouped_samples {
235 let count = group.samples.len() as u64;
236 let pct = (count as f64 / total as f64) * 100.0;
237 if pct > threshold_pct {
238 let significant_hints = if use_hints {
239 compute_significant_hints(&group.samples, threshold_pct)
240 } else {
241 Vec::new()
242 };
243 significant_comms.push(CommCluster {
244 name: cluster_name.clone(),
245 match_comms: group
246 .concrete_counts
247 .keys()
248 .cloned()
249 .collect::<BTreeSet<_>>()
250 .into_iter()
251 .collect(),
252 significant_hints,
253 });
254 }
255 }
256 }
257 significant_comms.sort_by(|a, b| a.name.cmp(&b.name));
258
259 ClusterResult { significant_comms }
260}
261
262fn group_samples_by_normalized_comm<'a>(
263 samples: &[&'a PerfMemRecord],
264) -> HashMap<String, GroupedCommSamples<'a>> {
265 let mut grouped = HashMap::new();
266 for sample in samples {
267 let cluster_name = normalize_comm_for_cluster(&sample.comm);
268 let entry = grouped
269 .entry(cluster_name)
270 .or_insert_with(GroupedCommSamples::default);
271 entry.samples.push(*sample);
272 *entry.hint_counts.entry(sample.hint).or_insert(0) += 1;
273 *entry
274 .concrete_counts
275 .entry(sample.comm.clone())
276 .or_insert(0) += 1;
277 }
278 grouped
279}
280
281fn normalize_comm_for_cluster(comm: &str) -> String {
282 let normalized = comm.trim_end_matches(|ch: char| ch.is_ascii_digit());
283 if normalized.is_empty() {
284 comm.to_string()
285 } else {
286 normalized.to_string()
287 }
288}
289
290fn compute_significant_hints(samples: &[&PerfMemRecord], threshold_pct: f64) -> Vec<u64> {
291 let mut hint_counts: HashMap<u64, u64> = HashMap::new();
292 for sample in samples {
293 *hint_counts.entry(sample.hint).or_insert(0) += 1;
294 }
295
296 if hint_counts.len() <= 1 {
297 return Vec::new();
298 }
299
300 let total = samples.len();
301 let mut significant_hints = Vec::new();
302 if total > 0 {
303 for (hint, count) in hint_counts {
304 let pct = (count as f64 / total as f64) * 100.0;
305 if pct > threshold_pct {
306 significant_hints.push(hint);
307 }
308 }
309 }
310 significant_hints.sort_unstable();
311 significant_hints
312}
313
314fn summarize_comm_groups(samples: &[&PerfMemRecord]) -> Vec<CommSummary> {
315 let mut summary: Vec<_> = group_samples_by_normalized_comm(samples)
316 .into_iter()
317 .map(|(name, group)| {
318 let mut hint_counts: Vec<_> = group.hint_counts.into_iter().collect();
319 hint_counts.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
320 let mut concrete_counts: Vec<_> = group.concrete_counts.into_iter().collect();
321 concrete_counts.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
322 CommSummary {
323 name,
324 count: group.samples.len() as u64,
325 aggregated_numeric_suffixes: concrete_counts.len() > 1,
326 hint_counts,
327 concrete_counts,
328 }
329 })
330 .collect();
331
332 summary.sort_by(|a, b| b.count.cmp(&a.count).then_with(|| a.name.cmp(&b.name)));
333 summary
334}
335
336fn build_subcells_from_clusters(result: &ClusterResult) -> Vec<CellSpec> {
338 if result.significant_comms.is_empty() {
339 return Vec::new();
340 }
341
342 let mut subcells = Vec::new();
343
344 for comm in &result.significant_comms {
345 if comm.significant_hints.is_empty() {
346 subcells.push(CellSpec {
347 name: comm.name.clone(),
348 matches: CellMatches::complex(build_comm_match_clauses(comm, None)),
349 subcells: Vec::new(),
350 });
351 } else {
352 for hint in &comm.significant_hints {
353 subcells.push(CellSpec {
354 name: format!("{}@hint={hint}", comm.name),
355 matches: CellMatches::complex(build_comm_match_clauses(comm, Some(*hint))),
356 subcells: Vec::new(),
357 });
358 }
359 }
360 }
361
362 subcells.push(CellSpec {
363 name: "rest".to_string(),
364 matches: CellMatches::complex(vec![vec![]]),
365 subcells: Vec::new(),
366 });
367
368 subcells
369}
370
371fn build_comm_match_clauses(comm: &CommCluster, hint: Option<u64>) -> Vec<Vec<CellMatch>> {
372 let comm_patterns = emitted_comm_patterns(comm);
373
374 comm_patterns
375 .iter()
376 .map(|exact_comm| {
377 let mut clause = vec![CellMatch::CommPrefix(exact_comm.clone())];
378 if let Some(hint) = hint {
379 clause.push(CellMatch::Hint(hint));
380 }
381 clause
382 })
383 .collect()
384}
385
386fn emitted_comm_patterns(comm: &CommCluster) -> Vec<String> {
387 if comm.match_comms.len() > 1
388 && !comm.name.is_empty()
389 && comm
390 .match_comms
391 .iter()
392 .all(|match_comm| match_comm.starts_with(&comm.name))
393 {
394 vec![comm.name.clone()]
395 } else {
396 comm.match_comms.clone()
397 }
398}
399
400#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
401enum CellMatch {
402 CommPrefix(String),
403 Hint(u64),
404}
405
406#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
407#[serde(untagged)]
408enum CellMatches {
409 Simple(SimpleCellMatches),
410 Complex(Vec<Vec<CellMatch>>),
411}
412
413impl CellMatches {
414 fn simple(matches: SimpleCellMatches) -> Self {
415 Self::Simple(matches)
416 }
417
418 fn complex(matches: Vec<Vec<CellMatch>>) -> Self {
419 Self::Complex(matches)
420 }
421}
422
423#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
424struct SimpleCellMatches {
425 #[serde(rename = "CgroupRegex", skip_serializing_if = "Option::is_none")]
426 cgroup_regex: Option<String>,
427 #[serde(rename = "CgroupContains", skip_serializing_if = "Option::is_none")]
428 cgroup_contains: Option<String>,
429}
430
431impl SimpleCellMatches {
432 fn cgroup_regex(value: String) -> Self {
433 Self {
434 cgroup_regex: Some(value),
435 cgroup_contains: None,
436 }
437 }
438
439 fn cgroup_contains(value: String) -> Self {
440 Self {
441 cgroup_regex: None,
442 cgroup_contains: Some(value),
443 }
444 }
445}
446
447#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
448struct CellSpec {
449 name: String,
450 matches: CellMatches,
451 #[serde(skip_serializing_if = "Vec::is_empty")]
452 subcells: Vec<CellSpec>,
453}
454
455#[derive(Debug, Clone, Serialize)]
456#[serde(transparent)]
457struct CellConfig {
458 specs: Vec<CellSpec>,
459}
460
461pub fn cmd_extract_mem(opts: ExtractMemOpts) -> Result<()> {
462 let file = File::open(&opts.file).context("failed to open perf.mem.jsonl")?;
463 let reader = BufReader::new(file);
464
465 let allotment_re =
466 Regex::new(&opts.workload_allotment_cgroup_regex).context("invalid allotment regex")?;
467 let workload_cgroup = &opts.workload_cgroup_regex;
468
469 let mut groups: HashMap<String, GroupData> = HashMap::new();
470 let mut global_total: u64 = 0;
471
472 for line in reader.lines() {
473 let line = line.context("failed to read line")?;
474 let record: PerfMemRecord =
475 serde_json::from_str(&line).context("failed to parse record")?;
476
477 let group = classify_cgroup(&record.cgroup, workload_cgroup, &allotment_re);
478 groups
479 .entry(group.to_string())
480 .or_insert_with(GroupData::new)
481 .push(record);
482 global_total += 1;
483 }
484
485 let mut group_names: Vec<_> = groups.keys().cloned().collect();
486 group_names.sort_by(|a, b| {
487 let order = |s: &str| -> u8 {
488 if s == "rest" {
489 2
490 } else if s == workload_cgroup {
491 1
492 } else {
493 0
494 }
495 };
496 order(a).cmp(&order(b)).then_with(|| a.cmp(b))
497 });
498
499 if opts.verbose > 0 {
500 eprintln!("Total samples: {}", global_total);
501 for name in &group_names {
502 if let Some(data) = groups.get(name) {
503 data.print(name, global_total, opts.verbose, opts.use_hints);
504 }
505 }
506 }
507
508 let config = generate_config(
509 &groups,
510 &group_names,
511 workload_cgroup,
512 &opts.workload_allotment_cgroup_regex,
513 opts.use_hints,
514 );
515 let json = serde_json::to_string_pretty(&config).context("failed to serialize config")?;
516 println!("{}", json);
517
518 Ok(())
519}
520
521pub fn cmd_extract(opts: ExtractOpts) -> Result<()> {
522 match opts.command {
523 ExtractCommand::Mem(opts) => cmd_extract_mem(opts),
524 ExtractCommand::Sched(opts) => cmd_extract_sched(opts),
525 }
526}
527
528pub fn cmd_extract_sched(opts: ExtractSchedOpts) -> Result<()> {
529 match opts.command {
530 ExtractSchedCommand::Util(opts) => cmd_extract_sched_util(opts),
531 }
532}
533
534fn generate_config(
535 groups: &HashMap<String, GroupData>,
536 group_names: &[String],
537 workload_cgroup: &str,
538 allotment_regex: &str,
539 use_hints: bool,
540) -> CellConfig {
541 let mut specs = Vec::new();
542
543 let group_types = [
544 (GroupType::Allotment, "allotment"),
545 (GroupType::Workload, workload_cgroup),
546 (GroupType::Rest, "rest"),
547 ];
548
549 for (group_type, name) in group_types {
550 let samples: Vec<&PerfMemRecord> = match group_type {
551 GroupType::Allotment => group_names
552 .iter()
553 .filter(|n| *n != "rest" && *n != workload_cgroup)
554 .filter_map(|n| groups.get(n))
555 .flat_map(|g| g.samples())
556 .collect(),
557 GroupType::Workload => groups
558 .get(workload_cgroup)
559 .map(|g| g.samples().iter().collect())
560 .unwrap_or_default(),
561 GroupType::Rest => groups
562 .get("rest")
563 .map(|g| g.samples().iter().collect())
564 .unwrap_or_default(),
565 };
566
567 if samples.is_empty() {
568 continue;
569 }
570
571 let clusters = compute_clusters(group_type, &samples, 5.0, use_hints);
572 let subcells = build_subcells_from_clusters(&clusters);
573
574 let matches = match group_type {
575 GroupType::Allotment => {
576 CellMatches::simple(SimpleCellMatches::cgroup_regex(allotment_regex.to_string()))
577 }
578 GroupType::Workload => CellMatches::simple(SimpleCellMatches::cgroup_contains(
579 workload_cgroup.to_string(),
580 )),
581 GroupType::Rest => CellMatches::simple(SimpleCellMatches::default()),
582 };
583
584 specs.push(CellSpec {
585 name: name.to_string(),
586 matches,
587 subcells,
588 });
589 }
590
591 CellConfig { specs }
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597 use serde_json::json;
598
599 fn sample(comm: &str, cgroup: &str, hint: u64) -> PerfMemRecord {
600 serde_json::from_value(json!({
601 "comm": comm,
602 "tid": 1,
603 "pid": 1,
604 "time": "0",
605 "addr": "0",
606 "cgroup": cgroup,
607 "ip": "0",
608 "sym": "sym",
609 "dso": "dso",
610 "phys_addr": "0",
611 "data_page_size": 4096,
612 "hint": hint,
613 }))
614 .expect("failed to build PerfMemRecord test sample")
615 }
616
617 fn push_samples(
618 groups: &mut HashMap<String, GroupData>,
619 group: &str,
620 comm: &str,
621 hint: u64,
622 count: usize,
623 ) {
624 let data = groups
625 .entry(group.to_string())
626 .or_insert_with(GroupData::new);
627 for _ in 0..count {
628 data.push(sample(comm, group, hint));
629 }
630 }
631
632 #[test]
633 fn mem_extract_does_not_split_comms_by_hint_without_flag() {
634 let allotment = "workload-tw-foo.allotment.slice";
635 let workload = "workload.slice";
636
637 let mut groups = HashMap::new();
638 push_samples(&mut groups, allotment, "alpha", 0, 60);
639 push_samples(&mut groups, allotment, "alpha", 7, 20);
640 push_samples(&mut groups, allotment, "alpha", 9, 10);
641 push_samples(&mut groups, allotment, "beta", 0, 10);
642
643 let group_names = vec![allotment.to_string()];
644 let config = generate_config(&groups, &group_names, workload, "allotment-regex", false);
645
646 let allotment_spec = &config.specs[0];
647 let alpha = allotment_spec
648 .subcells
649 .iter()
650 .find(|spec| spec.name == "alpha")
651 .expect("missing alpha comm subcell");
652
653 assert!(alpha.subcells.is_empty());
654 }
655
656 #[test]
657 fn mem_extract_splits_significant_comms_by_hint_with_flag() {
658 let allotment = "workload-tw-foo.allotment.slice";
659 let workload = "workload.slice";
660
661 let mut groups = HashMap::new();
662 push_samples(&mut groups, allotment, "alpha", 0, 60);
663 push_samples(&mut groups, allotment, "alpha", 7, 20);
664 push_samples(&mut groups, allotment, "alpha", 9, 10);
665 push_samples(&mut groups, allotment, "beta", 0, 10);
666
667 let group_names = vec![allotment.to_string()];
668 let config = generate_config(&groups, &group_names, workload, "allotment-regex", true);
669
670 let allotment_spec = &config.specs[0];
671 let alpha_hint_0 = allotment_spec
672 .subcells
673 .iter()
674 .find(|spec| spec.name == "alpha@hint=0")
675 .expect("missing alpha@hint=0 subcell");
676 let alpha_hint_7 = allotment_spec
677 .subcells
678 .iter()
679 .find(|spec| spec.name == "alpha@hint=7")
680 .expect("missing alpha@hint=7 subcell");
681 let alpha_hint_9 = allotment_spec
682 .subcells
683 .iter()
684 .find(|spec| spec.name == "alpha@hint=9")
685 .expect("missing alpha@hint=9 subcell");
686
687 assert_eq!(
688 alpha_hint_0.matches,
689 CellMatches::complex(vec![vec![
690 CellMatch::CommPrefix("alpha".to_string()),
691 CellMatch::Hint(0),
692 ]])
693 );
694 assert_eq!(
695 alpha_hint_7.matches,
696 CellMatches::complex(vec![vec![
697 CellMatch::CommPrefix("alpha".to_string()),
698 CellMatch::Hint(7),
699 ]])
700 );
701 assert_eq!(
702 alpha_hint_9.matches,
703 CellMatches::complex(vec![vec![
704 CellMatch::CommPrefix("alpha".to_string()),
705 CellMatch::Hint(9),
706 ]])
707 );
708
709 let beta = allotment_spec
710 .subcells
711 .iter()
712 .find(|spec| spec.name == "beta")
713 .expect("missing beta comm subcell");
714 assert!(beta.subcells.is_empty());
715 }
716
717 #[test]
718 fn mem_extract_uses_single_match_statement_for_top_level_cells() {
719 let allotment = "workload-tw-foo.allotment.slice";
720 let workload = "workload.slice";
721
722 let mut groups = HashMap::new();
723 push_samples(&mut groups, allotment, "alpha", 0, 10);
724 push_samples(&mut groups, workload, "beta", 0, 10);
725 push_samples(&mut groups, "rest", "gamma", 0, 10);
726
727 let mut group_names = vec![
728 allotment.to_string(),
729 workload.to_string(),
730 "rest".to_string(),
731 ];
732 group_names.sort();
733
734 let config = generate_config(&groups, &group_names, workload, "allotment-regex", false);
735
736 let allotment_spec = config
737 .specs
738 .iter()
739 .find(|spec| spec.name == "allotment")
740 .expect("missing allotment spec");
741 assert_eq!(
742 allotment_spec.matches,
743 CellMatches::simple(SimpleCellMatches::cgroup_regex(
744 "allotment-regex".to_string()
745 ))
746 );
747
748 let workload_spec = config
749 .specs
750 .iter()
751 .find(|spec| spec.name == workload)
752 .expect("missing workload spec");
753 assert_eq!(
754 workload_spec.matches,
755 CellMatches::simple(SimpleCellMatches::cgroup_contains(workload.to_string()))
756 );
757
758 let rest_spec = config
759 .specs
760 .iter()
761 .find(|spec| spec.name == "rest")
762 .expect("missing rest spec");
763 assert_eq!(
764 rest_spec.matches,
765 CellMatches::simple(SimpleCellMatches::default())
766 );
767 }
768
769 #[test]
770 fn mem_extract_clusters_numeric_suffix_comms_for_dominance() {
771 let allotment = "workload-tw-foo.allotment.slice";
772 let workload = "workload.slice";
773
774 let mut groups = HashMap::new();
775 push_samples(&mut groups, allotment, "mcrpxy-webNR1", 0, 4);
776 push_samples(&mut groups, allotment, "mcrpxy-webNR2", 0, 4);
777 push_samples(&mut groups, allotment, "beta", 0, 92);
778
779 let group_names = vec![allotment.to_string()];
780 let config = generate_config(&groups, &group_names, workload, "allotment-regex", false);
781
782 let allotment_spec = &config.specs[0];
783 let merged = allotment_spec
784 .subcells
785 .iter()
786 .find(|spec| spec.name == "mcrpxy-webNR")
787 .expect("missing merged numeric-suffix comm subcell");
788
789 assert_eq!(
790 merged.matches,
791 CellMatches::complex(vec![vec![CellMatch::CommPrefix(
792 "mcrpxy-webNR".to_string()
793 )]])
794 );
795 assert!(allotment_spec
796 .subcells
797 .iter()
798 .all(|spec| spec.name != "mcrpxy-webNR1" && spec.name != "mcrpxy-webNR2"));
799 }
800
801 #[test]
802 fn mem_extract_splits_merged_numeric_suffix_comm_by_hint_with_flag() {
803 let allotment = "workload-tw-foo.allotment.slice";
804 let workload = "workload.slice";
805
806 let mut groups = HashMap::new();
807 push_samples(&mut groups, allotment, "mcrpxy-webNR1", 0, 3);
808 push_samples(&mut groups, allotment, "mcrpxy-webNR1", 7, 1);
809 push_samples(&mut groups, allotment, "mcrpxy-webNR2", 7, 3);
810 push_samples(&mut groups, allotment, "mcrpxy-webNR2", 9, 1);
811 push_samples(&mut groups, allotment, "beta", 0, 92);
812
813 let group_names = vec![allotment.to_string()];
814 let config = generate_config(&groups, &group_names, workload, "allotment-regex", true);
815
816 let allotment_spec = &config.specs[0];
817 let merged_hint_0 = allotment_spec
818 .subcells
819 .iter()
820 .find(|spec| spec.name == "mcrpxy-webNR@hint=0")
821 .expect("missing merged numeric-suffix hint=0 subcell");
822 let merged_hint_7 = allotment_spec
823 .subcells
824 .iter()
825 .find(|spec| spec.name == "mcrpxy-webNR@hint=7")
826 .expect("missing merged numeric-suffix hint=7 subcell");
827 let merged_hint_9 = allotment_spec
828 .subcells
829 .iter()
830 .find(|spec| spec.name == "mcrpxy-webNR@hint=9")
831 .expect("missing merged numeric-suffix hint=9 subcell");
832 assert_eq!(
833 merged_hint_0.matches,
834 CellMatches::complex(vec![vec![
835 CellMatch::CommPrefix("mcrpxy-webNR".to_string()),
836 CellMatch::Hint(0),
837 ]])
838 );
839 assert_eq!(
840 merged_hint_7.matches,
841 CellMatches::complex(vec![vec![
842 CellMatch::CommPrefix("mcrpxy-webNR".to_string()),
843 CellMatch::Hint(7),
844 ]])
845 );
846 assert_eq!(
847 merged_hint_9.matches,
848 CellMatches::complex(vec![vec![
849 CellMatch::CommPrefix("mcrpxy-webNR".to_string()),
850 CellMatch::Hint(9),
851 ]])
852 );
853 }
854
855 #[test]
856 fn mem_extract_summary_groups_numeric_suffix_comms_and_reports_hints() {
857 let samples = vec![
858 sample("mcrpxy-webNR1", "cg", 0),
859 sample("mcrpxy-webNR2", "cg", 7),
860 sample("mcrpxy-webNR3", "cg", 7),
861 ];
862 let sample_refs: Vec<_> = samples.iter().collect();
863 let summary = summarize_comm_groups(&sample_refs);
864
865 assert_eq!(summary.len(), 1);
866 assert_eq!(summary[0].name, "mcrpxy-webNR");
867 assert_eq!(summary[0].count, 3);
868 assert!(summary[0].aggregated_numeric_suffixes);
869 assert_eq!(summary[0].hint_counts, vec![(7, 2), (0, 1)]);
870 assert_eq!(
871 summary[0].concrete_counts,
872 vec![
873 ("mcrpxy-webNR1".to_string(), 1),
874 ("mcrpxy-webNR2".to_string(), 1),
875 ("mcrpxy-webNR3".to_string(), 1),
876 ]
877 );
878 }
879}