@@ -37,7 +37,7 @@ import Data.Scientific (Scientific)
3737import qualified Data.Set as Set
3838import Data.Text (Text )
3939import qualified Data.Text as Text
40- import GHC.Conc (setAllocationCounter , enableAllocationLimit )
40+ import GHC.Conc (setAllocationCounter , getAllocationCounter , enableAllocationLimit )
4141import GHC.IO.Exception (AllocationLimitExceeded (.. ))
4242import Servant
4343import System.FilePath ((<.>) )
@@ -72,10 +72,14 @@ type DeploymentRoutes =
7272 :<|> " functions" :> Capture " name" Text :> FunctionRoutes
7373 :<|> " openapi.json" :> Get '[JSON ] DeploymentMetadata -- Simplified; returns metadata for now
7474
75+ -- | Evaluation responses include an X-Eval-Alloc-Bytes header reporting
76+ -- the GHC allocation bytes consumed by the evaluation.
77+ type EvalResponse a = Headers '[Header " X-Eval-Alloc-Bytes" Int64 ] a
78+
7579type FunctionRoutes =
7680 Get '[JSON ] Function
77- :<|> " evaluation" :> Header " X-L4-Trace" Text :> QueryParam " trace" TraceLevel :> QueryParam " graphviz" Bool :> ReqBody '[JSON ] FnArguments :> Post '[JSON ] SimpleResponse
78- :<|> " evaluation" :> " batch" :> Header " X-L4-Trace" Text :> QueryParam " trace" TraceLevel :> QueryParam " graphviz" Bool :> ReqBody '[JSON ] BatchRequest :> Post '[JSON ] BatchResponse
81+ :<|> " evaluation" :> Header " X-L4-Trace" Text :> QueryParam " trace" TraceLevel :> QueryParam " graphviz" Bool :> ReqBody '[JSON ] FnArguments :> Post '[JSON ] ( EvalResponse SimpleResponse )
82+ :<|> " evaluation" :> " batch" :> Header " X-L4-Trace" Text :> QueryParam " trace" TraceLevel :> QueryParam " graphviz" Bool :> ReqBody '[JSON ] BatchRequest :> Post '[JSON ] ( EvalResponse BatchResponse )
7983 :<|> " query-plan" :> ReqBody '[JSON ] FnArguments :> Post '[JSON ] QueryPlanResponse
8084 :<|> " state-graphs" :> Get '[JSON ] StateGraphListResponse
8185 :<|> " state-graphs" :> Capture " graphName" Text :> Get '[PlainText ] Text
@@ -163,7 +167,7 @@ evalFunctionHandler
163167 -> Maybe TraceLevel -- ?trace= query param
164168 -> Maybe Bool -- ?graphviz= query param
165169 -> FnArguments
166- -> AppM SimpleResponse
170+ -> AppM ( EvalResponse SimpleResponse )
167171evalFunctionHandler deployId fnName mTraceHeader mTraceParam mGraphViz fnArgs = do
168172 (fns, _meta) <- requireDeploymentReady deployId
169173 vf <- requireFunction fns fnName
@@ -173,7 +177,7 @@ evalFunctionHandler deployId fnName mTraceHeader mTraceParam mGraphViz fnArgs =
173177 isDeontic = Map. member " startTime" paramMap
174178 && Map. member " events" paramMap
175179
176- case (isDeontic, fnArgs. startTime, fnArgs. events) of
180+ (result, allocBytes) <- case (isDeontic, fnArgs. startTime, fnArgs. events) of
177181 -- Non-deontic function: reject deontic params
178182 (False , Just _, _) ->
179183 throwError err400 { errBody = jsonError " startTime and events are only valid for functions returning DEONTIC" }
@@ -192,6 +196,7 @@ evalFunctionHandler deployId fnName mTraceHeader mTraceParam mGraphViz fnArgs =
192196 runDeonticEvaluatorFor vf fnArgs. fnEvalBackend (Map. toList fnArgs. fnArguments) st evts
193197 vf. fnImpl. deonticPartyType vf. fnImpl. deonticActionType
194198 mTraceHeader mTraceParam mGraphViz
199+ pure $ addHeader allocBytes result
195200
196201-- | POST /deployments/{id}/functions/{fn}/evaluation/batch
197202batchFunctionHandler
@@ -201,7 +206,7 @@ batchFunctionHandler
201206 -> Maybe TraceLevel -- ?trace= query param
202207 -> Maybe Bool -- ?graphviz= query param
203208 -> BatchRequest
204- -> AppM BatchResponse
209+ -> AppM ( EvalResponse BatchResponse )
205210batchFunctionHandler deployId fnName mTraceHeader mTraceParam mGraphViz batchArgs = do
206211 let traceLevel = determineTraceLevel mTraceHeader mTraceParam
207212 includeGraphViz = traceLevel == TraceFull && Maybe. fromMaybe False mGraphViz
@@ -211,7 +216,7 @@ batchFunctionHandler deployId fnName mTraceHeader mTraceParam mGraphViz batchArg
211216 -- Capture the environment before going concurrent
212217 env <- ask
213218
214- -- Evaluate all cases in parallel
219+ -- Evaluate all cases in parallel, collecting alloc bytes per case
215220 evalResults <- liftIO $ forConcurrently batchArgs. cases $ \ inputCase -> do
216221 let args = Map. assocs $ fmap Just inputCase. attributes
217222 r <- runAppM env (runEvaluatorForDirect vf Nothing args outputFilter traceLevel includeGraphViz)
@@ -223,12 +228,13 @@ batchFunctionHandler deployId fnName mTraceHeader mTraceParam mGraphViz batchArg
223228 [] -> pure ()
224229
225230 let
226- responses = [(rid, simpleResp) | (rid, Right simpleResp) <- evalResults]
231+ responses = [(rid, simpleResp, alloc ) | (rid, Right ( simpleResp, alloc) ) <- evalResults]
227232 nCases = length responses
233+ totalAllocBytes = sum [alloc | (_, _, alloc) <- responses]
228234
229235 successfulRuns =
230236 Maybe. mapMaybe
231- ( \ (rid, simpleRes) -> case simpleRes of
237+ ( \ (rid, simpleRes, _ ) -> case simpleRes of
232238 SimpleResponse r -> Just (rid, r)
233239 SimpleError _ -> Nothing
234240 )
@@ -237,7 +243,7 @@ batchFunctionHandler deployId fnName mTraceHeader mTraceParam mGraphViz batchArg
237243 nSuccessful = length successfulRuns
238244 nIgnored = nCases - nSuccessful
239245
240- pure $ BatchResponse
246+ pure $ addHeader totalAllocBytes $ BatchResponse
241247 { cases =
242248 [ OutputCase
243249 { id = rid
@@ -341,7 +347,7 @@ openApiHandler deployId = do
341347-- Evaluation helpers
342348-- ----------------------------------------------------------------------------
343349
344- -- | Run a function evaluator, returning SimpleResponse.
350+ -- | Run a function evaluator, returning SimpleResponse and alloc bytes .
345351runEvaluatorFor
346352 :: ValidatedFunction
347353 -> Maybe EvalBackend
@@ -350,27 +356,27 @@ runEvaluatorFor
350356 -> Maybe Text -- X-L4-Trace header
351357 -> Maybe TraceLevel -- ?trace= query param
352358 -> Maybe Bool -- ?graphviz= query param
353- -> AppM SimpleResponse
359+ -> AppM ( SimpleResponse , Int64 )
354360runEvaluatorFor vf engine args outputFilter mTraceHeader mTraceParam mGraphViz = do
355361 let traceLevel = determineTraceLevel mTraceHeader mTraceParam
356362 includeGraphViz = traceLevel == TraceFull && Maybe. fromMaybe False mGraphViz
357363 runEvaluatorForDirect vf engine args outputFilter traceLevel includeGraphViz
358364
359- -- | Core evaluator logic.
365+ -- | Core evaluator logic. Returns the response and GHC allocation bytes consumed.
360366runEvaluatorForDirect
361367 :: ValidatedFunction
362368 -> Maybe EvalBackend
363369 -> [(Text , Maybe FnLiteral )]
364370 -> Maybe (Set. Set Text )
365371 -> TraceLevel
366372 -> Bool
367- -> AppM SimpleResponse
373+ -> AppM ( SimpleResponse , Int64 )
368374runEvaluatorForDirect vf engine args outputFilter traceLevel includeGraphViz = do
369375 let evalBackend = Maybe. fromMaybe JL4 engine
370376 case Map. lookup evalBackend vf. fnEvaluator of
371377 Nothing -> throwError err500 { errBody = jsonError " No evaluator available for backend" }
372378 Just runFn -> do
373- evaluationResult <-
379+ ( evaluationResult, allocBytes) <-
374380 timeoutAction $
375381 runExceptT
376382 ( runFn. runFunction
@@ -380,8 +386,8 @@ runEvaluatorForDirect vf engine args outputFilter traceLevel includeGraphViz = d
380386 includeGraphViz
381387 )
382388 case evaluationResult of
383- Left err -> pure $ SimpleError err
384- Right r -> pure $ SimpleResponse r
389+ Left err -> pure ( SimpleError err, allocBytes)
390+ Right r -> pure ( SimpleResponse r, allocBytes)
385391
386392-- | Run deontic evaluation with EVALTRACE.
387393runDeonticEvaluatorFor
@@ -395,7 +401,7 @@ runDeonticEvaluatorFor
395401 -> Maybe Text -- X-L4-Trace header
396402 -> Maybe TraceLevel -- ?trace= query param
397403 -> Maybe Bool -- ?graphviz= query param
398- -> AppM SimpleResponse
404+ -> AppM ( SimpleResponse , Int64 )
399405runDeonticEvaluatorFor vf _engine args startTime events mPartyType mActionType mTraceHeader mTraceParam mGraphViz = do
400406 let traceLevel = determineTraceLevel mTraceHeader mTraceParam
401407 includeGraphViz = traceLevel == TraceFull && Maybe. fromMaybe False mGraphViz
@@ -407,7 +413,7 @@ runDeonticEvaluatorFor vf _engine args startTime events mPartyType mActionType m
407413 let fnDecl = toDecl vf. fnImpl
408414 filepath = Text. unpack vf. fnImpl. name <.> " l4"
409415
410- evaluationResult <-
416+ ( evaluationResult, allocBytes) <-
411417 timeoutAction $
412418 runExceptT
413419 ( evaluateWithCompiledDeontic
@@ -424,16 +430,17 @@ runDeonticEvaluatorFor vf _engine args startTime events mPartyType mActionType m
424430 )
425431
426432 case evaluationResult of
427- Left err -> pure $ SimpleError err
428- Right r -> pure $ SimpleResponse r
433+ Left err -> pure ( SimpleError err, allocBytes)
434+ Right r -> pure ( SimpleResponse r, allocBytes)
429435
430436-- | Run an AppM action in IO with a given environment.
431437runAppM :: AppEnv -> AppM a -> IO (Either ServerError a )
432438runAppM env action = runHandler $ runReaderT action env
433439
434440-- | Timeout and memory-limited evaluation action.
435441-- Uses configurable eval timeout and per-evaluation allocation limits.
436- timeoutAction :: IO b -> AppM b
442+ -- Returns the result and the number of GHC allocation bytes consumed.
443+ timeoutAction :: IO b -> AppM (b , Int64 )
437444timeoutAction act = do
438445 cfg <- asks (. options)
439446 let timeoutMicros = cfg. evalTimeout * 1_000_000
@@ -442,12 +449,14 @@ timeoutAction act = do
442449 (timeout timeoutMicros $ do
443450 setAllocationCounter memLimitBytes
444451 enableAllocationLimit
445- act
452+ r <- act
453+ remaining <- getAllocationCounter
454+ pure (r, memLimitBytes - remaining)
446455 ) `catch` \ AllocationLimitExceeded ->
447456 pure Nothing
448457 case result of
449458 Nothing -> throwError err500 { errBody = jsonError " Evaluation resource limit exceeded" }
450- Just r -> pure r
459+ Just x -> pure x
451460
452461-- ----------------------------------------------------------------------------
453462-- Helpers
0 commit comments