VM aser-slot → sx-page-full: single-call page render, 0.55s warm
Compiler fixes: - Upvalue re-lookup returns own position (uv-index), not parent slot - Spec: cek-call uses (make-env) not (dict) — OCaml Dict≠Env - Bootstrap post-processes transpiler Dict→Env for cek_call VM runtime fixes: - compile_adapter evaluates constant defines (SPECIAL_FORM_NAMES etc.) via execute_module instead of wrapping as NativeFn closures - Native primitives: map-indexed, some, every? - Nil-safe HO forms: map/filter/for-each/some/every? accept nil as empty - expand-components? set in kernel env (not just VM globals) - unwrap_env diagnostic: reports actual type received sx-page-full command: - Single OCaml call: aser-slot body + render-to-html shell - Eliminates two pipe round-trips (was: aser-slot→Python→shell render) - Shell statics (component_defs, CSS, pages_sx) cached in Python, injected into kernel once, referenced by symbol in per-request command - Large blobs use placeholder tokens — Python splices post-render, pipe transfers ~51KB instead of 2MB Performance (warm): - Server total: 0.55s (was ~2s) - aser-slot VM: 0.3s, shell render: 0.01s, pipe: 0.06s - kwargs computation: 0.000s (cached) SX_STANDALONE mode for sx_docs dev (skips fragment fetches). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -792,6 +792,162 @@ def _sx_literal(v: object) -> str:
|
||||
|
||||
|
||||
|
||||
_cached_shell_static: dict[str, Any] | None = None
|
||||
_cached_shell_comp_hash: str | None = None
|
||||
|
||||
|
||||
def invalidate_shell_cache():
|
||||
"""Call on component hot-reload to recompute shell statics."""
|
||||
global _cached_shell_static, _cached_shell_comp_hash
|
||||
_cached_shell_static = None
|
||||
_cached_shell_comp_hash = None
|
||||
|
||||
|
||||
def _get_shell_static() -> dict[str, Any]:
|
||||
"""Compute and cache all shell kwargs that don't change per-request.
|
||||
|
||||
This is the expensive part: component dep scanning, serialization,
|
||||
CSS class scanning, rule lookup, pages registry. All stable until
|
||||
components are hot-reloaded.
|
||||
"""
|
||||
global _cached_shell_static, _cached_shell_comp_hash
|
||||
from .jinja_bridge import components_for_page, css_classes_for_page, _component_env_hash
|
||||
from .css_registry import lookup_rules, get_preamble, registry_loaded, store_css_hash
|
||||
|
||||
current_hash = _component_env_hash()
|
||||
if _cached_shell_static is not None and _cached_shell_comp_hash == current_hash:
|
||||
return _cached_shell_static
|
||||
|
||||
import time
|
||||
t0 = time.monotonic()
|
||||
|
||||
from quart import current_app as _ca
|
||||
from .jinja_bridge import client_components_tag, _COMPONENT_ENV, _CLIENT_LIBRARY_SOURCES
|
||||
from .jinja_bridge import _component_env_hash
|
||||
from .parser import serialize as _serialize
|
||||
|
||||
# Send ALL component definitions — the hash is stable per env so the
|
||||
# browser caches them across all pages. Server-side expansion handles
|
||||
# the per-page subset; the client needs the full set for client-side
|
||||
# routing to any page.
|
||||
parts = []
|
||||
for key, val in _COMPONENT_ENV.items():
|
||||
from .types import Island, Component, Macro
|
||||
if isinstance(val, Island):
|
||||
ps = ["&key"] + list(val.params)
|
||||
if val.has_children: ps.extend(["&rest", "children"])
|
||||
parts.append(f"(defisland ~{val.name} ({' '.join(ps)}) {_serialize(val.body, pretty=True)})")
|
||||
elif isinstance(val, Component):
|
||||
ps = ["&key"] + list(val.params)
|
||||
if val.has_children: ps.extend(["&rest", "children"])
|
||||
parts.append(f"(defcomp ~{val.name} ({' '.join(ps)}) {_serialize(val.body, pretty=True)})")
|
||||
elif isinstance(val, Macro):
|
||||
ps = list(val.params)
|
||||
if val.rest_param: ps.extend(["&rest", val.rest_param])
|
||||
parts.append(f"(defmacro {val.name} ({' '.join(ps)}) {_serialize(val.body, pretty=True)})")
|
||||
all_parts = list(_CLIENT_LIBRARY_SOURCES) + parts
|
||||
component_defs = "\n".join(all_parts)
|
||||
component_hash = _component_env_hash()
|
||||
|
||||
# CSS: scan ALL components (not per-page) for the static cache
|
||||
sx_css = ""
|
||||
sx_css_classes = ""
|
||||
if registry_loaded():
|
||||
classes: set[str] = set()
|
||||
from .types import Island as _I, Component as _C
|
||||
for val in _COMPONENT_ENV.values():
|
||||
if isinstance(val, (_I, _C)) and val.css_classes:
|
||||
classes.update(val.css_classes)
|
||||
classes.update(["bg-stone-50", "text-stone-900"])
|
||||
rules = lookup_rules(classes)
|
||||
sx_css = get_preamble() + rules
|
||||
sx_css_classes = store_css_hash(classes)
|
||||
|
||||
pages_sx = _build_pages_sx(_ca.name)
|
||||
|
||||
_shell_cfg = _ca.config.get("SX_SHELL", {})
|
||||
|
||||
static = dict(
|
||||
component_hash=component_hash,
|
||||
component_defs=component_defs,
|
||||
pages_sx=pages_sx,
|
||||
sx_css=sx_css,
|
||||
sx_css_classes=sx_css_classes,
|
||||
sx_js_hash=_script_hash("sx-browser.js"),
|
||||
body_js_hash=_script_hash("body.js"),
|
||||
asset_url=_ca.config.get("ASSET_URL", "/static"),
|
||||
head_scripts=_shell_cfg.get("head_scripts"),
|
||||
inline_css=_shell_cfg.get("inline_css"),
|
||||
inline_head_js=_shell_cfg.get("inline_head_js"),
|
||||
init_sx=_shell_cfg.get("init_sx"),
|
||||
body_scripts=_shell_cfg.get("body_scripts"),
|
||||
)
|
||||
|
||||
t1 = time.monotonic()
|
||||
import logging
|
||||
logging.getLogger("sx.pages").info(
|
||||
"[shell-static] computed in %.3fs, comp_defs=%d css=%d pages=%d",
|
||||
t1 - t0, len(component_defs), len(sx_css), len(pages_sx))
|
||||
|
||||
_cached_shell_static = static
|
||||
_cached_shell_comp_hash = current_hash
|
||||
return static
|
||||
|
||||
|
||||
async def _build_shell_kwargs(ctx: dict, page_sx: str, *,
|
||||
meta_html: str = "",
|
||||
head_scripts: list[str] | None = None,
|
||||
inline_css: str | None = None,
|
||||
inline_head_js: str | None = None,
|
||||
init_sx: str | None = None,
|
||||
body_scripts: list[str] | None = None) -> dict[str, Any]:
|
||||
"""Compute all shell kwargs for sx-page-shell.
|
||||
|
||||
Static parts (components, CSS, pages) are cached. Only per-request
|
||||
values (title, csrf) are computed fresh.
|
||||
"""
|
||||
static = _get_shell_static()
|
||||
|
||||
asset_url = get_asset_url(ctx) or static["asset_url"]
|
||||
title = ctx.get("base_title", "Rose Ash")
|
||||
csrf = _get_csrf_token()
|
||||
|
||||
kwargs: dict[str, Any] = dict(static)
|
||||
kwargs.update(
|
||||
title=_html_escape(title),
|
||||
asset_url=asset_url,
|
||||
meta_html=meta_html,
|
||||
csrf=_html_escape(csrf),
|
||||
)
|
||||
|
||||
# Per-page CSS: scan THIS page's classes and add to cached CSS
|
||||
from .css_registry import scan_classes_from_sx, lookup_rules, registry_loaded
|
||||
if registry_loaded() and page_sx:
|
||||
page_classes = scan_classes_from_sx(page_sx)
|
||||
if page_classes:
|
||||
extra_rules = lookup_rules(page_classes)
|
||||
if extra_rules:
|
||||
kwargs["sx_css"] = static["sx_css"] + extra_rules
|
||||
|
||||
# Cookie-based component caching
|
||||
client_hash = _get_sx_comp_cookie()
|
||||
if not _is_dev_mode() and client_hash and client_hash == static["component_hash"]:
|
||||
kwargs["component_defs"] = ""
|
||||
|
||||
# Per-call overrides
|
||||
if head_scripts is not None:
|
||||
kwargs["head_scripts"] = head_scripts
|
||||
if inline_css is not None:
|
||||
kwargs["inline_css"] = inline_css
|
||||
if inline_head_js is not None:
|
||||
kwargs["inline_head_js"] = inline_head_js
|
||||
if init_sx is not None:
|
||||
kwargs["init_sx"] = init_sx
|
||||
if body_scripts is not None:
|
||||
kwargs["body_scripts"] = body_scripts
|
||||
return kwargs
|
||||
|
||||
|
||||
async def sx_page(ctx: dict, page_sx: str, *,
|
||||
meta_html: str = "",
|
||||
head_scripts: list[str] | None = None,
|
||||
@@ -799,109 +955,18 @@ async def sx_page(ctx: dict, page_sx: str, *,
|
||||
inline_head_js: str | None = None,
|
||||
init_sx: str | None = None,
|
||||
body_scripts: list[str] | None = None) -> str:
|
||||
"""Return a minimal HTML shell that boots the page from sx source.
|
||||
|
||||
The browser loads component definitions and page sx, then sx.js
|
||||
renders everything client-side. CSS rules are scanned from the sx
|
||||
source and component defs, then injected as a <style> block.
|
||||
|
||||
The shell is rendered from the ~shared:shell/sx-page-shell SX component
|
||||
(shared/sx/templates/shell.sx).
|
||||
"""
|
||||
from .jinja_bridge import components_for_page, css_classes_for_page
|
||||
from .css_registry import lookup_rules, get_preamble, registry_loaded, store_css_hash
|
||||
|
||||
# Per-page component bundle: this page's deps + all :data page deps
|
||||
from quart import current_app as _ca
|
||||
component_defs, component_hash = components_for_page(page_sx, service=_ca.name)
|
||||
|
||||
# Check if client already has this version cached (via cookie)
|
||||
# In dev mode, always send full source so edits are visible immediately
|
||||
client_hash = _get_sx_comp_cookie()
|
||||
if not _is_dev_mode() and client_hash and client_hash == component_hash:
|
||||
# Client has current components cached — send empty source
|
||||
component_defs = ""
|
||||
|
||||
# Scan for CSS classes — only from components this page uses + page source
|
||||
sx_css = ""
|
||||
sx_css_classes = ""
|
||||
sx_css_hash = ""
|
||||
if registry_loaded():
|
||||
classes = css_classes_for_page(page_sx, service=_ca.name)
|
||||
# Always include body classes
|
||||
classes.update(["bg-stone-50", "text-stone-900"])
|
||||
rules = lookup_rules(classes)
|
||||
sx_css = get_preamble() + rules
|
||||
sx_css_hash = store_css_hash(classes)
|
||||
sx_css_classes = sx_css_hash
|
||||
|
||||
asset_url = get_asset_url(ctx)
|
||||
title = ctx.get("base_title", "Rose Ash")
|
||||
csrf = _get_csrf_token()
|
||||
|
||||
# Dev mode: pretty-print page sx for readable View Source
|
||||
if _is_dev_mode() and page_sx and page_sx.startswith("("):
|
||||
from .parser import parse as _parse, serialize as _serialize
|
||||
try:
|
||||
page_sx = _serialize(_parse(page_sx), pretty=True)
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.getLogger("sx").warning("Pretty-print page_sx failed: %s", e)
|
||||
|
||||
# Page registry for client-side routing
|
||||
import logging
|
||||
_plog = logging.getLogger("sx.pages")
|
||||
from quart import current_app
|
||||
pages_sx = _build_pages_sx(current_app.name)
|
||||
_plog.debug("sx_page: pages_sx %d bytes for service %s", len(pages_sx), current_app.name)
|
||||
if pages_sx:
|
||||
_plog.debug("sx_page: pages_sx first 200 chars: %s", pages_sx[:200])
|
||||
|
||||
# Ensure page_sx is a plain str, not SxExpr — _build_component_ast
|
||||
# parses SxExpr back into AST, which _arender then evaluates as HTML
|
||||
# instead of passing through as raw content for the script tag.
|
||||
"""Return a minimal HTML shell that boots the page from sx source."""
|
||||
# Ensure page_sx is a plain str
|
||||
if isinstance(page_sx, SxExpr):
|
||||
page_sx = "".join([page_sx])
|
||||
|
||||
# Per-app shell config: check explicit args, then app config, then defaults
|
||||
from quart import current_app as _app
|
||||
_shell_cfg = _app.config.get("SX_SHELL", {})
|
||||
if head_scripts is None:
|
||||
head_scripts = _shell_cfg.get("head_scripts")
|
||||
if inline_css is None:
|
||||
inline_css = _shell_cfg.get("inline_css")
|
||||
if inline_head_js is None:
|
||||
inline_head_js = _shell_cfg.get("inline_head_js")
|
||||
if init_sx is None:
|
||||
init_sx = _shell_cfg.get("init_sx")
|
||||
if body_scripts is None:
|
||||
body_scripts = _shell_cfg.get("body_scripts")
|
||||
|
||||
shell_kwargs: dict[str, Any] = dict(
|
||||
title=_html_escape(title),
|
||||
asset_url=asset_url,
|
||||
meta_html=meta_html,
|
||||
csrf=_html_escape(csrf),
|
||||
component_hash=component_hash,
|
||||
component_defs=component_defs,
|
||||
pages_sx=pages_sx,
|
||||
page_sx=page_sx,
|
||||
sx_css=sx_css,
|
||||
sx_css_classes=sx_css_classes,
|
||||
sx_js_hash=_script_hash("sx-browser.js"),
|
||||
body_js_hash=_script_hash("body.js"),
|
||||
)
|
||||
if head_scripts is not None:
|
||||
shell_kwargs["head_scripts"] = head_scripts
|
||||
if inline_css is not None:
|
||||
shell_kwargs["inline_css"] = inline_css
|
||||
if inline_head_js is not None:
|
||||
shell_kwargs["inline_head_js"] = inline_head_js
|
||||
if init_sx is not None:
|
||||
shell_kwargs["init_sx"] = init_sx
|
||||
if body_scripts is not None:
|
||||
shell_kwargs["body_scripts"] = body_scripts
|
||||
return await render_to_html("shared:shell/sx-page-shell", **shell_kwargs)
|
||||
kwargs = await _build_shell_kwargs(
|
||||
ctx, page_sx, meta_html=meta_html,
|
||||
head_scripts=head_scripts, inline_css=inline_css,
|
||||
inline_head_js=inline_head_js, init_sx=init_sx,
|
||||
body_scripts=body_scripts)
|
||||
kwargs["page_sx"] = page_sx
|
||||
return await render_to_html("shared:shell/sx-page-shell", **kwargs)
|
||||
|
||||
|
||||
_SX_STREAMING_RESOLVE = """\
|
||||
|
||||
Reference in New Issue
Block a user