Single-pass aser_slot for HTMX path + kernel eval timing + stable hash
Eliminated double-aser for HTMX requests: build OOB wrapper AST (~shared:layout/oob-sx :content wrapped_ast) and aser_slot in ONE pass — same pattern as the full-page path. Halves aser_slot calls. Added kernel-side timing to stderr: [aser-slot] eval=3.6s io_flush=0.0s batched=3 result=22235 chars Results show batch IO works (io_flush=0.0s for 3 highlight calls) and the bottleneck is pure CEK evaluation time, not IO. Performance after single-pass fix: Home: 0.7s eval (was 2.2s total) Reactive: 3.6s eval (was 6.8s total) Language: 1.1s eval (was 18.9s total — double-aser eliminated) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,3 @@
|
|||||||
(executables
|
(executables
|
||||||
(names run_tests debug_set sx_server)
|
(names run_tests debug_set sx_server)
|
||||||
(libraries sx))
|
(libraries sx unix))
|
||||||
|
|||||||
@@ -736,15 +736,21 @@ let dispatch env cmd =
|
|||||||
let call = List [Symbol "aser";
|
let call = List [Symbol "aser";
|
||||||
List [Symbol "quote"; expr];
|
List [Symbol "quote"; expr];
|
||||||
Env env] in
|
Env env] in
|
||||||
|
let t0 = Unix.gettimeofday () in
|
||||||
let result = Sx_ref.eval_expr call (Env env) in
|
let result = Sx_ref.eval_expr call (Env env) in
|
||||||
|
let t1 = Unix.gettimeofday () in
|
||||||
io_batch_mode := false;
|
io_batch_mode := false;
|
||||||
Hashtbl.remove env.bindings "expand-components?";
|
Hashtbl.remove env.bindings "expand-components?";
|
||||||
let result_str = match result with
|
let result_str = match result with
|
||||||
| String s | SxExpr s -> s
|
| String s | SxExpr s -> s
|
||||||
| _ -> serialize_value result
|
| _ -> serialize_value result
|
||||||
in
|
in
|
||||||
|
let n_batched = List.length !io_queue in
|
||||||
(* Flush batched IO: send requests, receive responses, replace placeholders *)
|
(* Flush batched IO: send requests, receive responses, replace placeholders *)
|
||||||
let final = flush_batched_io result_str in
|
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)
|
send (Printf.sprintf "(ok-raw %s)" final)
|
||||||
with
|
with
|
||||||
| Eval_error msg ->
|
| Eval_error msg ->
|
||||||
|
|||||||
@@ -59,11 +59,12 @@ class OcamlBridge:
|
|||||||
)
|
)
|
||||||
|
|
||||||
_logger.info("Starting OCaml SX kernel: %s", bin_path)
|
_logger.info("Starting OCaml SX kernel: %s", bin_path)
|
||||||
|
import sys
|
||||||
self._proc = await asyncio.create_subprocess_exec(
|
self._proc = await asyncio.create_subprocess_exec(
|
||||||
bin_path,
|
bin_path,
|
||||||
stdin=asyncio.subprocess.PIPE,
|
stdin=asyncio.subprocess.PIPE,
|
||||||
stdout=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
|
limit=10 * 1024 * 1024, # 10MB readline buffer for large spec data
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -312,12 +313,9 @@ class OcamlBridge:
|
|||||||
assert self._proc and self._proc.stdout
|
assert self._proc and self._proc.stdout
|
||||||
data = await self._proc.stdout.readline()
|
data = await self._proc.stdout.readline()
|
||||||
if not data:
|
if not data:
|
||||||
# Process died — collect stderr for diagnostics
|
# Process died
|
||||||
stderr = b""
|
|
||||||
if self._proc.stderr:
|
|
||||||
stderr = await self._proc.stderr.read()
|
|
||||||
raise OcamlBridgeError(
|
raise OcamlBridgeError(
|
||||||
f"OCaml subprocess died unexpectedly. stderr: {stderr.decode(errors='replace')}"
|
"OCaml subprocess died unexpectedly (check container stderr)"
|
||||||
)
|
)
|
||||||
line = data.decode().rstrip("\n")
|
line = data.decode().rstrip("\n")
|
||||||
_logger.debug("RECV: %s", line[:120])
|
_logger.debug("RECV: %s", line[:120])
|
||||||
|
|||||||
@@ -203,21 +203,22 @@ async def eval_sx_url(raw_path: str) -> Any:
|
|||||||
ocaml_ctx = {"_helper_service": "sx"}
|
ocaml_ctx = {"_helper_service": "sx"}
|
||||||
|
|
||||||
if is_htmx_request():
|
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(
|
content_sx = SxExpr(await bridge.aser_slot(
|
||||||
serialize(wrapped_ast), ctx=ocaml_ctx))
|
serialize(oob_ast), ctx=ocaml_ctx))
|
||||||
return sx_response(await oob_page_sx(content=content_sx))
|
return sx_response(content_sx)
|
||||||
else:
|
else:
|
||||||
# Full page: build layout+content AST and aser_slot
|
# Full page: single-pass — layout + content in ONE aser_slot
|
||||||
# in ONE pass — avoids double-aser that breaks when
|
|
||||||
# re-parsed content contains islands/reactive symbols.
|
|
||||||
full_ast = [
|
full_ast = [
|
||||||
Symbol("~shared:layout/app-body"),
|
Symbol("~shared:layout/app-body"),
|
||||||
Keyword("content"), wrapped_ast,
|
Keyword("content"), wrapped_ast,
|
||||||
]
|
]
|
||||||
full_text = serialize(full_ast)
|
|
||||||
body_sx = SxExpr(await bridge.aser_slot(
|
body_sx = SxExpr(await bridge.aser_slot(
|
||||||
full_text, ctx=ocaml_ctx))
|
serialize(full_ast), ctx=ocaml_ctx))
|
||||||
tctx = await get_template_context()
|
tctx = await get_template_context()
|
||||||
return await make_response(
|
return await make_response(
|
||||||
await sx_page(tctx, body_sx), 200)
|
await sx_page(tctx, body_sx), 200)
|
||||||
|
|||||||
Reference in New Issue
Block a user