use crate::{Error, Result};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CompileReport {
compiler: String,
target: String,
processor_mode: super::ProcessorMode,
document: Value,
}
#[derive(Clone, Debug, Serialize)]
pub struct EntryResources {
pub function: Option<String>,
pub code_bytes: Option<u64>,
pub scalar_registers: Option<u64>,
pub vector_registers: Option<u64>,
pub local_bytes: Option<u64>,
pub private_bytes: Option<u64>,
pub spills: Option<u64>,
pub occupancy_percent: Option<u64>,
}
#[derive(Clone, Debug, Serialize)]
pub struct EntryChange {
pub function: String,
pub before: Option<EntryResources>,
pub after: Option<EntryResources>,
pub delta: BTreeMap<&'static str, Option<i128>>,
}
impl CompileReport {
pub(super) fn new(
compiler: String,
target: String,
processor_mode: super::ProcessorMode,
document: Value,
) -> Result<Self> {
let report = Self {
compiler,
target,
processor_mode,
document,
};
report.validate()?;
Ok(report)
}
pub fn validate(&self) -> Result<()> {
if self.compiler.is_empty()
|| crate::Target::new(&self.target).is_err()
|| self.document["kind"] != "loom.compile_report"
|| self.document["schema_version"].as_u64() != Some(0)
{
return Err(Error::Unsupported(
"compile report schema or compiler identity is unsupported; regenerate the report"
.into(),
));
}
Ok(())
}
pub fn compiler_identity(&self) -> &str {
&self.compiler
}
pub fn target(&self) -> &str {
&self.target
}
pub fn processor_mode(&self) -> super::ProcessorMode {
self.processor_mode
}
pub fn json(&self) -> &Value {
&self.document
}
pub fn entries(&self) -> Result<Vec<EntryResources>> {
self.validate()?;
let rows = self
.document
.pointer("/entries/rows")
.and_then(Value::as_array);
Ok(rows
.into_iter()
.flatten()
.map(|row| {
let number = |path| row.pointer(path).and_then(Value::as_u64);
EntryResources {
function: row["function"].as_str().map(str::to_owned),
code_bytes: number("/code_byte_count"),
scalar_registers: number("/target_resources/scalar/final/register_count"),
vector_registers: number("/target_resources/vector/final/register_count"),
local_bytes: number("/local_memory_bytes"),
private_bytes: number("/private_memory_bytes"),
spills: number("/allocation_spill_count"),
occupancy_percent: number("/target_resources/occupancy_percent"),
}
})
.collect())
}
pub fn changes(&self, after: &Self) -> Result<Vec<EntryChange>> {
self.ensure_comparable(after)?;
let index = |report: &Self| -> Result<BTreeMap<String, EntryResources>> {
let mut rows = BTreeMap::new();
for row in report.entries()? {
let name = row.function.clone().ok_or_else(|| {
Error::Message("report entry has no function identity".into())
})?;
if rows.insert(name, row).is_some() {
return Err(Error::Message(
"report contains duplicate function identities".into(),
));
}
}
Ok(rows)
};
let mut before = index(self)?;
let mut after = index(after)?;
let names: std::collections::BTreeSet<_> =
before.keys().chain(after.keys()).cloned().collect();
Ok(names
.into_iter()
.map(|function| {
let before = before.remove(&function);
let after = after.remove(&function);
let fields = |row: Option<&EntryResources>| {
[
row.and_then(|r| r.code_bytes),
row.and_then(|r| r.scalar_registers),
row.and_then(|r| r.vector_registers),
row.and_then(|r| r.local_bytes),
row.and_then(|r| r.private_bytes),
row.and_then(|r| r.spills),
row.and_then(|r| r.occupancy_percent),
]
};
let names = [
"code_bytes",
"scalar_registers",
"vector_registers",
"local_bytes",
"private_bytes",
"spills",
"occupancy_percent",
];
let delta = names
.into_iter()
.zip(
fields(before.as_ref())
.into_iter()
.zip(fields(after.as_ref()))
.map(|(before, after)| {
before
.zip(after)
.map(|(before, after)| i128::from(after) - i128::from(before))
}),
)
.collect();
EntryChange {
function,
before,
after,
delta,
}
})
.collect())
}
pub fn ensure_comparable(&self, other: &Self) -> Result<()> {
self.validate()?;
other.validate()?;
if self.compiler != other.compiler
|| self.target != other.target
|| self.processor_mode != other.processor_mode
|| ["backend", "target_key", "mode"].iter().any(|key| {
self.document.get(key).is_none()
|| self.document.get(key) != other.document.get(key)
})
{
return Err(Error::Message(
"reports must use the same compiler, target, backend, processor policy, and detail mode".into(),
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn evidence() -> Value {
serde_json::json!({"kind":"loom.compile_report", "schema_version":0,
"backend":"amdgpu-hsaco", "target_key":"gfx1151", "mode":"summary",
"entries":{"rows":[{"function":"empty", "allocation_spill_count":0}]}})
}
#[test]
fn unknown_resources_are_not_zero_and_comparisons_check_identity() {
let report = CompileReport::new(
"compiler-a".into(),
"gfx1151".into(),
super::super::ProcessorMode::Default,
evidence(),
)
.unwrap();
let row = report.entries().unwrap().remove(0);
assert_eq!(row.spills, Some(0));
assert_eq!(row.vector_registers, None);
report.ensure_comparable(&report).unwrap();
let changes = report.changes(&report).unwrap();
assert_eq!(changes[0].delta["spills"], Some(0));
assert_eq!(changes[0].delta["vector_registers"], None);
let other = CompileReport::new(
"compiler-b".into(),
"gfx1151".into(),
super::super::ProcessorMode::Default,
evidence(),
)
.unwrap();
assert!(report.ensure_comparable(&other).is_err());
let mut policy = report.clone();
policy.processor_mode = super::super::ProcessorMode::ComputeUnit;
assert!(report.ensure_comparable(&policy).is_err());
let mut target = report.clone();
target.target = "gfx1100".into();
assert!(report.ensure_comparable(&target).is_err());
let mut future = evidence();
future["schema_version"] = 1.into();
assert!(
CompileReport::new(
"compiler-a".into(),
"gfx1151".into(),
super::super::ProcessorMode::Default,
future
)
.is_err()
);
}
}