diff --git a/hosts/ocaml/bin/dune b/hosts/ocaml/bin/dune index 354a7a4..8524650 100644 --- a/hosts/ocaml/bin/dune +++ b/hosts/ocaml/bin/dune @@ -1,3 +1,3 @@ (executables (names run_tests debug_set sx_server) - (libraries sx)) + (libraries sx unix)) diff --git a/hosts/ocaml/bin/sx_server.ml b/hosts/ocaml/bin/sx_server.ml index 7f9d0a6..3d7d679 100644 --- a/hosts/ocaml/bin/sx_server.ml +++ b/hosts/ocaml/bin/sx_server.ml @@ -736,15 +736,21 @@ let dispatch env cmd = let call = List [Symbol "aser"; List [Symbol "quote"; expr]; Env env] in + let t0 = Unix.gettimeofday () in let result = Sx_ref.eval_expr call (Env env) in + let t1 = Unix.gettimeofday () in io_batch_mode := false; Hashtbl.remove env.bindings "expand-components?"; let result_str = match result with | String s | SxExpr s -> s | _ -> serialize_value result in + let n_batched = List.length !io_queue in (* Flush batched IO: send requests, receive responses, replace placeholders *) let final = flush_batched_io result_str in + let t2 = Unix.gettimeofday () in + Printf.eprintf "[aser-slot] eval=%.1fs io_flush=%.1fs batched=%d result=%d chars\n%!" + (t1 -. t0) (t2 -. t1) n_batched (String.length final); send (Printf.sprintf "(ok-raw %s)" final) with | Eval_error msg -> diff --git a/shared/sx/ocaml_bridge.py b/shared/sx/ocaml_bridge.py index 6671f08..b947256 100644 --- a/shared/sx/ocaml_bridge.py +++ b/shared/sx/ocaml_bridge.py @@ -59,11 +59,12 @@ class OcamlBridge: ) _logger.info("Starting OCaml SX kernel: %s", bin_path) + import sys self._proc = await asyncio.create_subprocess_exec( bin_path, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, + stderr=sys.stderr, # kernel timing/debug to container logs limit=10 * 1024 * 1024, # 10MB readline buffer for large spec data ) @@ -312,12 +313,9 @@ class OcamlBridge: assert self._proc and self._proc.stdout data = await self._proc.stdout.readline() if not data: - # Process died — collect stderr for diagnostics - stderr = b"" - if self._proc.stderr: - stderr = await self._proc.stderr.read() + # Process died raise OcamlBridgeError( - f"OCaml subprocess died unexpectedly. stderr: {stderr.decode(errors='replace')}" + "OCaml subprocess died unexpectedly (check container stderr)" ) line = data.decode().rstrip("\n") _logger.debug("RECV: %s", line[:120]) diff --git a/sx/sxc/pages/sx_router.py b/sx/sxc/pages/sx_router.py index 60d064d..17287d2 100644 --- a/sx/sxc/pages/sx_router.py +++ b/sx/sxc/pages/sx_router.py @@ -203,21 +203,22 @@ async def eval_sx_url(raw_path: str) -> Any: ocaml_ctx = {"_helper_service": "sx"} if is_htmx_request(): - # HTMX: aser_slot the content, wrap in OOB layout + # HTMX: single-pass — OOB wrapper + content in ONE aser_slot + oob_ast = [ + Symbol("~shared:layout/oob-sx"), + Keyword("content"), wrapped_ast, + ] content_sx = SxExpr(await bridge.aser_slot( - serialize(wrapped_ast), ctx=ocaml_ctx)) - return sx_response(await oob_page_sx(content=content_sx)) + serialize(oob_ast), ctx=ocaml_ctx)) + return sx_response(content_sx) else: - # Full page: build layout+content AST and aser_slot - # in ONE pass — avoids double-aser that breaks when - # re-parsed content contains islands/reactive symbols. + # Full page: single-pass — layout + content in ONE aser_slot full_ast = [ Symbol("~shared:layout/app-body"), Keyword("content"), wrapped_ast, ] - full_text = serialize(full_ast) body_sx = SxExpr(await bridge.aser_slot( - full_text, ctx=ocaml_ctx)) + serialize(full_ast), ctx=ocaml_ctx)) tctx = await get_template_context() return await make_response( await sx_page(tctx, body_sx), 200)