nano-ui-sdl-0.1.0.0: benchmark/SdlBench.hs
{-# LANGUAGE OverloadedStrings #-}
module Main (main) where
import Control.Monad (replicateM_, void, when)
import GHC.IO.Encoding (setLocaleEncoding, utf8)
import GHC.Conc (getAllocationCounter)
import NanoUI
import NanoUI.Context (Context (..))
import NanoUI.Testing (newPixelContext, runFrame)
import NanoUI.Backend.Sdl (SdlEnv (..), sdlDrawFrame, syncDisplay, withSdlBench)
import System.Exit (exitFailure)
import System.IO (hSetEncoding, stderr, stdout)
import System.Mem (performGC)
import Test.Tasty.Bench
import Text.Printf (printf)
#if defined(mingw32_HOST_OS)
import System.Win32 (setConsoleCP, setConsoleOutputCP)
#endif
benchWindowSize :: Size
benchWindowSize = Size 800 600
benchInput :: Input
benchInput =
emptyInput
{ inputWindowSize = benchWindowSize
, inputMousePos = V2 400 300
, inputMouseDown = True
}
smallUi, mediumUi, largeUi :: NanoUI ()
smallUi =
columnWith (gap 8) $ do
void (button "OK")
label "Hello"
mediumUi =
columnWith
(grow . gap 8)
( do
replicateM_ 12 $
gridWith 8 (gap 8) $
replicateM_ 8 (void (button "OK"))
label "nano-ui SDL bench"
)
largeUi =
columnWith
(grow . gap 6)
( do
replicateM_ 20 $
gridWith 10 (gap 6) $
replicateM_ 10 (void (button "Item"))
replicateM_ 8 (label "Status line with a bit of text")
)
configureBenchIO :: IO ()
configureBenchIO = do
setLocaleEncoding utf8
hSetEncoding stdout utf8
hSetEncoding stderr utf8
#if defined(mingw32_HOST_OS)
void $ setConsoleCP 65001
void $ setConsoleOutputCP 65001
#endif
-- | Warm ASCII glyph lookups must not allocate: the atlas UV/bearing record
-- is cached and shared per font, so a steady-state 'fmGlyph' hit is array
-- reads and a pointer return. This gate catches reintroducing a
-- per-character 'GlyphQuad' / 'Just' allocation on the text hot path.
--
-- The probe walks a shared 'Char' list rather than 'T.index', because
-- 'T.index' allocates in this context and would mask the lookup cost.
glyphLookupAlloc :: Context -> IO Integer
glyphLookupAlloc ctx = do
(fm, _) <- ctxResolveFont ctx 16 WeightNormal FontStyleNormal FontRegular
let sample = "The quick brown fox jumps over the lazy dog 0123456789!?.,;:"
chars = sample
len = length chars
lookups = 20000 :: Int
step :: Int -> Float -> IO Float
step !n !acc =
if n <= 0
then pure acc
else
-- Force selection before the indirect glyph call; otherwise the
-- benchmark allocates a character-selection thunk per lookup.
let !c = chars !! (n `mod` len)
in drawGlyph fm c >>= \case
Just gq -> step (n - 1) (acc + gqW gq)
Nothing -> step (n - 1) acc
-- Warm every character so every lookup shares a cached 'Maybe'.
mapM_ (drawGlyph fm) chars
performGC
-- The thread allocation counter is current even if this probe never fills
-- the nursery. RTSStats.allocated_bytes only catches up at a GC.
before <- getAllocationCounter
_ <- step lookups 0
after <- getAllocationCounter
pure (fromIntegral before - fromIntegral after)
-- | Bytes per warm lookup tolerated before the gate trips. The cached path
-- should be zero; a reintroduced per-hit record would cost tens of bytes.
glyphLookupAllocBudget :: Double
glyphLookupAllocBudget = 1.0
glyphLookupGate :: Context -> IO ()
glyphLookupGate ctx = do
bytes <- glyphLookupAlloc ctx
let lookups = 20000 :: Int
perLookup = fromIntegral bytes / fromIntegral lookups :: Double
printf "glyph-lookup: %.3f B/lookup (budget %.1f)\n" perLookup glyphLookupAllocBudget
when (perLookup > glyphLookupAllocBudget) $ do
putStrLn "FAIL: warm glyph lookups allocate; expected the cached quad to be shared"
exitFailure
main :: IO ()
main = do
configureBenchIO
ctx0 <- newPixelContext
withSdlBench ctx0 $ \ctx sdlEnv -> do
(ctx', inp) <- syncDisplay ctx sdlEnv benchInput
warmup ctx' sdlEnv inp
glyphLookupGate ctx'
configureBenchIO
defaultMain
[ bgroup
"ui/runFrame"
[ benchRunFrame ctx' inp smallUi "small"
, benchRunFrame ctx' inp mediumUi "medium"
, benchRunFrame ctx' inp largeUi "large"
]
, bgroup
"sdl3/draw"
[ benchDraw ctx' sdlEnv inp smallUi "small"
, benchDraw ctx' sdlEnv inp mediumUi "medium"
, benchDraw ctx' sdlEnv inp largeUi "large"
]
]
warmup :: Context -> SdlEnv -> Input -> IO ()
warmup ctx sdlEnv inp = do
void (runFrame ctx inp mediumUi)
void (sdlDrawFrame ctx mediumUi sdlEnv inp False)
void (runFrame ctx inp mediumUi)
void (sdlDrawFrame ctx mediumUi sdlEnv inp False)
benchRunFrame :: Context -> Input -> NanoUI () -> String -> Benchmark
benchRunFrame ctx inp ui name =
bench name $ whnfIO (void . runFrame ctx inp $ ui)
benchDraw :: Context -> SdlEnv -> Input -> NanoUI () -> String -> Benchmark
benchDraw ctx sdlEnv inp ui name =
bench name $ whnfIO (void . sdlDrawFrame ctx ui sdlEnv inp $ False)