ppad-censor-0.6.0: Anytime-valid sequential constant-time testing.
Copyright(c) 2026 Jared Tobin
LicenseMIT
MaintainerJared Tobin <jared@ppad.tech>
Safe HaskellNone
LanguageHaskell2010

Censor

Description

Sequential constant-time testing.

Declare a Hypothesis (an IO action plus two input samplers); runCT measures paired class-A / class-B timings in random order, streams them through an anytime-valid test, and either rejects (a leak is detected) or exhausts its budget. Type-I error is controlled at cfgAlpha whenever the run stops.

The null

The null is conditional exchangeability of the measured pair (ta, tb): given the past, swapping the class labels leaves the joint law unchanged. The randomised per-pair order supplies it, provided the nuisance dynamics within a pair (cache and predictor state, frequency) are blind to the class labels; randomisation cannot undo a nuisance process that reacts to the class itself.

The test

Every component bets on an antisymmetric functional of the pair (g(ta, tb) = -g(tb, ta)), which is conditionally symmetric about zero under the null. The test is a uniform mixture of e-processes (Numeric.Eproc.Mixture) over a family of them, with d = ta - tb:

  • sign: sign d. Catches an asymmetry of d itself, including bulk shifts that heavy tails hide from the magnitude components.
  • magnitude: d clipped to [-c, c], [-4c, 4c], and [-16c, 16c], for a warmup-estimated c. The tight bound is the most powerful against small consistent shifts; the wide ones keep rare outliers.
  • CDF: 1[ta <= q] - 1[tb <= q] at warmup-fixed cut points q. Catches dispersion leaks, which leave d symmetric.

The family is finite, so the test is not exhaustive: a departure that moves none of these functionals off zero passes. Read a Pass as "no evidence on these channels", not as proof of constant time.

Synopsis

Hypothesis

data Hypothesis a Source #

A constant-time hypothesis.

  • target: the IO action under test.
  • prepare: a per-pair prologue, run before either sampler and outside the timed region; pure () unless the classes share per-pair state (see fixVsRandomCtx).
  • sampleA, sampleB: samplers for the two input classes, typically "fixed" and "random".

Only target is timed, so any asymmetry in how the samplers prepare inputs shows up as a class difference. Return inputs in normal form, build the fixed class by the same code path as the random one (not as a top-level constant), and keep the samplers symmetric in allocation and in where the secret is read from. fixVsRandom does this for the usual case.

Constructors

Hypothesis 

Fields

fixVsRandom Source #

Arguments

:: (a -> IO ())

target

-> IO s

secret sampler

-> (s -> IO s)

re-materialise a secret into a fresh value

-> (s -> IO a)

complete an input around a secret

-> IO (Hypothesis a) 

A dudect-style fix-vs-random Hypothesis, from a target, a secret sampler, a re-materialisation function, and a completion building a full input around a secret.

The fixed secret is drawn once from the sampler, so it is an ordinary heap value like the random class's. Each sample, class A draws and discards a fresh secret and class B keeps its draw, so the samplers allocate alike. Both classes then re-materialise the pinned secret: class A completes around the copy, class B discards it. With a real copy (for a ByteString, \s -> evaluate (BS.copy s)), neither class is alone in reading one long-lived, cache-hot value, a locality asymmetry that the A/A and B/B controls cannot see. Pass pure only for types with no cheap copy.

The completion runs identically in both classes, so per-pair public inputs stay fresh on both sides. The sampler, re-materialiser, and completion must return values in normal form.

hyp <- fixVsRandom
  (\x -> () <$ evaluate (inv x))   -- target
  (randomMont g)                   -- secret sampler
  pure                             -- no re-materialiser
  pure                             -- input is just the secret

fixVsRandomCtx Source #

Arguments

:: (a -> IO ())

target

-> IO s

secret sampler

-> (s -> IO s)

re-materialise a secret into a fresh value

-> IO c

public-context sampler (once per pair)

-> (c -> s -> IO a)

build an input from context and secret

-> IO (Hypothesis a) 

A fix-vs-random Hypothesis whose two classes share one freshly drawn public context per pair.

fixVsRandom completes each class independently, so per-pair public inputs (message, nonce, modulus) either differ across the pair, adding nuisance variance to d, or are pinned for the whole run, testing a single public input. Here prepare draws one context per pair and both classes are built around it, so the test is of secret dependence given identical fresh public input. The null is unaffected: given the context, the two readings are still exchangeable under H_0.

The re-materialisation function plays the same role as in fixVsRandom.

hyp <- fixVsRandomCtx
  (\(k, m) -> () <$ evaluate (mac k m))  -- target
  (randomBytes g 32)                     -- secret sampler
  (\k -> evaluate (BS.copy k))           -- re-materialise
  (randomBytes g 256)                    -- public context
  (\msg key -> pure (key, msg))          -- build the input

Configuration

data Config Source #

Driver configuration.

Constructors

Config 

Fields

  • cfgAlpha :: !Double

    significance level.

  • cfgBudget :: !Int

    maximum sample pairs to consume.

  • cfgBatch :: !Int

    target repetitions per timed region, all on one pre-drawn sample. This multiplies real work only when each repetition recomputes (an FFI call, mutable state). For a pure target \a -> evaluate (f a) the thunk is forced once and the rest are no-ops, so use 1, and widen the work if it is sub-quantum.

  • cfgWarmup :: !Int

    warmup pairs, from which the clip bound c (twice the ~p99 of |d|) and the CDF cut points are fixed. At 100 or fewer, the p99 is the sample maximum.

  • cfgSeed :: !(Maybe Word64)

    seed for the per-pair A/B order bit. Nothing (the default) draws one from /dev/urandom, falling back to the monotonic clock if that is unreadable. The seed used is reported as resSeed for replay. Keep it independent of any sampler seed: the null needs the order bit independent of the timings.

  • cfgMargin :: !(Maybe Double)

    relative interval-null margin. Nothing (the default) tests the sharp null. Just m resolves at warmup to an absolute margin delta = m * median of the pooled per-batch readings and tests |E d_clipped| <= delta: the magnitude components become interval-null tests and alone gate the verdict, while the sign and CDF components still report peaks as diagnostics. Relative, because the channels that motivate it (frequency, interrupts, scheduling) scale with the timed-region duration. Meant for the wall meter on frequency-scaled hosts (see the README); keep it Nothing under the PMU meters. The resolved margin must land in (0, c), else the run throws InvalidConfig; it is reported as resMargin.

Instances

Instances details
Show Config Source # 
Instance details

Defined in Censor

defaultConfig :: Config Source #

Defaults: alpha = 1e-6, budget 100000 pairs, batch 1 (no inner repetition), warmup 200 pairs, order-bit seed drawn from OS entropy per run, no margin (sharp null).

Result

data Result Source #

Test outcome: Reject (a leak was detected) or Pass (none within budget), each carrying the same run report.

Evidence is on the log e-value scale: a run starts at 0 and rejects when the mixture's running supremum (resPeakLogW) crosses log(1 / alpha). The per-component peaks say which channel found the evidence. They are diagnostic only: each is a supremum at its own time, and the verdict latches on the mixture.

Constructors

Reject 

Fields

  • resPairs :: !Int

    sample pairs consumed.

  • resPeakLogW :: !Double

    peak (supremum-so-far) log e-value of the mixture; starts at 0, crosses log(1 / alpha) on rejection.

  • resPValue :: !Double

    the anytime-valid p-value min 1 (exp -resPeakLogW); at or below cfgAlpha iff the verdict is Reject.

  • resEffect :: !(Double, Double)

    anytime-valid 1 - cfgAlpha confidence interval for the mean of d clipped to [-16c, 16c], in meter units per batch. Covers zero on a constant-time target. It can still be wide after an early Reject: detection outpaces estimation. Once the evidence pins the mean below the estimation grid's resolution, the last resolvable interval is reported. resClipped16 says how often the clipping bit.

  • resClipped :: !Int

    pairs whose |d| exceeded resBound and was clipped for the tight magnitude component. A large share of resPairs means the bound was too tight: raise cfgWarmup or cfgBatch.

  • resClipped4 :: !Int

    pairs whose |d| exceeded 4 * resBound.

  • resClipped16 :: !Int

    pairs whose |d| exceeded 16 * resBound, the bound behind the widest magnitude component and resEffect.

  • resBound :: !Double

    the tight clip bound c estimated during warmup (max 1 (2 * p99(|d|))).

  • resPeakSign :: !Double

    peak log e-value of the sign component.

  • resPeakMagn :: !Double

    peak log e-value of the magnitude component at c.

  • resPeakMagn4 :: !Double

    peak log e-value of the magnitude component at 4c.

  • resPeakMagn16 :: !Double

    peak log e-value of the magnitude component at 16c.

  • resCdf :: ![(Double, Double)]

    the CDF channel: each warmup-fixed cut point (ascending) with its component's peak log e-value, so a CDF-driven rejection names the threshold that separated the classes. Empty when warmup readings were degenerate.

  • resSeed :: !Word64

    the order-bit seed the run used; feed it back as cfgSeed to replay the same A/B order.

  • resMargin :: !(Maybe Double)

    the absolute margin resolved from cfgMargin, in meter units per batch; Nothing under the sharp null. When present, a Reject means the mean effect exceeds it, and a high resPeakSign on a Pass marks a systematic within tolerance.

Pass 

Fields

  • resPairs :: !Int

    sample pairs consumed.

  • resPeakLogW :: !Double

    peak (supremum-so-far) log e-value of the mixture; starts at 0, crosses log(1 / alpha) on rejection.

  • resPValue :: !Double

    the anytime-valid p-value min 1 (exp -resPeakLogW); at or below cfgAlpha iff the verdict is Reject.

  • resEffect :: !(Double, Double)

    anytime-valid 1 - cfgAlpha confidence interval for the mean of d clipped to [-16c, 16c], in meter units per batch. Covers zero on a constant-time target. It can still be wide after an early Reject: detection outpaces estimation. Once the evidence pins the mean below the estimation grid's resolution, the last resolvable interval is reported. resClipped16 says how often the clipping bit.

  • resClipped :: !Int

    pairs whose |d| exceeded resBound and was clipped for the tight magnitude component. A large share of resPairs means the bound was too tight: raise cfgWarmup or cfgBatch.

  • resClipped4 :: !Int

    pairs whose |d| exceeded 4 * resBound.

  • resClipped16 :: !Int

    pairs whose |d| exceeded 16 * resBound, the bound behind the widest magnitude component and resEffect.

  • resBound :: !Double

    the tight clip bound c estimated during warmup (max 1 (2 * p99(|d|))).

  • resPeakSign :: !Double

    peak log e-value of the sign component.

  • resPeakMagn :: !Double

    peak log e-value of the magnitude component at c.

  • resPeakMagn4 :: !Double

    peak log e-value of the magnitude component at 4c.

  • resPeakMagn16 :: !Double

    peak log e-value of the magnitude component at 16c.

  • resCdf :: ![(Double, Double)]

    the CDF channel: each warmup-fixed cut point (ascending) with its component's peak log e-value, so a CDF-driven rejection names the threshold that separated the classes. Empty when warmup readings were degenerate.

  • resSeed :: !Word64

    the order-bit seed the run used; feed it back as cfgSeed to replay the same A/B order.

  • resMargin :: !(Maybe Double)

    the absolute margin resolved from cfgMargin, in meter units per batch; Nothing under the sharp null. When present, a Reject means the mean effect exceeds it, and a high resPeakSign on a Pass marks a systematic within tolerance.

Instances

Instances details
Show Result Source # 
Instance details

Defined in Censor

resPeakCdf :: Result -> Double Source #

The largest peak log e-value across the CDF-indicator components — 0 when warmup resolved no cut points. A maximum over resCdf, which keeps the per-cut-point detail.

Advisories

data Advisory Source #

An actionable observation about a Result, produced by diagnose.

Constructors

HighClipRate !ClipRates

More than 5% of pairs were clipped at c, starving the tight magnitude component: raise cfgWarmup or cfgBatch, or treat the run with suspicion. Carries the rate at all three bounds; a nonzero rate at 16c means resEffect describes a truncated variable.

LowPower !Double

On Pass only: the effect-interval half-width exceeded the tight clip bound c (the ratio is carried here, in units of c). Leaks with a mean shift below that scale would not have been resolved by this run; raise cfgBudget to sharpen the interval.

RejectionDriver !LeakShape

On Reject only: which channel of the hedge drove the detection, i.e. what shape of leak was found.

Instances

Instances details
Show Advisory Source # 
Instance details

Defined in Censor

Eq Advisory Source # 
Instance details

Defined in Censor

data ClipRates Source #

The share of pairs clipped at each of the hedge's three magnitude bounds, carried by HighClipRate. Nested by construction: clipWidest <= clipWide <= clipTight, since |d| > 16c implies |d| > 4c implies |d| > c.

Constructors

ClipRates 

Fields

Instances

Instances details
Show ClipRates Source # 
Instance details

Defined in Censor

Eq ClipRates Source # 
Instance details

Defined in Censor

data LeakShape Source #

The qualitative shape of a leak, inferred from the per-component peaks of the hedge. Reported by RejectionDriver and available standalone via leakShape.

Constructors

BulkShift

resPeakMagn (tight magnitude) dominates: a small consistent mean shift in d.

ShapeLeak

resPeakSign dominates: an equal-mean asymmetry of the difference itself (P(d > 0) /= 1/2).

RareOutlier

resPeakMagn4 or resPeakMagn16 dominates: rare-tail outliers (e.g. a rarely-taken data-dependent branch).

CdfShift

resPeakCdf dominates: the two classes' CDFs differ at a warmup-fixed cut point. Typical of a dispersion (variance) leak, which leaves d symmetric and so registers on no other channel.

MixedShape

No single component peak dominates by >= 1.5x; the leak registers on several channels comparably.

Instances

Instances details
Show LeakShape Source # 
Instance details

Defined in Censor

Eq LeakShape Source # 
Instance details

Defined in Censor

diagnose :: Result -> [Advisory] Source #

Interpret a Result: heavy clipping, a low-power Pass (the effect interval is wider than the tight bound), and, on Reject, which channel of the hedge drove the detection. Always in that order, so tooling can match by position.

leakShape :: Result -> LeakShape Source #

Identify which channel of the hedge has the largest peak log e-value in a Result, binning the two wider magnitude components together as RareOutlier and the CDF-indicator components together as CdfShift. Returns MixedShape when no single peak exceeds the next-largest by a factor of 1.5.

Only meaningful on a Reject — on a Pass every peak sits near the calibrated floor of 0 and the returned shape is not informative.

Under a margin-mode run (resMargin present) only the gating magnitude components are ranked: the sign and CDF peaks did not drive the verdict there, and a sub-margin systematic routinely inflates them.

Errors

data CensorError Source #

Errors thrown by runCT when calibration or configuration fails.

Constructors

WarmupZeroDuration

warmup observed only zero-duration measurements: the meter cannot resolve the target under the current cfgBatch. Raise the batch size so the target runs in an inner loop. (Zero differences across all warmup pairs are OK — that just means the target is deterministic in |d| for the sampled inputs, which is the expected happy path under a PMU meter on truly CT code.)

InvalidConfig !String

a Config field was outside its admissible range. The String names which field and why.

InvalidEprocConfig !ConfigError

the underlying e-process rejected the test configuration derived from cfgAlpha and the warmup-estimated bound. In practice this is reachable only via a cfgAlpha outside (0, 1); adjust cfgAlpha to the standard significance range.

Running

runCT :: Meter -> Config -> Hypothesis a -> IO Result Source #

Run a constant-time test. See runCTWith for the phase-by-phase description; this is that driver with the per-pair observer discarded.

runCTWith :: Meter -> Config -> Hypothesis a -> (Frame -> IO ()) -> IO Result Source #

Run a constant-time test, handing a Frame to the observer after every main-phase pair. The observer runs outside the timed region and gets the frame lazily, so the no-op observer of runCT costs nothing; a recording one reconstructs the trajectory (see record in Censor.Runner).

  1. Warmup: cfgWarmup pairs fix the clip bound c and the CDF cut points (order statistics of the pooled readings, so they carry no class information).
  2. Main: each pair runs in random order, and every component's functional of (ta, tb) feeds its e-process. The run halts when the mixture crosses 1 / alpha or the budget runs out. d clipped to [-16c, 16c] also feeds the confidence sequence behind resEffect.

With cfgMargin set, warmup also resolves the absolute margin, and only the magnitude components gate the verdict.

Betting on d rather than on the raw readings lets the bet size scale with the noise, p99(|d|), rather than with the target's runtime: a large gain for slow targets with little jitter.

data Frame Source #

A per-pair snapshot of the driver's state, delivered to the observer passed to runCTWith after every main-phase pair. All quantities are on the same scale as the corresponding Result fields; recording the stream of frames reconstructs the wealth trajectory without reimplementing the driver.

Constructors

Frame 

Fields

  • frPair :: !Int

    pair index (1-based; excludes warmup).

  • frDiff :: !Double

    the raw per-pair difference d = ta - tb, pre-clip.

  • frLogW :: !Double

    the mixture's current log e-value at this pair.

  • frLogWSup :: !Double

    its supremum-so-far (the quantity the reject latches on).

  • frLo :: !Double

    effect-interval lower endpoint (meter units per batch).

  • frHi :: !Double

    effect-interval upper endpoint.

  • frClipped :: !Bool

    whether |d| exceeded the warmup bound and was clipped.

Instances

Instances details
Show Frame Source # 
Instance details

Defined in Censor

Methods

showsPrec :: Int -> Frame -> ShowS #

show :: Frame -> String #

showList :: [Frame] -> ShowS #

runCTAttributed :: Meter -> Config -> Hypothesis a -> IO (Result, Maybe Attribution) Source #

Run a constant-time test and, if it rejects, qualify the rejection with the A/A and B/B negative controls (attribute). A Pass returns Nothing (no controls needed); a Reject returns Just att reporting whether either control convicted the harness. The controls re-run the test twice, so this costs up to three runs on a rejection.

Split runs

data Split a Source #

One value per channel of a split run (runSplitWith): a Result each, or the controls' Attribution each.

Constructors

Split 

Fields

  • splitCycles :: !a

    the cycle count: whether the code's execution depends on the class. The verdict is this channel's.

  • splitPeriod :: !a

    wall time per cycle, in femtoseconds: whether the clock the cycles ran at does.

  • splitWall :: !a

    wall time, in nanoseconds: the two together, as elapsed time shows them to an observer.

Instances

Instances details
Functor Split Source # 
Instance details

Defined in Censor

Methods

fmap :: (a -> b) -> Split a -> Split b #

(<$) :: a -> Split b -> Split a #

Show a => Show (Split a) Source # 
Instance details

Defined in Censor

Methods

showsPrec :: Int -> Split a -> ShowS #

show :: Split a -> String #

showList :: [Split a] -> ShowS #

Eq a => Eq (Split a) Source # 
Instance details

Defined in Censor

Methods

(==) :: Split a -> Split a -> Bool #

(/=) :: Split a -> Split a -> Bool #

runSplit :: SplitMeter -> Config -> Hypothesis a -> IO (Split Result) Source #

runSplitWith with the per-pair observer discarded.

runSplitWith :: SplitMeter -> Config -> Hypothesis a -> (Frame -> IO ()) -> IO (Split Result) Source #

Run a constant-time test under a SplitMeter, handing the cycles channel's Frame to the observer after every main-phase pair.

Every pair is read once, as cycles and wall time, and tested on three channels: cycles, the clock period (wall time per cycle), and wall time. Each channel is a full test as runCTWith describes, with its own clip bound and cut points fixed by the same warmup pairs, and each holds its own type-I error at cfgAlpha. The cycles channel decides when the run stops: on its rejection, or at the budget. That stop is a stopping time for the readings of all three, and the order bit is independent of all three, so the other two channels' guarantees hold where it falls.

The cycles channel answers the constant-time question. The period channel answers whether the clock moved with the class, as it does when the core's frequency follows data-dependent power; wall time answers whether elapsed time moved, by either route. A wall rejection beside a cycles pass, with the period rejecting too, is a frequency effect rather than a code path.

The period reads cleanly only beside a cycles pass: the counted region carries a fixed overhead (the clock reads), so wall time per cycle falls as cycles grow, and a cycles difference registers on the period as well.

attributeSplit :: SplitMeter -> Config -> Hypothesis a -> IO (Split Attribution) Source #

The A/A and B/B controls under a SplitMeter: each control is one split run, and each channel's verdict comes from that channel's readings of the two. Only meaningful for a channel that rejected, as for attribute.

Batch calibration

calibrateBatch :: Meter -> Hypothesis a -> IO Int Source #

Pick a cfgBatch that lifts the per-batch reading well above the meter's resolution: probe a class-A sample at geometrically growing batches until the reading reaches a target magnitude, then scale to it. Only meaningful for targets that recompute on every repetition (see cfgBatch).

b <- calibrateBatch meter hyp
r <- runCT meter cfg { cfgBatch = b } hyp

Consumes one prepare and one class-A draw; build a fresh hypothesis for the run if its sampler stream must start undisturbed. Returns 1 if the meter never resolves the target (the run then throws WarmupZeroDuration). The result is meter-specific; pin cfgBatch to compare meters.

data CalibReport Source #

The probe history behind calibrateBatchReport, for when the chosen batch surprises.

Constructors

CalibReport 

Fields

Instances

Instances details
Show CalibReport Source # 
Instance details

Defined in Censor

data Noise Source #

Dispersion of meter readings, in per-batch meter units (the scale of resBound and resEffect). Deterministic meters read an IQR of 0.

Constructors

Noise 

Fields

  • nsMedian :: !Word64

    median reading at the selected batch.

  • nsIqr :: !Word64

    interquartile range of readings at the selected batch.

Instances

Instances details
Show Noise Source # 
Instance details

Defined in Censor

Methods

showsPrec :: Int -> Noise -> ShowS #

show :: Noise -> String #

showList :: [Noise] -> ShowS #

Eq Noise Source # 
Instance details

Defined in Censor

Methods

(==) :: Noise -> Noise -> Bool #

(/=) :: Noise -> Noise -> Bool #

Baseline probe

baselineReading :: Meter -> Config -> Hypothesis a -> IO Noise Source #

Baseline cost of the target: median and IQR of single measurements on fresh class-A draws (each after a prepare), at cfgBatch, in the units of resEffect. Unlike crNoise, which remeasures one applied action (no-ops after the first, for a pure target), every measurement here is genuine work. Give it its own hypothesis instance if the run's sampler stream must start undisturbed.

A/A and B/B negative controls

toAA :: Hypothesis a -> Hypothesis a Source #

The A/A control: sampleB replaced by sampleA. The hypothesis is then H_0 by construction, so a Reject indicts the harness (unbalanced samplers, RTS state, order effects) rather than the target. Pair with toBB.

runAA :: Meter -> Config -> Hypothesis a -> IO Result Source #

runCT meter cfg (toAA hyp).

toBB :: Hypothesis a -> Hypothesis a Source #

The B/B control: sampleA replaced by sampleB, catching asymmetries specific to sampleB that toAA cannot see.

runBB :: Meter -> Config -> Hypothesis a -> IO Result Source #

runCT meter cfg (toBB hyp).

data Attribution Source #

The control verdict with both control runs in full, so the evidence can be inspected rather than taken on trust.

Constructors

Attribution 

Fields

Instances

Instances details
Show Attribution Source # 
Instance details

Defined in Censor

data ControlVerdict Source #

The outcome of the A/A and B/B controls. They can convict the harness but not acquit it: each compares a sampler against itself, so an asymmetry only between the samplers (say, one returns a thunk that the target forces while timed) is invisible to both.

Constructors

ControlsPass

Neither control rejected. A primary Reject is then consistent with a target leak, not proof of one.

SampleAAsymmetry

The A/A control rejected: sampleA itself induces a timing asymmetry, and the primary verdict is not informative until it is fixed.

SampleBAsymmetry

The B/B control rejected: as above, for sampleB.

PervasiveAsymmetry

Both controls rejected: the harness asymmetry is not specific to either sampler.

Instances

Instances details
Show ControlVerdict Source # 
Instance details

Defined in Censor

Eq ControlVerdict Source # 
Instance details

Defined in Censor

attribute :: Meter -> Config -> Hypothesis a -> IO Attribution Source #

Run the A/A and B/B controls. Only meaningful after a Reject: on a symmetric harness both pass whether or not the target leaks.

Measurement (re-exports)

newtype Meter Source #

A measurement device. measure m k act runs act k times back to back and returns one reading for the batch: nanoseconds for wallClock, counted events for a withCounter meter. Each meter owns its inner loop, reading the clock or arming the counter once per batch.

Constructors

Meter 

Fields

wallClock :: Meter Source #

Wall-clock meter: elapsed nanoseconds over the batch.

On Darwin, backed by clock_gettime_nsec_np(CLOCK_UPTIME_RAW), which resolves the hardware tick (~42 ns on Apple Silicon, so sub-microsecond targets need batching; see cfgBatch). Not GHC.Clock.getMonotonicTimeNSec, which macOS quantises to microseconds, putting CDF cut points on quantisation boundaries (see issues/handled/ISSUE2.md). Elsewhere, backed by GHC.Clock.getMonotonicTimeNSec.

data Counter Source #

A hardware (or, for TaskClock, software) event to count.

With wallClock the counters form a ladder in which each adjacent gap isolates one cause of a class difference: instructions (code path) -> cycles (microarchitectural latency) -> ref-cycles (frequency) -> task-clock (kernel and interrupt time) -> wall (scheduling gaps).

Constructors

InstructionsRetired

PERF_COUNT_HW_INSTRUCTIONS; deterministic.

BranchesRetired

PERF_COUNT_HW_BRANCH_INSTRUCTIONS; deterministic.

Cycles

PERF_COUNT_HW_CPU_CYCLES; also sensitive to data-dependent latency.

RefCycles

PERF_COUNT_HW_REF_CPU_CYCLES: cycles at the constant reference rate, so a difference here with clean Cycles is a frequency effect. Not available on every microarchitecture.

TaskClock

PERF_COUNT_SW_TASK_CLOCK: on-CPU nanoseconds, including kernel time on the task's behalf. A software counter, so available in VMs, but counting kernel time needs kernel.perf_event_paranoid at most 1.

BranchMisses

PERF_COUNT_HW_BRANCH_MISSES: catches a secret-dependent branch direction when retired branch counts balance.

CacheMisses

PERF_COUNT_HW_CACHE_MISSES: attributes a cycles-level difference to memory access patterns.

Instances

Instances details
Show Counter Source # 
Instance details

Defined in Censor.Meter

Eq Counter Source # 
Instance details

Defined in Censor.Meter

Methods

(==) :: Counter -> Counter -> Bool #

(/=) :: Counter -> Counter -> Bool #

data MeterError Source #

Why a withCounter meter could not be opened.

Constructors

Unsupported

not a Linux build; no hardware-counter support

OpenFailed !Int

perf_event_open failed with this errno (commonly EACCES/EPERM when kernel.perf_event_paranoid is too high)

withCounter :: Counter -> (Meter -> IO a) -> IO a Source #

Acquire a performance-counter Meter for the duration of the callback, then release it. Throws MeterError if the counter cannot be opened.

Counters count user space only, which unprivileged processes may open at kernel.perf_event_paranoid 2 or lower; TaskClock deliberately includes kernel time and needs 1 or lower, failing with OpenFailed 13 otherwise rather than silently degrading to a user-only clock. A counter is bound to the thread that opened it, so the callback runs on one OS thread.

newtype SplitMeter Source #

A meter reading two quantities over one execution of the batch: measureSplit m k act returns (cycles, nanoseconds). Wall time is cycles times the clock period, and a secret can move it through either factor: through cycles when the code's execution depends on it, through the period when the core's frequency does (data-dependent power under frequency scaling). Reading both on the same execution tells the two apart.

Constructors

SplitMeter 

Fields

withSplitMeter :: (SplitMeter -> IO a) -> IO a Source #

Acquire a SplitMeter for the duration of the callback, then release it. Throws MeterError if the cycle counter cannot be opened.

The wall clock is read inside the counted region, so both readings cover the batch and the clock reads; the overhead is the same in both classes. As with withCounter, the callback runs on one OS thread.

Sampler support (re-exports)

data Rng Source #

splitmix64 state threaded through an IORef.

mkRng :: Word64 -> IO Rng Source #

Create a generator from a seed. Equal seeds yield equal streams.

nextWord :: Rng -> IO Word64 Source #

Draw the next Word64 from the generator.

reseed :: Rng -> Word64 -> IO () Source #

Reset a generator to a seed in place: after reseed g s the generator draws exactly the stream of a fresh mkRng s.

Lets one long-lived generator replay a pinned stream, or adopt a fresh one, without allocating per use — so the two classes of a fix-vs-random sampler can share a single generator object and differ only in the seed value written into it.

randomBytes :: Rng -> Int -> IO ByteString Source #

Draw n pseudorandom bytes as a strict ByteString.

The result is a fresh, fully-materialised heap value -- no thunks for the meter to pay inside the timed region -- so a sampler can return it directly.