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

{- | The transient memory budget: the live data metadata requests may hold at once, carved from what
the collector can hold without strain.

A busy copying collector keeps about four times its live data, plus the nursery, so the live target
is a quarter of the heap ceiling less the nursery. The idle process and the retained tenants come off
the target first, and what remains is the budget the meter starts from. The sampler may raise it
until charges and the live data it measures outside them reach a third of that heap, two thirds of
the point where copying overflows.
-}
module Ecluse.Composition.MemoryPlan.Transient (
    transientBudget,
    renderTransientBudget,
    brakeBounds,
    liveTargetBytes,
    liveCacheShareBytes,

    -- * The starting constants
    idleLiveFloorBytes,
    transientFloorBytes,
    noCeilingTransientBytes,
    liveCacheSharePercent,
    meterStepBytes,
) where

import Ecluse.Composition.MemoryPlan.Types (TransientBudget (..))
import Ecluse.Core.Server.Admission.Types (BrakeBounds (..))

{- | Resolve the budget from the heap ceiling, the capability count, the allocation area and the other
tenants' bytes. With no ceiling the budget is a large constant the brake alone moves.
-}
transientBudget :: Maybe Int -> Int -> Int -> Int -> TransientBudget
transientBudget :: Maybe Int -> Int -> Int -> Int -> TransientBudget
transientBudget Maybe Int
heapCeiling Int
capabilities Int
allocArea Int
tenants = case Maybe Int
heapCeiling of
    Maybe Int
Nothing ->
        TransientBudget
            { tbLiveTargetBytes :: Maybe Int
tbLiveTargetBytes = Maybe Int
forall a. Maybe a
Nothing
            , tbExplainedBytes :: Int
tbExplainedBytes = Int
explained
            , tbBootBytes :: Int
tbBootBytes = Int
noCeilingTransientBytes
            , tbLiveCeilingBytes :: Maybe Int
tbLiveCeilingBytes = Maybe Int
forall a. Maybe a
Nothing
            , tbFloorBytes :: Int
tbFloorBytes = Int
transientFloorBytes
            , tbOverflowLiveBytes :: Maybe Int
tbOverflowLiveBytes = Maybe Int
forall a. Maybe a
Nothing
            }
    Just Int
ceiling' ->
        let copyable :: Int
copyable = Int -> Int -> Int -> Int
copyableHeap Int
ceiling' Int
capabilities Int
allocArea
            boot :: Int
boot = Int -> Int -> Int
forall a. Ord a => a -> a -> a
max Int
transientFloorBytes (Int
copyable Int -> Int -> Int
forall a. Integral a => a -> a -> a
`div` Int
4 Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
explained)
         in TransientBudget
                { tbLiveTargetBytes :: Maybe Int
tbLiveTargetBytes = Int -> Maybe Int
forall a. a -> Maybe a
Just (Int
copyable Int -> Int -> Int
forall a. Integral a => a -> a -> a
`div` Int
4)
                , tbExplainedBytes :: Int
tbExplainedBytes = Int
explained
                , tbBootBytes :: Int
tbBootBytes = Int
boot
                , tbLiveCeilingBytes :: Maybe Int
tbLiveCeilingBytes = Int -> Maybe Int
forall a. a -> Maybe a
Just (Int
copyable Int -> Int -> Int
forall a. Integral a => a -> a -> a
`div` Int
3)
                , tbFloorBytes :: Int
tbFloorBytes = Int
transientFloorBytes
                , tbOverflowLiveBytes :: Maybe Int
tbOverflowLiveBytes = Int -> Maybe Int
forall a. a -> Maybe a
Just (Int
copyable Int -> Int -> Int
forall a. Integral a => a -> a -> a
`div` Int
2)
                }
  where
    explained :: Int
explained = Int
idleLiveFloorBytes Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int -> Int -> Int
forall a. Ord a => a -> a -> a
max Int
0 Int
tenants

-- | The boot line for the budget, with the arithmetic an operator needs to check it.
renderTransientBudget :: TransientBudget -> Text
renderTransientBudget :: TransientBudget -> Text
renderTransientBudget TransientBudget
budget = case TransientBudget -> Maybe Int
tbLiveTargetBytes TransientBudget
budget of
    Maybe Int
Nothing ->
        Text
"memory plan: transient budget " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Int -> Text
forall b a. (Show a, IsString b) => a -> b
show (TransientBudget -> Int
tbBootBytes TransientBudget
budget) Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" (built-in default, no heap-ceiling datapoint)"
    Just Int
target ->
        Text
"memory plan: transient budget "
            Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Int -> Text
forall b a. (Show a, IsString b) => a -> b
show (TransientBudget -> Int
tbBootBytes TransientBudget
budget)
            Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" (live target "
            Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Int -> Text
forall b a. (Show a, IsString b) => a -> b
show Int
target
            Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" less "
            Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Int -> Text
forall b a. (Show a, IsString b) => a -> b
show (TransientBudget -> Int
tbExplainedBytes TransientBudget
budget)
            Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
" for the idle process and the other tenants, floor "
            Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Int -> Text
forall b a. (Show a, IsString b) => a -> b
show (TransientBudget -> Int
tbFloorBytes TransientBudget
budget)
            Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text -> (Int -> Text) -> Maybe Int -> Text
forall b a. b -> (a -> b) -> Maybe a -> b
maybe Text
"" (\Int
ceiling' -> Text
", live ceiling " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Int -> Text
forall b a. (Show a, IsString b) => a -> b
show Int
ceiling') (TransientBudget -> Maybe Int
tbLiveCeilingBytes TransientBudget
budget)
            Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
")"

-- | The fixed bounds the sampler's brake steers the budget within.
brakeBounds :: TransientBudget -> BrakeBounds
brakeBounds :: TransientBudget -> BrakeBounds
brakeBounds TransientBudget
budget =
    BrakeBounds
        { bbBootBytes :: Int
bbBootBytes = TransientBudget -> Int
tbBootBytes TransientBudget
budget
        , bbFloorBytes :: Int
bbFloorBytes = TransientBudget -> Int
tbFloorBytes TransientBudget
budget
        , bbLiveCeilingBytes :: Maybe Int
bbLiveCeilingBytes = TransientBudget -> Maybe Int
tbLiveCeilingBytes TransientBudget
budget
        , bbFixedLiveBytes :: Int
bbFixedLiveBytes = Int
idleLiveFloorBytes
        , bbExplainedBytes :: Int
bbExplainedBytes = TransientBudget -> Int
tbExplainedBytes TransientBudget
budget
        , bbOverflowLiveBytes :: Maybe Int
bbOverflowLiveBytes = TransientBudget -> Maybe Int
tbOverflowLiveBytes TransientBudget
budget
        , bbGrowFloorBytes :: Int
bbGrowFloorBytes = Int
meterStepBytes
        }

-- | The live target L for a heap ceiling: a quarter of the heap the nursery leaves.
liveTargetBytes :: Int -> Int -> Int -> Int
liveTargetBytes :: Int -> Int -> Int -> Int
liveTargetBytes Int
ceiling' Int
capabilities Int
allocArea = Int -> Int -> Int -> Int
copyableHeap Int
ceiling' Int
capabilities Int
allocArea Int -> Int -> Int
forall a. Integral a => a -> a -> a
`div` Int
4

-- | The retained cache's share of the live target, so the cache alone cannot push the heap past it.
liveCacheShareBytes :: Int -> Int -> Int -> Int
liveCacheShareBytes :: Int -> Int -> Int -> Int
liveCacheShareBytes Int
ceiling' Int
capabilities Int
allocArea =
    Int -> Int -> Int -> Int
liveTargetBytes Int
ceiling' Int
capabilities Int
allocArea Int -> Int -> Int
forall a. Num a => a -> a -> a
* Int
liveCacheSharePercent Int -> Int -> Int
forall a. Integral a => a -> a -> a
`div` Int
100

-- The nursery sits inside the heap ceiling, so it comes off before the collector's factor applies.
copyableHeap :: Int -> Int -> Int -> Int
copyableHeap :: Int -> Int -> Int -> Int
copyableHeap Int
ceiling' Int
capabilities Int
allocArea = Int -> Int -> Int
forall a. Ord a => a -> a -> a
max Int
0 (Int
ceiling' Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int -> Int -> Int
forall a. Ord a => a -> a -> a
max Int
1 Int
capabilities Int -> Int -> Int
forall a. Num a => a -> a -> a
* Int -> Int -> Int
forall a. Ord a => a -> a -> a
max Int
0 Int
allocArea)

{- | The live data of a booted, idle process. The load test measures about 3 MiB, and the sampler's
live correction takes any excess off the budget at run time.
-}
idleLiveFloorBytes :: Int
idleLiveFloorBytes :: Int
idleLiveFloorBytes = Int
8 Int -> Int -> Int
forall a. Num a => a -> a -> a
* Int
1024 Int -> Int -> Int
forall a. Num a => a -> a -> a
* Int
1024

-- | The smallest budget, so a small pod still moves more than one request's entry step.
transientFloorBytes :: Int
transientFloorBytes :: Int
transientFloorBytes = Int
16 Int -> Int -> Int
forall a. Num a => a -> a -> a
* Int
1024 Int -> Int -> Int
forall a. Num a => a -> a -> a
* Int
1024

-- | The budget with no heap ceiling. Nothing bounds the heap, so only the GC brake moves it.
noCeilingTransientBytes :: Int
noCeilingTransientBytes :: Int
noCeilingTransientBytes = Int
1024 Int -> Int -> Int
forall a. Num a => a -> a -> a
* Int
1024 Int -> Int -> Int
forall a. Num a => a -> a -> a
* Int
1024

-- | The share of the live target the retained cache may hold.
liveCacheSharePercent :: Int
liveCacheSharePercent :: Int
liveCacheSharePercent = Int
30

-- | The entry step and the unit a read pays in: one STM transaction per step.
meterStepBytes :: Int
meterStepBytes :: Int
meterStepBytes = Int
1024 Int -> Int -> Int
forall a. Num a => a -> a -> a
* Int
1024