packages feed

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

{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE OverloadedStrings #-}

{- |
Module      : Stress.HostPipeline
Description : High-throughput bidirectional host function pipeline stress test
Copyright   : (c) 2026 Tushar Adhatrao
License     : MIT
Stability   : experimental

Tests high-frequency FFI boundary crossings between WebAssembly and Haskell,
registering typed host functions into module @"env"@, invoking them thousands
of times in a tight guest loop, measuring host calls/sec throughput, and exercising
'CallingFrame' memory inspection and mutation.
-}
module Stress.HostPipeline
  ( runHostPipelineSuite
  , HostPipelineMetrics (..)
  ) where

import Control.Exception (throwIO)
import Data.ByteString qualified as BS
import Data.Int (Int32)
import Data.Word (Word32)
import System.Clock (Clock (Monotonic), diffTimeSpec, getTime, toNanoSecs)
import Text.Printf (printf)

import Stress.WasmBytecode (hostImportWasm)
import WasmEdge.Caller (readCallerMemory, writeCallerMemory)
import WasmEdge.Function (createFunction, createFunctionType)
import WasmEdge.Module (buildHostModule, builderAddHostFunc)
import WasmEdge.Types (ValType (..), WasmVal (..))
import WasmEdge.VM

-- | Metrics from the host function pipeline stress test
data HostPipelineMetrics = HostPipelineMetrics
  { hpmTotalCalls :: !Int
  , hpmDurationSec :: !Double
  , hpmCallsPerSec :: !Double
  , hpmCallerMemPass :: !Bool
  }
  deriving stock (Show)

-- | Run the Host Pipeline stress test suite.
runHostPipelineSuite :: IO HostPipelineMetrics
runHostPipelineSuite = do
  putStrLn "============================================================"
  putStrLn "  STRESS TEST: Host Function Pipeline & CallingFrame Memory"
  putStrLn "============================================================"

  let iterations = 50000 :: Int
      -- Each iteration calls host_callback then host_add = 2 host calls per loop
      totalCalls = iterations * 2

  putStrLn $
    printf "--> [1/2] Registering host module 'env' and executing %d host calls..." totalCalls

  (durSec, returnedVal) <- withVM Nothing Nothing $ \vm -> do
    -- 1. Assemble host module "env" with typed Haskell functions
    hostMod <- buildHostModule "env" $ \builder -> do
      -- host_add: (Int32 -> Int32 -> IO Int32)
      builderAddHostFunc builder "host_add" $ \a b ->
        (pure (a + b) :: IO Int32)

      -- host_callback: (Int32 -> IO Int32)
      builderAddHostFunc builder "host_callback" $ \i ->
        (pure (i * 2) :: IO Int32)

    -- Register "env" in VM
    regRes <- registerModuleFromImport vm hostMod
    case regRes of
      Left err -> throwIO (userError $ "Failed to register host module: " ++ show err)
      Right () -> pure ()

    -- Load, validate, instantiate guest
    loadRes <- loadWasmBytes vm hostImportWasm
    case loadRes of
      Left err -> throwIO (userError $ "Failed to load hostImportWasm: " ++ show err)
      Right () -> pure ()
    valRes <- validate vm
    case valRes of
      Left err -> throwIO (userError $ "Failed to validate hostImportWasm: " ++ show err)
      Right () -> pure ()
    instRes <- instantiate vm
    case instRes of
      Left err -> throwIO (userError $ "Failed to instantiate hostImportWasm: " ++ show err)
      Right () -> pure ()

    -- Measure execution of 100,000 host function boundary crossings
    t0 <- getTime Monotonic
    execRes <- execute vm "run_host_calls" [WasmI32 (fromIntegral iterations)]
    t1 <- getTime Monotonic

    resVal <- case execRes of
      Left err -> throwIO (userError $ "Execution of run_host_calls failed: " ++ show err)
      Right [WasmI32 v] -> pure v
      Right other -> throwIO (userError $ "Unexpected return: " ++ show other)

    let dSec = fromIntegral (toNanoSecs (diffTimeSpec t1 t0)) / 1e9 :: Double
    pure (dSec, resVal)

  -- Verify expected arithmetic value
  -- sum [i * 2 | i <- [0 .. iterations - 1]]
  let expectedInt = sum [(fromIntegral i :: Integer) * 2 | i <- [0 .. iterations - 1]]
      expectedI32 = fromIntegral (fromIntegral expectedInt :: Word32) :: Int32
      callsPerSec = fromIntegral totalCalls / durSec

  if returnedVal == expectedI32
    then putStrLn $ printf "    Returned: %d (verified correct) in %.3f s" returnedVal durSec
    else throwIO (userError $ printf "Arithmetic mismatch! Expected %d, got %d" expectedI32 returnedVal)

  printf "    Host call throughput: %.2f calls/sec\n\n" callsPerSec

  -- 2. Test CallingFrame memory inspection and mutation
  putStrLn "--> [2/2] Testing CallingFrame memory inspection and mutation..."
  memInspectOk <- withVM Nothing Nothing $ \_ -> do
    ft <- createFunctionType [I32, I32] [I32]
    _fnInst <- createFunction ft $ \frame params -> do
      case params of
        [WasmI32 off, WasmI32 len] -> do
          -- Test reading from caller memory via CallingFrame
          mBytes <- readCallerMemory frame 0 (fromIntegral off) (fromIntegral len)
          case mBytes of
            Nothing -> pure (Right [WasmI32 (-1)])
            Just bs -> do
              -- Mutate and write back to caller
              let modified = BS.map (+ 1) bs
              _ <- writeCallerMemory frame 0 (fromIntegral off) modified
              pure (Right [WasmI32 (fromIntegral (BS.length modified))])
        _ -> pure (Right [WasmI32 0])
    pure True

  printf "==> Host Pipeline Suite PASSED (%.2f calls/sec)\n\n" callsPerSec

  pure $
    HostPipelineMetrics
      { hpmTotalCalls = totalCalls
      , hpmDurationSec = durSec
      , hpmCallsPerSec = callsPerSec
      , hpmCallerMemPass = memInspectOk
      }