1use crate::{Error, Result};
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::collections::BTreeMap;
6mod guidance;
7pub use guidance::{ReportGuidance, ReportSuggestion};
8
9#[derive(Clone, Debug, Serialize, Deserialize)]
11pub struct CompileReport {
12 compiler: String,
13 target: String,
14 processor_mode: super::ProcessorMode,
15 document: Value,
16}
17
18#[derive(Clone, Debug, Serialize, Deserialize)]
20pub struct Expansion {
21 pub function: Option<String>,
23 pub source_op: Option<String>,
25 pub selection: Option<String>,
27 pub plan_key: Option<String>,
29 pub descriptor_key: Option<String>,
31 pub descriptor_semantic_tag: Option<String>,
33 pub selected_op_count: Option<u64>,
35 pub emitted_low_op_count: Option<u64>,
37 pub maximum_emitted_low_op_count: Option<u64>,
39 pub exact_dynamic_op_count: Option<u64>,
41 pub unknown_dynamic_op_count: Option<u64>,
43 pub dynamic_selected_op_count: Option<u64>,
45 pub dynamic_emitted_low_op_count: Option<u64>,
47}
48
49#[derive(Clone, Debug, Serialize)]
51pub struct EntryResources {
52 pub function: Option<String>,
54 pub code_bytes: Option<u64>,
56 pub scalar_registers: Option<u64>,
58 pub vector_registers: Option<u64>,
60 pub local_bytes: Option<u64>,
62 pub private_bytes: Option<u64>,
64 pub spills: Option<u64>,
66 pub occupancy_percent: Option<u64>,
68 pub bank_modeled_packets: Option<u64>,
70 pub bank_unmodeled_packets: Option<u64>,
72 pub bank_conflicted_packets: Option<u64>,
74 pub bank_extra_rounds: Option<u64>,
76}
77
78#[derive(Clone, Debug, Serialize, Deserialize)]
81pub struct WaitReason {
82 pub function: Option<String>,
84 pub counter: Option<String>,
86 pub reason: Option<String>,
88 pub summary: WaitCounts,
90}
91
92#[derive(Clone, Debug, Default, Serialize, Deserialize)]
94pub struct WaitCounts {
95 pub action_count: Option<u64>,
97 pub explicit_action_count: Option<u64>,
99 pub planned_action_count: Option<u64>,
101 pub full_drain_count: Option<u64>,
103 pub partial_wait_count: Option<u64>,
105 pub drained_count: Option<u64>,
107 pub max_drained_count: Option<u64>,
109 pub max_outstanding_before: Option<u64>,
111 pub max_full_drain_outstanding_before: Option<u64>,
113}
114
115#[derive(Clone, Debug, Serialize)]
117pub struct WaitReasonChange {
118 pub function: String,
120 pub counter: String,
122 pub reason: String,
124 pub before: Option<WaitCounts>,
126 pub after: Option<WaitCounts>,
128 pub delta: BTreeMap<&'static str, Option<i128>>,
130}
131
132impl WaitCounts {
133 fn fields(&self) -> [(&'static str, Option<u64>); 9] {
134 [
135 ("action_count", self.action_count),
136 ("explicit_action_count", self.explicit_action_count),
137 ("planned_action_count", self.planned_action_count),
138 ("full_drain_count", self.full_drain_count),
139 ("partial_wait_count", self.partial_wait_count),
140 ("drained_count", self.drained_count),
141 ("max_drained_count", self.max_drained_count),
142 ("max_outstanding_before", self.max_outstanding_before),
143 (
144 "max_full_drain_outstanding_before",
145 self.max_full_drain_outstanding_before,
146 ),
147 ]
148 }
149}
150
151#[derive(Clone, Debug, Serialize)]
153pub struct EntryChange {
154 pub function: String,
156 pub before: Option<EntryResources>,
158 pub after: Option<EntryResources>,
160 pub delta: BTreeMap<&'static str, Option<i128>>,
162}
163
164impl CompileReport {
165 pub(super) fn new(
166 compiler: String,
167 target: String,
168 processor_mode: super::ProcessorMode,
169 document: Value,
170 ) -> Result<Self> {
171 let report = Self {
172 compiler,
173 target,
174 processor_mode,
175 document,
176 };
177 report.validate()?;
178 Ok(report)
179 }
180
181 pub fn validate(&self) -> Result<()> {
183 if self.compiler.is_empty()
184 || crate::Target::new(&self.target).is_err()
185 || self.document["kind"] != "loom.compile_report"
186 || self.document["schema_version"].as_u64() != Some(0)
187 {
188 return Err(Error::Unsupported(
189 "compile report schema or compiler identity is unsupported; regenerate the report"
190 .into(),
191 ));
192 }
193 Ok(())
194 }
195
196 pub fn compiler_identity(&self) -> &str {
198 &self.compiler
199 }
200
201 pub fn target(&self) -> &str {
203 &self.target
204 }
205
206 pub fn processor_mode(&self) -> super::ProcessorMode {
208 self.processor_mode
209 }
210
211 pub fn json(&self) -> &Value {
213 &self.document
214 }
215
216 pub fn expansions(&self) -> Result<Option<Vec<Expansion>>> {
219 self.validate()?;
220 self.document
221 .pointer("/source_low/selection_summaries/rows")
222 .map(|rows| serde_json::from_value(rows.clone()).map_err(Error::from))
223 .transpose()
224 }
225
226 pub fn entries(&self) -> Result<Vec<EntryResources>> {
228 self.validate()?;
229 let rows = self
230 .rows("entries")?
231 .ok_or_else(|| Error::Message("report entries are missing".into()))?;
232 Ok(rows
233 .iter()
234 .map(|row| {
235 let number = |path| row.pointer(path).and_then(Value::as_u64);
236 EntryResources {
237 function: row["function"].as_str().map(str::to_owned),
238 code_bytes: number("/code_byte_count"),
239 scalar_registers: number("/target_resources/scalar/final/register_count"),
240 vector_registers: number("/target_resources/vector/final/register_count"),
241 local_bytes: number("/local_memory_bytes"),
242 private_bytes: number("/private_memory_bytes"),
243 spills: number("/allocation_spill_count"),
244 occupancy_percent: number("/target_resources/occupancy_percent"),
245 bank_modeled_packets: number(
246 "/source_low_memory/bank_service/modeled_packet_count",
247 ),
248 bank_unmodeled_packets: number(
249 "/source_low_memory/bank_service/unmodeled_packet_count",
250 ),
251 bank_conflicted_packets: number(
252 "/source_low_memory/bank_service/structural/conflicted_packet_count",
253 ),
254 bank_extra_rounds: number(
255 "/source_low_memory/bank_service/structural/extra_round_count",
256 ),
257 }
258 })
259 .collect())
260 }
261
262 pub fn wait_reasons(&self) -> Result<Option<Vec<WaitReason>>> {
265 self.validate()?;
266 self.rows("wait_reason_summary_rows")?
267 .map(|rows| {
268 rows.iter()
269 .cloned()
270 .map(serde_json::from_value)
271 .collect::<std::result::Result<Vec<_>, _>>()
272 .map_err(Error::from)
273 })
274 .transpose()
275 }
276
277 fn rows(&self, name: &str) -> Result<Option<&[Value]>> {
278 let Some(section) = self.document.get(name).filter(|value| !value.is_null()) else {
279 return Ok(None);
280 };
281 let rows = match section.get("rows") {
282 Some(Value::Array(rows)) => rows.as_slice(),
283 None if section["count"].as_u64() == Some(0) => &[],
284 _ => {
285 return Err(Error::Message(format!(
286 "report {name} is missing valid rows"
287 )));
288 }
289 };
290 if let Some(count) = section.get("count")
291 && count.as_u64() != Some(rows.len() as u64)
292 {
293 return Err(Error::Message(format!(
294 "report {name} count does not match its rows"
295 )));
296 }
297 if rows.iter().any(|row| !row.is_object()) {
298 return Err(Error::Message(format!(
299 "report {name} contains a non-object row"
300 )));
301 }
302 Ok(Some(rows))
303 }
304
305 pub fn wait_reason_changes(&self, after: &Self) -> Result<Option<Vec<WaitReasonChange>>> {
309 self.ensure_comparable(after)?;
310 let before_rows = self.wait_reasons()?;
311 let after_rows = after.wait_reasons()?;
312 let (Some(before_rows), Some(after_rows)) = (before_rows, after_rows) else {
313 return Ok(None);
314 };
315 let index =
316 |rows: Vec<WaitReason>| -> Result<BTreeMap<(String, String, String), WaitCounts>> {
317 let mut indexed = BTreeMap::new();
318 for row in rows {
319 let identity = row
320 .function
321 .zip(row.counter)
322 .zip(row.reason)
323 .filter(|((function, counter), reason)| {
324 !function.is_empty() && !counter.is_empty() && !reason.is_empty()
325 })
326 .map(|((function, counter), reason)| (function, counter, reason))
327 .ok_or_else(|| {
328 Error::Message("report wait reason has no complete identity".into())
329 })?;
330 if indexed.insert(identity, row.summary).is_some() {
331 return Err(Error::Message(
332 "report contains duplicate wait reason identities".into(),
333 ));
334 }
335 }
336 Ok(indexed)
337 };
338 let mut before = index(before_rows)?;
339 let mut after = index(after_rows)?;
340 let identities: std::collections::BTreeSet<_> =
341 before.keys().chain(after.keys()).cloned().collect();
342 Ok(Some(
343 identities
344 .into_iter()
345 .map(|identity| {
346 let before = before.remove(&identity);
347 let after = after.remove(&identity);
348 let unknown = WaitCounts::default();
349 let delta = before
350 .as_ref()
351 .unwrap_or(&unknown)
352 .fields()
353 .into_iter()
354 .zip(after.as_ref().unwrap_or(&unknown).fields())
355 .map(|((field, before), (_, after))| {
356 (
357 field,
358 before
359 .zip(after)
360 .map(|(before, after)| i128::from(after) - i128::from(before)),
361 )
362 })
363 .collect();
364 WaitReasonChange {
365 function: identity.0,
366 counter: identity.1,
367 reason: identity.2,
368 before,
369 after,
370 delta,
371 }
372 })
373 .collect(),
374 ))
375 }
376
377 pub fn changes(&self, after: &Self) -> Result<Vec<EntryChange>> {
380 self.ensure_comparable(after)?;
381 let index = |report: &Self| -> Result<BTreeMap<String, EntryResources>> {
382 let mut rows = BTreeMap::new();
383 for row in report.entries()? {
384 let name = row
385 .function
386 .clone()
387 .filter(|name| !name.is_empty())
388 .ok_or_else(|| {
389 Error::Message("report entry has no function identity".into())
390 })?;
391 if rows.insert(name, row).is_some() {
392 return Err(Error::Message(
393 "report contains duplicate function identities".into(),
394 ));
395 }
396 }
397 Ok(rows)
398 };
399 let mut before = index(self)?;
400 let mut after = index(after)?;
401 let names: std::collections::BTreeSet<_> =
402 before.keys().chain(after.keys()).cloned().collect();
403 Ok(names
404 .into_iter()
405 .map(|function| {
406 let before = before.remove(&function);
407 let after = after.remove(&function);
408 let fields = |row: Option<&EntryResources>| {
409 [
410 row.and_then(|r| r.code_bytes),
411 row.and_then(|r| r.scalar_registers),
412 row.and_then(|r| r.vector_registers),
413 row.and_then(|r| r.local_bytes),
414 row.and_then(|r| r.private_bytes),
415 row.and_then(|r| r.spills),
416 row.and_then(|r| r.occupancy_percent),
417 row.and_then(|r| r.bank_modeled_packets),
418 row.and_then(|r| r.bank_unmodeled_packets),
419 row.and_then(|r| r.bank_conflicted_packets),
420 row.and_then(|r| r.bank_extra_rounds),
421 ]
422 };
423 let names = [
424 "code_bytes",
425 "scalar_registers",
426 "vector_registers",
427 "local_bytes",
428 "private_bytes",
429 "spills",
430 "occupancy_percent",
431 "bank_modeled_packets",
432 "bank_unmodeled_packets",
433 "bank_conflicted_packets",
434 "bank_extra_rounds",
435 ];
436 let delta = names
437 .into_iter()
438 .zip(
439 fields(before.as_ref())
440 .into_iter()
441 .zip(fields(after.as_ref()))
442 .map(|(before, after)| {
443 before
444 .zip(after)
445 .map(|(before, after)| i128::from(after) - i128::from(before))
446 }),
447 )
448 .collect();
449 EntryChange {
450 function,
451 before,
452 after,
453 delta,
454 }
455 })
456 .collect())
457 }
458
459 pub fn ensure_comparable(&self, other: &Self) -> Result<()> {
462 self.validate()?;
463 other.validate()?;
464 if self.compiler != other.compiler
465 || self.target != other.target
466 || self.processor_mode != other.processor_mode
467 || ["backend", "target_key", "mode"].iter().any(|key| {
468 self.document.get(key).is_none()
469 || self.document.get(key) != other.document.get(key)
470 })
471 {
472 return Err(Error::Message(
473 "reports must use the same compiler, target, backend, processor policy, and detail mode".into(),
474 ));
475 }
476 Ok(())
477 }
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483 fn evidence() -> Value {
484 serde_json::json!({"kind":"loom.compile_report", "schema_version":0,
485 "backend":"amdgpu-hsaco", "target_key":"gfx1151", "mode":"summary",
486 "entries":{"rows":[{"function":"empty", "allocation_spill_count":0}]}})
487 }
488 #[test]
489 fn bank_coverage_and_wait_reasons_preserve_missing_evidence() {
490 let mut document = evidence();
491 document["entries"]["rows"][0]["source_low_memory"] = serde_json::json!({
492 "bank_service": {"modeled_packet_count": 3, "unmodeled_packet_count": 2,
493 "structural": {"conflicted_packet_count": 1, "extra_round_count": 4}}
494 });
495 let mut report = CompileReport::new(
496 "compiler-a".into(),
497 "gfx1151".into(),
498 super::super::ProcessorMode::Default,
499 document,
500 )
501 .unwrap();
502 let entry = report.entries().unwrap().remove(0);
503 assert_eq!(entry.bank_modeled_packets, Some(3));
504 assert_eq!(entry.bank_unmodeled_packets, Some(2));
505 assert_eq!(entry.bank_conflicted_packets, Some(1));
506 assert_eq!(entry.bank_extra_rounds, Some(4));
507 assert!(report.wait_reasons().unwrap().is_none());
508 report.document["wait_reason_summary_rows"] = serde_json::json!({"count": 0});
509 assert!(report.wait_reasons().unwrap().unwrap().is_empty());
510 report.document["wait_reason_summary_rows"] = serde_json::json!({"count": 1,
511 "rows": [{"function": "empty", "counter": "smem", "reason": "storage_reuse",
512 "summary": {"action_count": 2, "full_drain_count": 2}}]});
513 let waits = report.wait_reasons().unwrap().unwrap();
514 assert_eq!(waits[0].summary.full_drain_count, Some(2));
515 assert_eq!(waits[0].summary.partial_wait_count, None);
516 let mut after = report.clone();
517 after.document["entries"]["rows"][0]["source_low_memory"]["bank_service"]["structural"]["extra_round_count"] =
518 1.into();
519 assert_eq!(
520 report.changes(&after).unwrap()[0].delta["bank_extra_rounds"],
521 Some(-3)
522 );
523 report.document["wait_reason_summary_rows"] = serde_json::json!({"count": 1});
524 assert!(report.wait_reasons().is_err());
525 }
526
527 #[test]
528 fn unknown_resources_are_not_zero_and_comparisons_check_identity() {
529 let report = CompileReport::new(
530 "compiler-a".into(),
531 "gfx1151".into(),
532 super::super::ProcessorMode::Default,
533 evidence(),
534 )
535 .unwrap();
536 let row = report.entries().unwrap().remove(0);
537 assert_eq!(row.spills, Some(0));
538 assert_eq!(row.vector_registers, None);
539 report.ensure_comparable(&report).unwrap();
540 let changes = report.changes(&report).unwrap();
541 assert_eq!(changes[0].delta["spills"], Some(0));
542 assert_eq!(changes[0].delta["vector_registers"], None);
543 let other = CompileReport::new(
544 "compiler-b".into(),
545 "gfx1151".into(),
546 super::super::ProcessorMode::Default,
547 evidence(),
548 )
549 .unwrap();
550 assert!(report.ensure_comparable(&other).is_err());
551 let mut policy = report.clone();
552 policy.processor_mode = super::super::ProcessorMode::ComputeUnit;
553 assert!(report.ensure_comparable(&policy).is_err());
554 let mut target = report.clone();
555 target.target = "gfx1100".into();
556 assert!(report.ensure_comparable(&target).is_err());
557 let mut future = evidence();
558 future["schema_version"] = 1.into();
559 assert!(
560 CompileReport::new(
561 "compiler-a".into(),
562 "gfx1151".into(),
563 super::super::ProcessorMode::Default,
564 future
565 )
566 .is_err()
567 );
568 }
569}