use super::program::{
A1, BACKENDS, T1, T2, config, ending_in_spin, run_to_pc, system_with_configured_latency,
};
use super::{Recorder, accounting_checks};
use crate::config::{BackendKind, Config, MemoryControllerKind};
use crate::soc::memory::ddr5::controller::ClockRatio;
use crate::tests::support::builder::instruction::InstructionBuilder;
use crate::tests::support::harness::TestContext;
const SETTLE: u64 = 20_000;
fn ddr5_system(backend: BackendKind, params: &str) -> Config {
let mut config = config(backend);
for (cache, bytes, ways) in [
(&mut config.cache.l1_d, 1024, 1),
(&mut config.cache.l2, 4096, 4),
(&mut config.cache.l3, 16 * 1024, 8),
] {
cache.enabled = true;
cache.size_bytes = bytes;
cache.ways = ways;
}
config.memory.controller = MemoryControllerKind::Ddr5;
config.memory.ddr5 = serde_json::from_str(params).expect("DDR5 parameters");
config
}
fn read_modify_write_lines(lines: i32) -> (Vec<u32>, u64) {
let i = InstructionBuilder::new;
ending_in_spin(vec![
i().addi(T1, 0, lines).build(),
i().ld(T2, A1, 0).build(),
i().addi(T2, T2, 1).build(),
i().sd(A1, T2, 0).build(),
i().addi(A1, A1, 64).build(),
i().addi(T1, T1, -1).build(),
i().bne(T1, 0, -20).build(),
])
}
fn run_settled(config: &Config, (program, end): (Vec<u32>, u64), context: &str) -> TestContext {
let mut ctx = system_with_configured_latency(config, &program, &[]);
run_to_pc(&mut ctx, end, context);
ctx.run(SETTLE);
ctx
}
fn subchannel_paths(ctx: &TestContext, stat: &str) -> Vec<String> {
let query = ctx.sim.stats().query(&format!("memctrl0.ch*.sc*.{stat}"));
let paths: Vec<String> = query.iter().map(|(path, _)| path.to_owned()).collect();
assert!(!paths.is_empty(), "no subchannel counts {stat}");
paths
}
fn total(rec: &mut Recorder, ctx: &TestContext, stat: &str) -> u64 {
subchannel_paths(ctx, stat).iter().map(|path| rec.read(&ctx.sim, path)).sum()
}
fn llc(rec: &mut Recorder, ctx: &TestContext, stat: &str) -> u64 {
rec.read(&ctx.sim, &format!("llc.{stat}"))
}
fn every_llc_fetch_and_writeback_reaches_dram_once(rec: &mut Recorder) {
for backend in BACKENDS {
let context = format!("{backend:?}");
let ctx = run_settled(&ddr5_system(backend, "{}"), read_modify_write_lines(512), &context);
let fetched = llc(rec, &ctx, "misses") - llc(rec, &ctx, "mshr_hits")
+ llc(rec, &ctx, "prefetches.issued");
let read = total(rec, &ctx, "reads") + total(rec, &ctx, "reads_hit_write_queue");
assert_eq!(read, fetched, "{context}: every LLC fetch is a DRAM read");
let written = total(rec, &ctx, "writes") + total(rec, &ctx, "writes_merged");
assert_eq!(written, llc(rec, &ctx, "writebacks"), "{context}: every writeback a write");
assert!(written > 0, "{context}: 512 dirty lines through a 16 KiB LLC");
}
}
fn each_column_command_counts_its_row_and_its_burst(rec: &mut Recorder) {
for backend in BACKENDS {
let context = format!("{backend:?}");
let config = ddr5_system(backend, "{}");
let burst = config.memory.ddr5.to_config().timing.bl_half;
let ctx = run_settled(&config, read_modify_write_lines(512), &context);
let (hits, misses) = (total(rec, &ctx, "row_hits"), total(rec, &ctx, "row_misses"));
let columns = total(rec, &ctx, "reads")
+ total(rec, &ctx, "scrub_reads")
+ total(rec, &ctx, "writes");
assert_eq!(hits + misses, columns, "{context}: every queued request issued once");
assert_eq!(total(rec, &ctx, "activates"), misses, "{context}: a row miss activates");
assert!(total(rec, &ctx, "precharges") <= misses, "{context}");
assert_eq!(total(rec, &ctx, "bus_busy_clocks"), columns * burst, "{context}");
for path in subchannel_paths(&ctx, "row_hits") {
let subchannel = path.trim_end_matches(".row_hits");
let stat = |rec: &mut Recorder, name: &str| {
rec.read(&ctx.sim, &format!("{subchannel}.{name}"))
};
let (h, m) = (stat(rec, "row_hits"), stat(rec, "row_misses"));
let rate = if h + m == 0 { 0.0 } else { h as f64 / (h + m) as f64 };
rec.expect_ratio(&ctx.sim, &format!("{subchannel}.row_hit_rate"), rate, &context);
let (busy, clocks) = (stat(rec, "bus_busy_clocks"), stat(rec, "clocks"));
let used = busy as f64 / clocks as f64;
rec.expect_ratio(
&ctx.sim,
&format!("{subchannel}.data_bus_utilization"),
used,
&context,
);
}
}
}
fn the_latency_and_queue_samples_are_one_per_request(rec: &mut Recorder) {
for backend in BACKENDS {
let context = format!("{backend:?}");
let ctx = run_settled(&ddr5_system(backend, "{}"), read_modify_write_lines(512), &context);
let samples = |rec: &mut Recorder, name: &str| -> u64 {
subchannel_paths(&ctx, "reads")
.iter()
.map(|path| path.trim_end_matches(".reads").to_owned())
.filter_map(|subchannel| {
let path = format!("{subchannel}.{name}");
ctx.sim
.stats()
.histogram_at(path.as_str())
.is_some()
.then(|| rec.histogram(&ctx.sim, &path).count())
})
.sum()
};
let reads = total(rec, &ctx, "reads");
assert_eq!(samples(rec, "read_latency"), reads, "{context}: a latency per demand read");
let queued = reads + total(rec, &ctx, "scrub_reads");
assert_eq!(samples(rec, "read_queue_depth"), queued, "{context}");
assert_eq!(samples(rec, "write_queue_depth"), total(rec, &ctx, "writes"), "{context}");
}
}
fn every_dram_clock_is_counted_and_refreshed_on_time(rec: &mut Recorder) {
for backend in BACKENDS {
let context = format!("{backend:?}");
let config = ddr5_system(backend, "{}");
let ddr5 = config.memory.ddr5.to_config();
let clock = ClockRatio::new(config.system.cpu_clock_mhz, ddr5.timing.data_rate_mts);
let ctx = run_settled(&config, read_modify_write_lines(64), &context);
let elapsed = clock.to_dram(ctx.sim.cycle()) + 1;
let subchannels = subchannel_paths(&ctx, "clocks");
for path in &subchannels {
rec.expect(&ctx.sim, path, elapsed, &context);
}
let ranks = u64::from(ddr5.ranks_per_channel) * subchannels.len() as u64;
let due = elapsed / ddr5.timing.t_refi * ranks;
let refreshes = total(rec, &ctx, "refreshes");
assert!(
(due.saturating_sub(ranks)..=due + ranks).contains(&refreshes),
"{context}: {refreshes} of {due}"
);
assert!(total(rec, &ctx, "precharge_alls") <= refreshes, "{context}: before refreshes");
}
}
fn idle_ranks_power_down_and_a_patrol_scrubs_when_configured(rec: &mut Recorder) {
let configured = r#"{"power_down_idle_ns": 50, "ecc": "SecDed", "patrol_scrub_ns": 5000}"#;
for backend in BACKENDS {
let context = format!("{backend:?}");
let plain = run_settled(&ddr5_system(backend, "{}"), read_modify_write_lines(64), &context);
let both =
run_settled(&ddr5_system(backend, configured), read_modify_write_lines(64), &context);
for stat in ["power_down_entries", "power_down_exits", "scrub_reads"] {
assert_eq!(total(rec, &plain, stat), 0, "{context}: {stat} unconfigured");
}
let (entries, exits) =
(total(rec, &both, "power_down_entries"), total(rec, &both, "power_down_exits"));
let ranks = subchannel_paths(&both, "clocks").len() as u64
* u64::from(both.sim.state.config.memory.ddr5.to_config().ranks_per_channel);
assert!(
exits > 0 && (exits..=exits + ranks).contains(&entries),
"{context}: {entries} in, {exits} out"
);
assert!(total(rec, &both, "scrub_reads") > 0, "{context}");
}
}
fn requests_wait_for_a_queue_slot_only_when_the_queues_are_full(rec: &mut Recorder) {
let tiny = r#"{"read_queue_entries": 2, "write_queue_entries": 4, "write_high_watermark": 3, "write_low_watermark": 1}"#;
let context = "OutOfOrder width 4";
let mut stalls = [[0; 2]; 2];
for (n, params) in ["{}", tiny].into_iter().enumerate() {
let mut config = ddr5_system(BackendKind::OutOfOrder, params);
config.pipeline.width = 4;
config.cache.l3.prefetcher = crate::config::PrefetcherKind::NextLine;
let ctx = run_settled(&config, read_modify_write_lines(512), context);
stalls[n] =
[total(rec, &ctx, "read_admission_stalls"), total(rec, &ctx, "write_admission_stalls")];
}
assert!(stalls[1][0] > 0 && stalls[1][1] > 0, "{context}: tiny queues: {stalls:?}");
assert!(stalls[0][0] < stalls[1][0] && stalls[0][1] < stalls[1][1], "{context}: {stalls:?}");
}
accounting_checks!(
every_llc_fetch_and_writeback_reaches_dram_once,
each_column_command_counts_its_row_and_its_burst,
the_latency_and_queue_samples_are_one_per_request,
every_dram_clock_is_counted_and_refreshed_on_time,
idle_ranks_power_down_and_a_patrol_scrubs_when_configured,
requests_wait_for_a_queue_slot_only_when_the_queues_are_full,
);