ppad-censor-0.5.1: lib/Censor/Runner.hs
{-# OPTIONS_HADDOCK prune #-}
{-# LANGUAGE BangPatterns #-}
-- |
-- Module: Censor.Runner
-- Copyright: (c) 2026 Jared Tobin
-- License: MIT
-- Maintainer: Jared Tobin <jared@ppad.tech>
--
-- Batteries-included harness support for "Censor": meter selection
-- from a command-line token, trace recording (built on
-- 'Censor.runCTWith', so it does not reimplement the driver), and
-- parameter sweeps.
module Censor.Runner (
-- * Meter selection
MeterChoice(..)
, parseMeter
, meterName
, withMeterArg
-- * Recording traces
, Trace(..)
, record
, thin
-- * Sweeps
, sweep
-- * Verdicts and rendering
, verdictName
, shapeName
, summary
, clipText
) where
import Control.Exception (try)
import Control.Monad (forM)
import Data.IORef (newIORef, modifyIORef', readIORef)
import System.Exit (die)
import Text.Printf (printf)
import Censor
-- meter selection -------------------------------------------------------------
-- | A meter chosen by name. The PMU variants carry their display
-- label alongside the 'Counter'.
data MeterChoice
= MeterWall
| MeterPMU !Counter !String
-- | Parse a meter name: @wall@, @instructions@, @cycles@,
-- @branches@, @ref-cycles@, @task-clock@, @branch-misses@, or
-- @cache-misses@.
parseMeter :: String -> Maybe MeterChoice
parseMeter s = case s of
"wall" -> Just MeterWall
"instructions" -> Just (MeterPMU InstructionsRetired "instructions")
"cycles" -> Just (MeterPMU Cycles "cycles")
"branches" -> Just (MeterPMU BranchesRetired "branches")
"ref-cycles" -> Just (MeterPMU RefCycles "ref-cycles")
"task-clock" -> Just (MeterPMU TaskClock "task-clock")
"branch-misses" -> Just (MeterPMU BranchMisses "branch-misses")
"cache-misses" -> Just (MeterPMU CacheMisses "cache-misses")
_ -> Nothing
-- | The display name of a chosen meter.
meterName :: MeterChoice -> String
meterName MeterWall = "wall"
meterName (MeterPMU _ name) = name
-- | Parse a meter token (defaulting to wall-clock when absent) and
-- run the callback with the resolved t'Meter' and its name,
-- bracketing a PMU counter for its lifetime and dying with a clear
-- message if it cannot be opened (e.g. the PMU meters off Linux, or
-- an unknown name). Handles the whole "wall everywhere, PMU
-- Linux-only, hard error otherwise" dance in one place.
withMeterArg :: Maybe String -> (String -> Meter -> IO a) -> IO a
withMeterArg marg k = do
choice <- case marg of
Nothing -> pure MeterWall
Just s -> case parseMeter s of
Just c -> pure c
Nothing -> die $ "unknown meter " ++ show s
++ " (expected wall|instructions|cycles|branches|ref-cycles"
++ "|task-clock|branch-misses|cache-misses)"
case choice of
MeterWall -> k "wall" wallClock
MeterPMU cnt name -> do
r <- try (withCounter cnt (k name))
case r of
Right a -> pure a
Left e -> die $ name ++ " meter unavailable: "
++ show (e :: MeterError) ++ hint e
where
-- EPERM/EACCES means the kernel refused a counter that exists,
-- which for an unprivileged process is the paranoid level.
-- task-clock counts kernel time and so needs a lower one than
-- the rest; see Censor.Meter.
hint (OpenFailed n)
| n == 1 || n == 13 =
" (check kernel.perf_event_paranoid: task-clock needs 1"
++ " or lower, the other counters 2 or lower)"
hint _ = ""
-- recording -------------------------------------------------------------------
-- | A recorded run: the final 'Result' plus the per-pair t'Frame'
-- trajectory.
data Trace = Trace
{ traceName :: !String
, traceMeter :: !String
, traceResult :: !Result
, traceFrames :: ![Frame]
} deriving Show
-- | Record a run, capturing every t'Frame' via 'runCTWith'. The
-- observer strictly accumulates frames; the driver is 'runCTWith'
-- itself, not a reimplementation of it.
record :: String -> String -> Meter -> Config -> Hypothesis a -> IO Trace
record name mname meter cfg hyp = do
ref <- newIORef []
res <- runCTWith meter cfg hyp $ \ !fr -> modifyIORef' ref (fr :)
frs <- readIORef ref
pure $! Trace name mname res (reverse frs)
-- | Downsample a frame list for compact plotting: keep all of the
-- first 150 frames (the early motion), then a uniform stride, then
-- the final frame.
thin :: [Frame] -> [Frame]
thin frs =
let !m = length frs
!stride = max 1 (m `div` 450)
keep fr = frPair fr <= 150
|| frPair fr `mod` stride == 0
|| frPair fr == m
in filter keep frs
-- sweeps ----------------------------------------------------------------------
-- | Run a parameterised family of hypotheses under one meter and
-- return each parameter's result: the recurring "control vs swept
-- input" pattern (a mismatch position, a length, ...).
sweep
:: Meter -> Config -> (p -> IO (Hypothesis a)) -> [p] -> IO [(p, Result)]
sweep meter cfg mk ps = forM ps $ \p -> do
h <- mk p
r <- runCT meter cfg h
pure (p, r)
-- verdicts and rendering ------------------------------------------------------
-- | @\"REJECT\"@ or @\"PASS\"@.
verdictName :: Result -> String
verdictName Reject{} = "REJECT"
verdictName Pass{} = "PASS"
-- | A one-line human-readable render of a t'Result', suitable for
-- logging or embedding in a larger status line. Includes the
-- verdict, pairs consumed, evidence (peak log e-value, p-value),
-- effect interval, and clip count; on 'Reject' it also names the
-- driver channel of the hedge ('leakShape').
--
-- > REJECT after 342 pairs; driver=bulk peakLogW=18.42 p=1.0e-8
-- > effect=[-101.5,-98.4] clipped=6/342
summary :: Result -> String
summary r =
let !effLo = fst (resEffect r)
!effHi = snd (resEffect r)
base = printf "peakLogW=%.2f effect=[%.1f,%.1f] %s"
(resPeakLogW r) effLo effHi (clipText r)
in case r of
Reject{} ->
printf "REJECT after %d pairs; driver=%s %s p=%.2g"
(resPairs r) (shapeName (leakShape r))
(base :: String) (resPValue r)
Pass{} ->
printf "PASS at %d pairs; %s"
(resPairs r) (base :: String)
-- | Render a t'Result'\'s clipping as one field. The tight count is
-- always shown; the wider bounds are appended only when clipping
-- actually reached them, so the common case stays as terse as it
-- was and a run whose effect interval saw truncated input says so.
--
-- > clipped=6/342
-- > clipped=129/138 (4c 44, 16c 3)
clipText :: Result -> String
clipText r
| resClipped4 r == 0 && resClipped16 r == 0 = tight
| otherwise = tight ++ printf " (4c %d, 16c %d)"
(resClipped4 r) (resClipped16 r)
where
tight = printf "clipped=%d/%d" (resClipped r) (resPairs r) :: String
-- | The short label for a 'LeakShape', as it appears on a case row
-- and in the @advisories@ block of the JSON report.
shapeName :: LeakShape -> String
shapeName s = case s of
BulkShift -> "bulk"
ShapeLeak -> "shape"
RareOutlier -> "tail"
CdfShift -> "cdf"
MixedShape -> "mixed"