packages feed

wasmedge-hs-0.17.1.0: examples/Stress/ConcurrentPool.hs

{-# LANGUAGE OverloadedStrings #-}

{- |
Module      : Stress.ConcurrentPool
Description : High-concurrency VM worker pool and throughput stress testing
Copyright   : (c) 2026 Tushar Adhatrao
License     : MIT
Stability   : experimental

Tests high-concurrency execution across multiple GHC capabilities and OS threads.
Implements a production-grade 'VMPool' with bounded queuing ('TBQueue'), and compares
reusable pooled VMs against ephemeral (per-request) VMs under heavy load (1,000+ requests).
Computes throughput (ops/sec) and p50/p95/p99 latency distributions.
-}
module Stress.ConcurrentPool
  ( runConcurrentPoolSuite
  , ConcurrentMetrics (..)
  , VMPool (..)
  , createVMPool
  , withPooledVM
  , destroyVMPool
  ) where

import Control.Concurrent.Async (forConcurrently)
import Control.Concurrent.STM
  ( TBQueue
  , atomically
  , newTBQueueIO
  , readTBQueue
  , tryReadTBQueue
  , writeTBQueue
  )
import Control.Exception (bracket, throwIO)
import Control.Monad (replicateM)
import Data.List (sort)
import System.Clock (Clock (Monotonic), diffTimeSpec, getTime, toNanoSecs)
import Text.Printf (printf)

import Stress.WasmBytecode (addWasm, fibWasm)
import WasmEdge.Types (WasmVal (..))
import WasmEdge.VM

-- | Production VM Pool backed by a thread-safe STM bounded queue.
newtype VMPool = VMPool (TBQueue VM)

-- | Create a VM pool with @size@ pre-allocated and initialized VMs.
createVMPool :: Int -> IO VMPool
createVMPool size = do
  q <- newTBQueueIO (fromIntegral size)
  vms <- replicateM size $ do
    vm <- createVM Nothing Nothing
    -- Pre-load and instantiate fibWasm
    loadRes <- loadWasmBytes vm fibWasm
    case loadRes of
      Left err -> throwIO (userError $ "Failed to load fibWasm in pool: " ++ show err)
      Right () -> pure ()
    valRes <- validate vm
    case valRes of
      Left err -> throwIO (userError $ "Failed to validate fibWasm in pool: " ++ show err)
      Right () -> pure ()
    instRes <- instantiate vm
    case instRes of
      Left err -> throwIO (userError $ "Failed to instantiate fibWasm in pool: " ++ show err)
      Right () -> pure ()
    pure vm
  atomically $ mapM_ (writeTBQueue q) vms
  pure (VMPool q)

-- | Borrow a VM from the pool, execute an action, and safely return it.
withPooledVM :: VMPool -> (VM -> IO a) -> IO a
withPooledVM (VMPool q) =
  bracket
    (atomically $ readTBQueue q)
    (atomically . writeTBQueue q)

-- | Destroy all VMs in the pool.
destroyVMPool :: VMPool -> IO ()
destroyVMPool (VMPool q) = do
  mVM <- atomically $ tryReadTBQueue q
  case mVM of
    Nothing -> pure ()
    Just vm -> do
      deleteVM vm
      destroyVMPool (VMPool q)

-- | Benchmark metrics for a concurrency run.
data ConcurrentMetrics = ConcurrentMetrics
  { cmTotalRequests :: !Int
  , cmConcurrency :: !Int
  , cmDurationSec :: !Double
  , cmThroughputRPS :: !Double
  , cmP50LatencyMs :: !Double
  , cmP95LatencyMs :: !Double
  , cmP99LatencyMs :: !Double
  , cmMaxLatencyMs :: !Double
  }
  deriving stock (Show)

-- | Helper to compute percentile from a sorted list of latencies in milliseconds.
percentile :: Double -> [Double] -> Double
percentile _ [] = 0.0
percentile p xs =
  let idx = floor (p * fromIntegral (length xs - 1))
   in xs !! idx

-- | Run the high-concurrency stress test suite.
runConcurrentPoolSuite :: IO (ConcurrentMetrics, ConcurrentMetrics)
runConcurrentPoolSuite = do
  putStrLn "============================================================"
  putStrLn "  STRESS TEST: High-Concurrency & VM Pool Throughput"
  putStrLn "============================================================"

  let totalTasks = 1000 :: Int
      poolSize = 16 :: Int
      fibArg = 20 :: Int -- fib(20) = 6765
      expectedFib = 6765 :: Int

  -- -------------------------------------------------------------
  -- Test A: Pooled VMs (amortized instantiation cost)
  -- -------------------------------------------------------------
  putStrLn $
    printf
      "--> [1/2] Pooled VMs: running %d concurrent requests across %d pooled VMs..."
      totalTasks
      poolSize
  pool <- createVMPool poolSize

  startA <- getTime Monotonic
  latenciesA <- forConcurrently [1 .. totalTasks] $ \_ -> do
    t0 <- getTime Monotonic
    res <- withPooledVM pool $ \vm ->
      execute vm "fib" [WasmI32 (fromIntegral fibArg)]
    t1 <- getTime Monotonic
    case res of
      Left err -> throwIO (userError $ "Pooled execution failed: " ++ show err)
      Right [WasmI32 v]
        | fromIntegral v == expectedFib -> pure ()
        | otherwise -> throwIO (userError $ "Incorrect fib result: " ++ show v)
      Right other -> throwIO (userError $ "Unexpected return: " ++ show other)
    let nanos = toNanoSecs (diffTimeSpec t1 t0)
    pure (fromIntegral nanos / 1e6 :: Double)
  endA <- getTime Monotonic

  let durNanosA = toNanoSecs (diffTimeSpec endA startA)
      durSecA = fromIntegral durNanosA / 1e9 :: Double
      rpsA = fromIntegral totalTasks / durSecA
      sortedLatA = sort latenciesA
      p50A = percentile 0.50 sortedLatA
      p95A = percentile 0.95 sortedLatA
      p99A = percentile 0.99 sortedLatA
      maxLatA = if null sortedLatA then 0.0 else last sortedLatA

  printf "    Pooled Results: %.2f req/sec (%.3f s total)\n" rpsA durSecA
  printf
    "    Latencies: p50 = %.3f ms | p95 = %.3f ms | p99 = %.3f ms | max = %.3f ms\n\n"
    p50A
    p95A
    p99A
    maxLatA
  destroyVMPool pool

  -- -------------------------------------------------------------
  -- Test B: Ephemeral VMs (Fresh VM created & destroyed per request)
  -- -------------------------------------------------------------
  putStrLn $
    printf
      "--> [2/2] Ephemeral VMs: running %d requests with fresh VM creation & teardown..."
      (totalTasks `div` 2)
  let ephemeralCount = totalTasks `div` 2

  startB <- getTime Monotonic
  latenciesB <- forConcurrently [1 .. ephemeralCount] $ \_ -> do
    t0 <- getTime Monotonic
    res <- withVM Nothing Nothing $ \vm ->
      runWasmBytes vm addWasm "add" [WasmI32 20, WasmI32 22]
    t1 <- getTime Monotonic
    case res of
      Left err -> throwIO (userError $ "Ephemeral execution failed: " ++ show err)
      Right [WasmI32 42] -> pure ()
      Right other -> throwIO (userError $ "Unexpected return: " ++ show other)
    let nanos = toNanoSecs (diffTimeSpec t1 t0)
    pure (fromIntegral nanos / 1e6 :: Double)
  endB <- getTime Monotonic

  let durNanosB = toNanoSecs (diffTimeSpec endB startB)
      durSecB = fromIntegral durNanosB / 1e9 :: Double
      rpsB = fromIntegral ephemeralCount / durSecB
      sortedLatB = sort latenciesB
      p50B = percentile 0.50 sortedLatB
      p95B = percentile 0.95 sortedLatB
      p99B = percentile 0.99 sortedLatB
      maxLatB = if null sortedLatB then 0.0 else last sortedLatB

  printf "    Ephemeral Results: %.2f req/sec (%.3f s total)\n" rpsB durSecB
  printf
    "    Latencies: p50 = %.3f ms | p95 = %.3f ms | p99 = %.3f ms | max = %.3f ms\n\n"
    p50B
    p95B
    p99B
    maxLatB

  let metricsA = ConcurrentMetrics totalTasks poolSize durSecA rpsA p50A p95A p99A maxLatA
      metricsB = ConcurrentMetrics ephemeralCount 1 durSecB rpsB p50B p95B p99B maxLatB
  pure (metricsA, metricsB)