From 9539f7bcec6affeed8a8e24e3f0e716d20ac3c57 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sun, 27 Sep 2026 22:17:27 +0300 Subject: [PATCH] perf(json): 23.2 - a parsed tree can be let go (Value.free / Json.free: its nodes and lists, not its strings), a string is parsed in one allocation instead of one per character, the parser's cursor is freed, and gltf_load frees the last file's tree and the text it parsed 300 parses of a 446 KB glTF: 2118 MB before, 528 MB with the one-allocation string, 94 MB freed. A headless Maroon Lake at play: 2334 -> 2195 MB by footprint. Co-Authored-By: Claude Opus 5.5 --- packages/ludic.render3d/gltf.ludic | 4 ++++ runtime/native/namespaces.ludic | 2 ++ runtime/native/value.ludic | 38 ++++++++++++++++++++++++++++-- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/ludic.render3d/gltf.ludic b/packages/ludic.render3d/gltf.ludic index c991972a..2862f985 100644 --- a/packages/ludic.render3d/gltf.ludic +++ b/packages/ludic.render3d/gltf.ludic @@ -163,7 +163,11 @@ function gltf_load(render3d_st: mut Render3dState, dir: string, file: string, no render3d_st.gltf_dir = dir let text = Fs.read_text(dir + "/" + file) if text == null { print(`gltf: cannot read {dir}/{file}`); return null } + # the last file's tree goes first (it is read only until the next load: ludic.anim's read.ludic), + # and the text once parsed - nothing gives memory back on its own (plan 23 of maroon-lake) + if render3d_st.gltf_doc != null { Json.free(render3d_st.gltf_doc) } render3d_st.gltf_doc = Json.parse(text) + free(text) let buffers = value_get(render3d_st.gltf_doc, "buffers") let bin_uri = value_as_str(value_get(value_at(buffers, 0), "uri")) render3d_st.gltf_bin = file_open(dir + "/" + bin_uri, "rb") diff --git a/runtime/native/namespaces.ludic b/runtime/native/namespaces.ludic index 7c24ea21..3f8627e4 100644 --- a/runtime/native/namespaces.ludic +++ b/runtime/native/namespaces.ludic @@ -478,10 +478,12 @@ namespace Value { alias as_int(value) = value_as_int alias as_float(value) = value_as_float alias as_str(value) = value_as_str + alias free(value) = value_free } namespace Json { alias encode(value) = json_encode alias parse(text) = json_parse + alias free(value) = json_free } namespace Xml { alias parse(text) = xml_parse diff --git a/runtime/native/value.ludic b/runtime/native/value.ludic index dab65b28..cc3e4c0b 100644 --- a/runtime/native/value.ludic +++ b/runtime/native/value.ludic @@ -166,9 +166,23 @@ function jp_skip_ws(p: JP) -> void { function json_parse(s: pointer) -> Val { let p = new JP p.s = s; p.i = 0; p.n = len(s) - return jp_value(p) + let v = jp_value(p) + free(p) + return v } +# Let a parsed tree go once what it describes is built: every node and its lists. Its strings stay - +# a caller may keep a name it read out of the tree - and a tree put together by hand, which may share +# a node or hold a literal, is not for this (Ludic has no collector; plan 23 of maroon-lake). +function value_free(v: Val) -> void { + if v == null { return } + for i in 0 .. len(v.kids) { value_free(v.kids[i]) } + free(v.kids) + free(v.keys) + free(v) +} +function json_free(v: Val) -> void { value_free(v) } + function jp_value(p: JP) -> Val { jp_skip_ws(p) if p.i >= p.n { return value_null() } @@ -182,9 +196,29 @@ function jp_value(p: JP) -> Val { return jp_number(p) } -# read a quoted string (cursor on the opening quote) -> the unescaped contents. +# read a quoted string (cursor on the opening quote) -> the unescaped contents. One allocation for a +# string with no escape in it: built a character at a time, every step was a string never freed function jp_string(p: JP) -> string { p.i += 1 # skip opening quote + let a = p.i + while p.i < p.n { + let c = p.s[p.i] + if c == '"' { + let out = p.s[a..p.i] + p.i += 1 + return out + } + if c == '\\' { + p.i = a + return jp_string_esc(p) + } + p.i += 1 + } + return p.s[a..p.n] +} + +# the same with escapes in it, the rare case, a character at a time +function jp_string_esc(p: JP) -> string { var out = "" while p.i < p.n { let c = p.s[p.i]