#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(usize)]
pub enum Stage {
Total = 0,
FillBuffers,
Fdct,
Quantize,
Entropy,
HuffmanOptimize,
Headers,
GetBlock,
RowSetup,
DecEntropy,
DecIdct,
DecScan,
DecPlaneInit,
DecOutput,
BlockBody,
DecMcuRowAlloc,
DecBlockLoop,
DecRowDispatch,
}
impl Stage {
pub fn is_info(self) -> bool {
matches!(
self,
Stage::BlockBody | Stage::DecScan | Stage::DecBlockLoop | Stage::DecRowDispatch
)
}
}
impl Stage {
pub const COUNT: usize = 24;
pub fn name(self) -> &'static str {
match self {
Stage::Total => "Total",
Stage::FillBuffers => "FillBuffers",
Stage::Fdct => "Fdct",
Stage::Quantize => "Quantize",
Stage::Entropy => "Entropy",
Stage::HuffmanOptimize => "HuffmanOptimize",
Stage::Headers => "Headers",
Stage::GetBlock => "GetBlock",
Stage::RowSetup => "RowSetup",
Stage::DecEntropy => "DecEntropy",
Stage::DecIdct => "DecIdct",
Stage::DecScan => "[i] DecScan",
Stage::DecPlaneInit => "DecPlaneInit",
Stage::DecOutput => "DecOutput",
Stage::BlockBody => "[i] BlockBody",
Stage::DecMcuRowAlloc => "DecMcuRowAlloc",
Stage::DecBlockLoop => "[i] DecBlockLoop",
Stage::DecRowDispatch => "[i] DecRowDispatch",
}
}
#[allow(dead_code)]
fn from_index(i: usize) -> Stage {
match i {
0 => Stage::Total,
1 => Stage::FillBuffers,
2 => Stage::Fdct,
3 => Stage::Quantize,
4 => Stage::Entropy,
5 => Stage::HuffmanOptimize,
6 => Stage::Headers,
7 => Stage::GetBlock,
8 => Stage::RowSetup,
9 => Stage::DecEntropy,
10 => Stage::DecIdct,
11 => Stage::DecScan,
12 => Stage::DecPlaneInit,
13 => Stage::DecOutput,
14 => Stage::BlockBody,
15 => Stage::DecMcuRowAlloc,
16 => Stage::DecBlockLoop,
_ => Stage::DecRowDispatch,
}
}
}
#[cfg(not(feature = "profile"))]
mod imp {
use super::Stage;
pub struct Guard;
#[inline(always)]
pub fn scope(_stage: Stage) -> Guard {
Guard
}
#[inline(always)]
pub fn reset() {}
pub fn snapshot() -> [(f64, u64); Stage::COUNT] {
[(0.0, 0); Stage::COUNT]
}
pub fn dump() -> alloc::string::String {
alloc::string::String::from(
"profiling disabled — rebuild with `--features profile` to get a breakdown\n",
)
}
}
#[cfg(feature = "profile")]
mod imp {
use super::Stage;
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use core::sync::atomic::{AtomicU64, Ordering};
#[allow(clippy::declare_interior_mutable_const)]
const ZERO: AtomicU64 = AtomicU64::new(0);
static CYCLES: [AtomicU64; Stage::COUNT] = [ZERO; Stage::COUNT];
static CALLS: [AtomicU64; Stage::COUNT] = [ZERO; Stage::COUNT];
#[inline(always)]
fn now() -> u64 {
#[cfg(all(feature = "profile", any(target_arch = "x86", target_arch = "x86_64")))]
{
#[allow(unsafe_code)]
unsafe {
#[cfg(target_arch = "x86")]
use core::arch::x86::_rdtsc;
#[cfg(target_arch = "x86_64")]
use core::arch::x86_64::_rdtsc;
_rdtsc()
}
}
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
{
0
}
}
pub struct Guard {
stage: usize,
start: u64,
}
impl Drop for Guard {
#[inline(always)]
fn drop(&mut self) {
let elapsed = now().wrapping_sub(self.start);
CYCLES[self.stage].fetch_add(elapsed, Ordering::Relaxed);
CALLS[self.stage].fetch_add(1, Ordering::Relaxed);
}
}
#[inline(always)]
pub fn scope(stage: Stage) -> Guard {
Guard {
stage: stage as usize,
start: now(),
}
}
pub fn reset() {
for i in 0..Stage::COUNT {
CYCLES[i].store(0, Ordering::Relaxed);
CALLS[i].store(0, Ordering::Relaxed);
}
}
pub fn snapshot() -> [(f64, u64); Stage::COUNT] {
let mut out = [(0.0, 0); Stage::COUNT];
for i in 0..Stage::COUNT {
out[i] = (
CYCLES[i].load(Ordering::Relaxed) as f64,
CALLS[i].load(Ordering::Relaxed),
);
}
out
}
pub fn scope_cost_cycles() -> f64 {
const ITERS: usize = 200_000;
let mut best = f64::MAX;
for _ in 0..5 {
let t0 = now();
for _ in 0..ITERS {
let g = scope(Stage::Headers);
core::hint::black_box(&g);
}
let t1 = now();
let with = t1.wrapping_sub(t0) as f64;
let t0 = now();
for _ in 0..ITERS {
core::hint::black_box(0u64);
}
let t1 = now();
let without = t1.wrapping_sub(t0) as f64;
best = best.min((with - without) / ITERS as f64);
}
reset();
best.max(0.0)
}
pub fn dump() -> String {
let snap = snapshot();
let total = snap[Stage::Total as usize].0.max(1.0);
let mut s = String::from("stage cycles% Mcycles calls\n");
let mut named = 0.0;
for i in 1..Stage::COUNT {
let (cy, calls) = snap[i];
if !Stage::from_index(i).is_info() {
named += cy;
}
s.push_str(&format!(
"{:<16} {:>7.2}% {:>12.1} {:>10}\n",
Stage::from_index(i).name(),
100.0 * cy / total,
cy / 1e6,
calls
));
}
let residue = total - named;
let scopes: u64 = snap.iter().map(|(_, c)| *c).sum();
s.push_str(&format!(
"{:<16} {:>7.2}% {:>12.1} {:>10}\n",
"residue",
100.0 * residue / total,
residue / 1e6,
"-"
));
s.push_str(&format!(
"{:<16} {:>7.2}% {:>12.1} {:>10}\n",
"Total",
100.0,
total / 1e6,
snap[Stage::Total as usize].1
));
let cost_per_scope = scope_cost_cycles();
let mut corrected = [0f64; Stage::COUNT];
let mut corrected_named = 0.0;
for i in 1..Stage::COUNT {
let (cy, calls) = snap[i];
corrected[i] = (cy - calls as f64 * cost_per_scope).max(0.0);
if !Stage::from_index(i).is_info() {
corrected_named += corrected[i];
}
}
let probe_total = scopes as f64 * cost_per_scope;
let real_total = (total - probe_total).max(1.0);
let real_residue = (real_total - corrected_named).max(0.0);
s.push_str("\nprobe-corrected (subtract calls x measured scope cost):\n");
let mut ranked: Vec<usize> = (1..Stage::COUNT)
.filter(|i| !Stage::from_index(*i).is_info())
.collect();
ranked.sort_by(|a, b| corrected[*b].partial_cmp(&corrected[*a]).unwrap());
for i in ranked {
s.push_str(&format!(
"{:<16} {:>7.2}% {:>12.1}\n",
Stage::from_index(i).name(),
100.0 * corrected[i] / real_total,
corrected[i] / 1e6,
));
}
s.push_str(&format!(
"{:<16} {:>7.2}% {:>12.1}\n",
"residue",
100.0 * real_residue / real_total,
real_residue / 1e6,
));
let cost = scope_cost_cycles();
let probe = scopes as f64 * cost;
s.push_str(&format!(
"\n{} scope entries x {:.1} cycles/scope = {:.1} Mcycles = {:.1}% of Total\n\
\n\
CAVEAT: {:.1} cycles/scope is measured in an EMPTY loop, so it is an\n\
UPPER bound - in a real loop the two `lock xadd`s partly overlap with\n\
surrounding work. The probe-corrected table above therefore\n\
OVER-subtracts high-call stages. If a per-block kernel there reads\n\
implausibly cheap, that is this effect, not a fast kernel.\n\
The fix is fewer scopes, not a better estimate: coarsen per-block\n\
scopes to per-block-row and the ambiguity goes away.\n",
scopes,
cost,
probe / 1e6,
100.0 * probe / total,
cost,
));
s
}
}
pub use imp::{dump, reset, scope, snapshot, Guard};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(usize)]
pub enum Count {
Symbols = 0,
BitWrites,
Bits,
BufferFlushes,
StuffedFlushes,
NonZeroAc,
DecBlocks,
DecDcOnlyBlocks,
DecRefills,
DecBytesRead,
DecSymbols,
DecReceiveExtend,
DecBulkRefills,
DecLutHit,
DecLutMiss,
DecFastAcHit,
DecFastAcMiss,
DecIdctPairs,
DecBottomHalfZero,
DecTopRowOnly,
GetBlockInterior,
GetBlockEdge,
DecUpsampleRows,
DecCoefSpanSum,
}
impl Count {
pub const COUNT: usize = 24;
pub fn name(self) -> &'static str {
match self {
Count::Symbols => "symbols",
Count::BitWrites => "bit_writes",
Count::Bits => "bits",
Count::BufferFlushes => "buffer_flushes",
Count::StuffedFlushes => "stuffed_flushes",
Count::NonZeroAc => "nonzero_ac",
Count::DecBlocks => "dec_blocks",
Count::DecDcOnlyBlocks => "dec_dc_only",
Count::DecRefills => "dec_refills",
Count::DecBytesRead => "dec_bytes_read",
Count::DecSymbols => "dec_symbols",
Count::DecReceiveExtend => "dec_receive_extend",
Count::DecBulkRefills => "dec_bulk_refills",
Count::DecLutHit => "lut_hit",
Count::DecLutMiss => "lut_MISS",
Count::DecFastAcHit => "fast_ac_hit",
Count::DecFastAcMiss => "fast_ac_miss",
Count::DecIdctPairs => "idct_PAIRS",
Count::DecBottomHalfZero => "bottom_half_zero",
Count::DecTopRowOnly => "top_row_only",
Count::DecCoefSpanSum => "coef_span_sum",
Count::DecUpsampleRows => "upsample_rows",
Count::GetBlockInterior => "getblock_interior",
Count::GetBlockEdge => "getblock_EDGE",
}
}
}
#[cfg(not(feature = "counters"))]
mod counters {
use super::Count;
#[inline(always)]
pub fn bump(_c: Count, _n: u64) {}
pub fn read() -> [u64; Count::COUNT] {
[0; Count::COUNT]
}
pub fn reset_counts() {}
}
#[cfg(feature = "counters")]
mod counters {
use super::Count;
use core::sync::atomic::{AtomicU64, Ordering};
#[allow(clippy::declare_interior_mutable_const)]
const ZERO: AtomicU64 = AtomicU64::new(0);
static COUNTS: [AtomicU64; Count::COUNT] = [ZERO; Count::COUNT];
#[inline(always)]
pub fn bump(c: Count, n: u64) {
COUNTS[c as usize].fetch_add(n, Ordering::Relaxed);
}
pub fn read() -> [u64; Count::COUNT] {
let mut out = [0; Count::COUNT];
for i in 0..Count::COUNT {
out[i] = COUNTS[i].load(Ordering::Relaxed);
}
out
}
pub fn reset_counts() {
for c in COUNTS.iter() {
c.store(0, Ordering::Relaxed);
}
}
}
pub use counters::{bump, read, reset_counts};
#[cfg(all(test, feature = "profile"))]
mod nesting_tests {
use super::*;
#[test]
fn parent_scope_is_never_smaller_than_its_child() {
reset();
for _ in 0..200 {
let _outer = scope(Stage::DecRowDispatch);
for _ in 0..50 {
let _inner = scope(Stage::DecIdct);
core::hint::black_box(0u64);
}
}
let snap = snapshot();
let outer = snap[Stage::DecRowDispatch as usize].0;
let inner = snap[Stage::DecIdct as usize].0;
assert!(
outer >= inner,
"parent {outer} < nested child {inner} - the profiler itself is unsound"
);
}
}