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 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-27 22:17:27 +03:00
parent 537d3a0ab3
commit 9539f7bcec
3 changed files with 42 additions and 2 deletions

View file

@ -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

View file

@ -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]