-- SPDX-FileCopyrightText: 2026 Alexandra de Wit
--
-- SPDX-License-Identifier: MIT

{- | The memory budget's feedback: how one sample of the collector and the heap moves the budget.

After each major collection the brake measures the live data outside the charges, so the budget may
grow until charges and that measured remainder reach the live ceiling. The ceiling also leaves room
under the heap's overflow point for the largest request seen lately, which may run past the budget.
The brake halves the budget when the collector takes too much of the CPU or the heap nears overflow,
and grows it back in small steps while the collector stays calm. "Ecluse.Rts.Sampler" feeds it.
-}
module Ecluse.Core.Server.Admission.Brake (
    -- * The marks
    BrakeMarks (..),
    defaultBrakeMarks,

    -- * One step
    BrakeState (..),
    initialBrakeState,
    GcSample (..),
    brakeStep,

    -- * Reading the collector
    CollectorReading (..),
    gcSharePermille,
    SampleWindow,
    newSampleWindow,
    windowSample,
) where

import Data.Bits (shiftR)

import Ecluse.Core.Server.Admission.Types (BrakeBounds (..), BrakeLevel (..))

-- | The brake's thresholds. Shares are thousandths of the process's CPU time over the sampler's window.
data BrakeMarks = BrakeMarks
    { BrakeMarks -> Int
bmGcHighPermille :: Int
    -- ^ Above this GC share of CPU, the budget halves.
    , BrakeMarks -> Int
bmGcLowPermille :: Int
    -- ^ Below this GC share of CPU, the budget may grow.
    , BrakeMarks -> Int
bmKernelHighPermille :: Int
    -- ^ Above this share of the cgroup limit in use (less reclaimable file pages), the budget halves.
    , BrakeMarks -> Int
bmOverflowGuardPermille :: Int
    -- ^ Above this share of the copying collector's overflow point in live data, the budget halves.
    , BrakeMarks -> Int
bmCalmSamples :: Int
    -- ^ Consecutive calm samples before one growth step.
    , BrakeMarks -> Int
bmGrowPermille :: Int
    -- ^ One growth step, as a share of the current budget.
    , BrakeMarks -> Int
bmReturnPermille :: Int
    -- ^ How much of a fall in the measured outside live data one major collection gives back.
    , BrakeMarks -> Int
bmForgetShift :: Int
    -- ^ The largest request seen fades by one part in @2^shift@ per sample.
    , BrakeMarks -> Int
bmCooldownSamples :: Int
    -- ^ The fewest samples from one halving to the next.
    }
    deriving stock (BrakeMarks -> BrakeMarks -> Bool
(BrakeMarks -> BrakeMarks -> Bool)
-> (BrakeMarks -> BrakeMarks -> Bool) -> Eq BrakeMarks
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: BrakeMarks -> BrakeMarks -> Bool
== :: BrakeMarks -> BrakeMarks -> Bool
$c/= :: BrakeMarks -> BrakeMarks -> Bool
/= :: BrakeMarks -> BrakeMarks -> Bool
Eq, Int -> BrakeMarks -> ShowS
[BrakeMarks] -> ShowS
BrakeMarks -> String
(Int -> BrakeMarks -> ShowS)
-> (BrakeMarks -> String)
-> ([BrakeMarks] -> ShowS)
-> Show BrakeMarks
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> BrakeMarks -> ShowS
showsPrec :: Int -> BrakeMarks -> ShowS
$cshow :: BrakeMarks -> String
show :: BrakeMarks -> String
$cshowList :: [BrakeMarks] -> ShowS
showList :: [BrakeMarks] -> ShowS
Show)

{- | Above half the CPU the collector is thrashing, the point where Go's memory limit stops helping.
A loaded proxy with no memory pressure spends 20 to 34% of its CPU in the collector.
-}
defaultBrakeMarks :: BrakeMarks
defaultBrakeMarks :: BrakeMarks
defaultBrakeMarks =
    BrakeMarks
        { bmGcHighPermille :: Int
bmGcHighPermille = Int
500
        , bmGcLowPermille :: Int
bmGcLowPermille = Int
350
        , bmKernelHighPermille :: Int
bmKernelHighPermille = Int
900
        , bmOverflowGuardPermille :: Int
bmOverflowGuardPermille = Int
800
        , bmCalmSamples :: Int
bmCalmSamples = Int
10
        , bmGrowPermille :: Int
bmGrowPermille = Int
125
        , bmReturnPermille :: Int
bmReturnPermille = Int
250
        , bmForgetShift :: Int
bmForgetShift = Int
8
        , bmCooldownSamples :: Int
bmCooldownSamples = Int
10
        }

-- | The brake's memory between samples.
data BrakeState = BrakeState
    { BrakeState -> Int
bsBudget :: Int
    , BrakeState -> Maybe Int
bsOutside :: Maybe Int
    -- ^ Live data outside the charges at recent major collections. 'Nothing' before the first.
    , BrakeState -> Int
bsLargestRequest :: Int
    -- ^ The largest charge one request reached lately, fading while no request matches it.
    , BrakeState -> Int
bsCalmSamples :: Int
    , BrakeState -> Int
bsCooldown :: Int
    -- ^ Samples left before pressure may halve the budget again.
    , BrakeState -> BrakeLevel
bsLevel :: BrakeLevel
    }
    deriving stock (BrakeState -> BrakeState -> Bool
(BrakeState -> BrakeState -> Bool)
-> (BrakeState -> BrakeState -> Bool) -> Eq BrakeState
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: BrakeState -> BrakeState -> Bool
== :: BrakeState -> BrakeState -> Bool
$c/= :: BrakeState -> BrakeState -> Bool
/= :: BrakeState -> BrakeState -> Bool
Eq, Int -> BrakeState -> ShowS
[BrakeState] -> ShowS
BrakeState -> String
(Int -> BrakeState -> ShowS)
-> (BrakeState -> String)
-> ([BrakeState] -> ShowS)
-> Show BrakeState
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> BrakeState -> ShowS
showsPrec :: Int -> BrakeState -> ShowS
$cshow :: BrakeState -> String
show :: BrakeState -> String
$cshowList :: [BrakeState] -> ShowS
showList :: [BrakeState] -> ShowS
Show)

-- | Start at the boot budget, with nothing measured yet.
initialBrakeState :: BrakeBounds -> BrakeState
initialBrakeState :: BrakeBounds -> BrakeState
initialBrakeState BrakeBounds
bounds =
    BrakeState{bsBudget :: Int
bsBudget = Int -> Int -> Int
forall a. Ord a => a -> a -> a
min (BrakeBounds -> Maybe Int -> Int -> Int
budgetCeiling BrakeBounds
bounds Maybe Int
forall a. Maybe a
Nothing Int
0) (BrakeBounds -> Int
bbBootBytes BrakeBounds
bounds), bsOutside :: Maybe Int
bsOutside = Maybe Int
forall a. Maybe a
Nothing, bsLargestRequest :: Int
bsLargestRequest = Int
0, bsCalmSamples :: Int
bsCalmSamples = Int
0, bsCooldown :: Int
bsCooldown = Int
0, bsLevel :: BrakeLevel
bsLevel = BrakeLevel
Calm}

-- | One sample's readings. A missing reading leaves its rule out of this step.
data GcSample = GcSample
    { GcSample -> Maybe Int
gsGcSharePermille :: Maybe Int
    -- ^ The collector's share of CPU over the window. 'Nothing' when the process was idle.
    , GcSample -> Maybe Int
gsLiveAfterMajor :: Maybe Int
    -- ^ Live bytes, present only when a major collection finished since the last sample.
    , GcSample -> Int
gsChargedBytes :: Int
    -- ^ What the meter held when the sample was taken.
    , GcSample -> Int
gsLargestCharge :: Int
    -- ^ The largest total one request reached since the previous sample.
    , GcSample -> Maybe Int
gsKernelPermille :: Maybe Int
    -- ^ The cgroup's non-reclaimable use against its limit, when a limit binds.
    }
    deriving stock (GcSample -> GcSample -> Bool
(GcSample -> GcSample -> Bool)
-> (GcSample -> GcSample -> Bool) -> Eq GcSample
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: GcSample -> GcSample -> Bool
== :: GcSample -> GcSample -> Bool
$c/= :: GcSample -> GcSample -> Bool
/= :: GcSample -> GcSample -> Bool
Eq, Int -> GcSample -> ShowS
[GcSample] -> ShowS
GcSample -> String
(Int -> GcSample -> ShowS)
-> (GcSample -> String) -> ([GcSample] -> ShowS) -> Show GcSample
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> GcSample -> ShowS
showsPrec :: Int -> GcSample -> ShowS
$cshow :: GcSample -> String
show :: GcSample -> String
$cshowList :: [GcSample] -> ShowS
showList :: [GcSample] -> ShowS
Show)

{- | Move the budget by one sample: pressure halves it at most once per cooldown, a lower ceiling cuts
it at once, and only a calm stretch grows it. It stays between the floor and the ceiling.
-}
brakeStep :: BrakeMarks -> BrakeBounds -> BrakeState -> GcSample -> BrakeState
brakeStep :: BrakeMarks -> BrakeBounds -> BrakeState -> GcSample -> BrakeState
brakeStep BrakeMarks
marks BrakeBounds
bounds BrakeState
before GcSample
sample =
    BrakeState
        { bsBudget :: Int
bsBudget = Int
grown
        , bsOutside :: Maybe Int
bsOutside = Maybe Int
outside
        , bsLargestRequest :: Int
bsLargestRequest = Int
largest
        , bsCalmSamples :: Int
bsCalmSamples = if BrakeLevel
level BrakeLevel -> BrakeLevel -> Bool
forall a. Eq a => a -> a -> Bool
== BrakeLevel
Calm Bool -> Bool -> Bool
&& Bool -> Bool
not Bool
growNow then Int
calm else Int
0
        , bsCooldown :: Int
bsCooldown = if Bool
halveNow then BrakeMarks -> Int
bmCooldownSamples BrakeMarks
marks Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
1 else Int -> Int -> Int
forall a. Ord a => a -> a -> a
max Int
0 (BrakeState -> Int
bsCooldown BrakeState
before Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
1)
        , bsLevel :: BrakeLevel
bsLevel = BrakeLevel
level
        }
  where
    outside :: Maybe Int
outside = Maybe Int -> (Int -> Maybe Int) -> Maybe Int -> Maybe Int
forall b a. b -> (a -> b) -> Maybe a -> b
maybe (BrakeState -> Maybe Int
bsOutside BrakeState
before) (Int -> Maybe Int
forall a. a -> Maybe a
Just (Int -> Maybe Int) -> (Int -> Int) -> Int -> Maybe Int
forall b c a. (b -> c) -> (a -> b) -> a -> c
. BrakeMarks -> BrakeState -> Int -> Int -> Int
measuredOutside BrakeMarks
marks BrakeState
before (GcSample -> Int
gsChargedBytes GcSample
sample)) (GcSample -> Maybe Int
gsLiveAfterMajor GcSample
sample)
    largest :: Int
largest = Int -> Int -> Int
forall a. Ord a => a -> a -> a
max (GcSample -> Int
gsLargestCharge GcSample
sample) (BrakeState -> Int
bsLargestRequest BrakeState
before Int -> Int -> Int
forall a. Num a => a -> a -> a
- BrakeState -> Int
bsLargestRequest BrakeState
before Int -> Int -> Int
forall a. Bits a => a -> Int -> a
`shiftR` BrakeMarks -> Int
bmForgetShift BrakeMarks
marks)
    ceilingNow :: Int
ceilingNow = BrakeBounds -> Maybe Int -> Int -> Int
budgetCeiling BrakeBounds
bounds Maybe Int
outside Int
largest
    level :: BrakeLevel
level
        | BrakeMarks -> BrakeBounds -> GcSample -> Bool
pressed BrakeMarks
marks BrakeBounds
bounds GcSample
sample = BrakeLevel
Braking
        | (Int -> Bool) -> Maybe Int -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
all (Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
< BrakeMarks -> Int
bmGcLowPermille BrakeMarks
marks) (GcSample -> Maybe Int
gsGcSharePermille GcSample
sample) = BrakeLevel
Calm
        | Bool
otherwise = BrakeLevel
Holding
    calm :: Int
calm = BrakeState -> Int
bsCalmSamples BrakeState
before Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
1
    growNow :: Bool
growNow = BrakeLevel
level BrakeLevel -> BrakeLevel -> Bool
forall a. Eq a => a -> a -> Bool
== BrakeLevel
Calm Bool -> Bool -> Bool
&& Int
calm Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
>= BrakeMarks -> Int
bmCalmSamples BrakeMarks
marks
    kept :: Int
kept = Int -> Int -> Int
forall a. Ord a => a -> a -> a
min Int
ceilingNow (BrakeState -> Int
bsBudget BrakeState
before)
    halveNow :: Bool
halveNow = BrakeLevel
level BrakeLevel -> BrakeLevel -> Bool
forall a. Eq a => a -> a -> Bool
== BrakeLevel
Braking Bool -> Bool -> Bool
&& BrakeState -> Int
bsCooldown BrakeState
before Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= Int
0
    held :: Int
held
        | Bool
halveNow = Int -> Int -> Int
forall a. Ord a => a -> a -> a
max (BrakeBounds -> Int
bbFloorBytes BrakeBounds
bounds) (Int
kept Int -> Int -> Int
forall a. Integral a => a -> a -> a
`div` Int
2)
        | Bool
otherwise = Int
kept
    grown :: Int
grown
        | Bool
growNow = Int -> Int -> Int
forall a. Ord a => a -> a -> a
min Int
ceilingNow (Int
held Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int -> Int -> Int
forall a. Ord a => a -> a -> a
max (BrakeBounds -> Int
bbGrowFloorBytes BrakeBounds
bounds) (Int
held Int -> Int -> Int
forall a. Num a => a -> a -> a
* BrakeMarks -> Int
bmGrowPermille BrakeMarks
marks Int -> Int -> Int
forall a. Integral a => a -> a -> a
`div` Int
1000))
        | Bool
otherwise = Int
held

{- The most the budget may reach: the live ceiling, or the overflow point less the largest request
when that is lower, less the live data outside the charges. Without a heap ceiling, the boot budget. -}
budgetCeiling :: BrakeBounds -> Maybe Int -> Int -> Int
budgetCeiling :: BrakeBounds -> Maybe Int -> Int -> Int
budgetCeiling BrakeBounds
bounds Maybe Int
outside Int
largest = Int -> Int -> Int
forall a. Ord a => a -> a -> a
max (BrakeBounds -> Int
bbFloorBytes BrakeBounds
bounds) (Int -> Int) -> Int -> Int
forall a b. (a -> b) -> a -> b
$ case BrakeBounds -> Maybe Int
bbLiveCeilingBytes BrakeBounds
bounds of
    Maybe Int
Nothing -> BrakeBounds -> Int
bbBootBytes BrakeBounds
bounds
    Just Int
live -> Int -> (Int -> Int) -> Maybe Int -> Int
forall b a. b -> (a -> b) -> Maybe a -> b
maybe Int
live (Int -> Int -> Int
forall a. Ord a => a -> a -> a
min Int
live (Int -> Int) -> (Int -> Int) -> Int -> Int
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Int -> Int -> Int
forall a. Num a => a -> a -> a
subtract Int
largest) (BrakeBounds -> Maybe Int
bbOverflowLiveBytes BrakeBounds
bounds) Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int -> Int -> Int
forall a. Ord a => a -> a -> a
max (BrakeBounds -> Int
bbFixedLiveBytes BrakeBounds
bounds) (Int -> Maybe Int -> Int
forall a. a -> Maybe a -> a
fromMaybe (BrakeBounds -> Int
bbExplainedBytes BrakeBounds
bounds) Maybe Int
outside)

-- A rise in the remainder counts at once. A fall is given back a fraction at a time.
measuredOutside :: BrakeMarks -> BrakeState -> Int -> Int -> Int
measuredOutside :: BrakeMarks -> BrakeState -> Int -> Int -> Int
measuredOutside BrakeMarks
marks BrakeState
before Int
charged Int
live = case BrakeState -> Maybe Int
bsOutside BrakeState
before of
    Just Int
previous | Int
measured Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
< Int
previous -> Int
previous Int -> Int -> Int
forall a. Num a => a -> a -> a
- (Int
previous Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
measured) Int -> Int -> Int
forall a. Num a => a -> a -> a
* BrakeMarks -> Int
bmReturnPermille BrakeMarks
marks Int -> Int -> Int
forall a. Integral a => a -> a -> a
`div` Int
1000
    Maybe Int
_ -> Int
measured
  where
    measured :: Int
measured = Int
live Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
charged

pressed :: BrakeMarks -> BrakeBounds -> GcSample -> Bool
pressed :: BrakeMarks -> BrakeBounds -> GcSample -> Bool
pressed BrakeMarks
marks BrakeBounds
bounds GcSample
sample =
    (Int -> Bool) -> Maybe Int -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any (Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
> BrakeMarks -> Int
bmGcHighPermille BrakeMarks
marks) (GcSample -> Maybe Int
gsGcSharePermille GcSample
sample)
        Bool -> Bool -> Bool
|| (Int -> Bool) -> Maybe Int -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any (Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
> BrakeMarks -> Int
bmKernelHighPermille BrakeMarks
marks) (GcSample -> Maybe Int
gsKernelPermille GcSample
sample)
        Bool -> Bool -> Bool
|| Bool
nearOverflow
  where
    nearOverflow :: Bool
nearOverflow = case (GcSample -> Maybe Int
gsLiveAfterMajor GcSample
sample, BrakeBounds -> Maybe Int
bbOverflowLiveBytes BrakeBounds
bounds) of
        (Just Int
live, Just Int
overflow) -> Int
live Int -> Int -> Int
forall a. Num a => a -> a -> a
* Int
1000 Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
> Int
overflow Int -> Int -> Int
forall a. Num a => a -> a -> a
* BrakeMarks -> Int
bmOverflowGuardPermille BrakeMarks
marks
        (Maybe Int, Maybe Int)
_ -> Bool
False

-- | The collector's cumulative counters at one sample.
data CollectorReading = CollectorReading
    { CollectorReading -> Int64
crCpuNs :: Int64
    -- ^ Process CPU time, collector included.
    , CollectorReading -> Int64
crGcCpuNs :: Int64
    -- ^ Collector CPU time.
    , CollectorReading -> Word32
crMajorCollections :: Word32
    , CollectorReading -> Int
crLiveBytes :: Int
    -- ^ Live bytes after the latest collection, exact after a major one.
    }
    deriving stock (CollectorReading -> CollectorReading -> Bool
(CollectorReading -> CollectorReading -> Bool)
-> (CollectorReading -> CollectorReading -> Bool)
-> Eq CollectorReading
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: CollectorReading -> CollectorReading -> Bool
== :: CollectorReading -> CollectorReading -> Bool
$c/= :: CollectorReading -> CollectorReading -> Bool
/= :: CollectorReading -> CollectorReading -> Bool
Eq, Int -> CollectorReading -> ShowS
[CollectorReading] -> ShowS
CollectorReading -> String
(Int -> CollectorReading -> ShowS)
-> (CollectorReading -> String)
-> ([CollectorReading] -> ShowS)
-> Show CollectorReading
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> CollectorReading -> ShowS
showsPrec :: Int -> CollectorReading -> ShowS
$cshow :: CollectorReading -> String
show :: CollectorReading -> String
$cshowList :: [CollectorReading] -> ShowS
showList :: [CollectorReading] -> ShowS
Show)

-- | The collector's share of the CPU between two readings, or 'Nothing' when no CPU passed.
gcSharePermille :: CollectorReading -> CollectorReading -> Maybe Int
gcSharePermille :: CollectorReading -> CollectorReading -> Maybe Int
gcSharePermille CollectorReading
older CollectorReading
newer
    | Int64
cpu Int64 -> Int64 -> Bool
forall a. Ord a => a -> a -> Bool
<= Int64
0 = Maybe Int
forall a. Maybe a
Nothing
    | Bool
otherwise = Int -> Maybe Int
forall a. a -> Maybe a
Just (Int64 -> Int
forall a b. (Integral a, Num b) => a -> b
fromIntegral (Int64 -> Int64 -> Int64
forall a. Ord a => a -> a -> a
min Int64
1000 (Int64 -> Int64 -> Int64
forall a. Ord a => a -> a -> a
max Int64
0 (Int64
gc Int64 -> Int64 -> Int64
forall a. Num a => a -> a -> a
* Int64
1000 Int64 -> Int64 -> Int64
forall a. Integral a => a -> a -> a
`div` Int64
cpu))))
  where
    cpu :: Int64
cpu = CollectorReading -> Int64
crCpuNs CollectorReading
newer Int64 -> Int64 -> Int64
forall a. Num a => a -> a -> a
- CollectorReading -> Int64
crCpuNs CollectorReading
older
    gc :: Int64
gc = CollectorReading -> Int64
crGcCpuNs CollectorReading
newer Int64 -> Int64 -> Int64
forall a. Num a => a -> a -> a
- CollectorReading -> Int64
crGcCpuNs CollectorReading
older

-- | The recent readings the GC share spans, newest first, and the capacity of that span.
data SampleWindow = SampleWindow Int [CollectorReading]

-- | An empty window that spans the given number of sampling periods.
newSampleWindow :: Int -> SampleWindow
newSampleWindow :: Int -> SampleWindow
newSampleWindow Int
periods = Int -> [CollectorReading] -> SampleWindow
SampleWindow (Int -> Int -> Int
forall a. Ord a => a -> a -> a
max Int
1 Int
periods) []

{- | Fold one reading into the window and form the brake's sample. Live data counts only after a major
collection since an earlier reading, and a missing reading (no @-T@) leaves the collector's rules out.
-}
windowSample :: SampleWindow -> Maybe CollectorReading -> Int -> Int -> Maybe Int -> (GcSample, SampleWindow)
windowSample :: SampleWindow
-> Maybe CollectorReading
-> Int
-> Int
-> Maybe Int
-> (GcSample, SampleWindow)
windowSample (SampleWindow Int
periods [CollectorReading]
readings) Maybe CollectorReading
reading Int
charged Int
largest Maybe Int
kernel =
    ( GcSample
        { gsGcSharePermille :: Maybe Int
gsGcSharePermille = do
            newest <- Maybe CollectorReading
reading
            oldest <- listToMaybe (reverse spanned)
            gcSharePermille oldest newest
        , gsLiveAfterMajor :: Maybe Int
gsLiveAfterMajor = do
            newest <- Maybe CollectorReading
reading
            previous <- listToMaybe readings
            guard (crMajorCollections previous /= crMajorCollections newest)
            pure (crLiveBytes newest)
        , gsChargedBytes :: Int
gsChargedBytes = Int
charged
        , gsLargestCharge :: Int
gsLargestCharge = Int
largest
        , gsKernelPermille :: Maybe Int
gsKernelPermille = Maybe Int
kernel
        }
    , Int -> [CollectorReading] -> SampleWindow
SampleWindow Int
periods [CollectorReading]
spanned
    )
  where
    spanned :: [CollectorReading]
spanned = Int -> [CollectorReading] -> [CollectorReading]
forall a. Int -> [a] -> [a]
take (Int
periods Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
1) (Maybe CollectorReading -> [CollectorReading]
forall a. Maybe a -> [a]
maybeToList Maybe CollectorReading
reading [CollectorReading] -> [CollectorReading] -> [CollectorReading]
forall a. Semigroup a => a -> a -> a
<> [CollectorReading]
readings)