diff --git a/selfhost/backend/emit_deps.ludic b/selfhost/backend/emit_deps.ludic index a8e1f09b..710093bc 100644 --- a/selfhost/backend/emit_deps.ludic +++ b/selfhost/backend/emit_deps.ludic @@ -304,6 +304,16 @@ function dr_walk(n: Node, refs: []int) -> void { return } if dr_reg_field(n, refs) { return } + if n.kind == S_EMIT and n.s != null { # 25.2: an emit reaches its event's listeners + var li = 0 + while li < len(g_onlisten) { + if (g_onlisten[li].s == n.s) { + let lk = dr_index(`@On {n.s}#{itoa(li)}`) + if lk >= 0 { push(refs, lk) } + } + li += 1 + } + } if (n.kind == E_ID or n.kind == E_FNREF) and n.s != null { let k = dr_index(n.s) if k >= 0 { push(refs, k) } @@ -430,7 +440,7 @@ function deps_reach(f: pointer) -> void { var i = 0 while i < len(prog) { let d = prog[i] - if d.s != null and (d.kind == N_FN or d.kind == N_VAR or d.kind == N_CONST) and ck_tab_get(g_dr_find_k, g_dr_find_v, d.s) == null { + if d.s != null and (d.kind == N_FN or d.kind == N_VAR or d.kind == N_CONST or d.kind == N_SYS) and ck_tab_get(g_dr_find_k, g_dr_find_v, d.s) == null { let h = new Node h.ival = len(g_dr_node) ck_tab_put(g_dr_find_k, g_dr_find_v, d.s, h) @@ -439,6 +449,17 @@ function deps_reach(f: pointer) -> void { } i += 1 } + # 25.2: each @On body is a node, reached from every `emit` of its event (dr_walk) + var li = 0 + while li < len(g_onlisten) { + let nm = `@On {g_onlisten[li].s}#{itoa(li)}` + let h = new Node + h.ival = len(g_dr_node) + ck_tab_put(g_dr_find_k, g_dr_find_v, nm, h) + push(g_dr_name, nm) + push(g_dr_node, g_onlisten[li]) + li += 1 + } let nw = dr_words() var j = 0 while j < len(g_dr_node) { @@ -493,4 +514,5 @@ function deps_reach(f: pointer) -> void { } j += 1 } + deps_frame(f) # 25.2: what a frame can come to allocate } diff --git a/selfhost/backend/emit_frame.ludic b/selfhost/backend/emit_frame.ludic new file mode 100644 index 00000000..810bd608 --- /dev/null +++ b/selfhost/backend/emit_frame.ludic @@ -0,0 +1,164 @@ +# emit_frame.ludic — 25.2: what a frame can come to allocate. The roots are the code that runs every +# frame - a handler in a frame phase, every reducer (the drain runs after each phase), an `@On` body, +# a function stored as a System's `tick`, and one stored in a field whose type marks it `@frame` +# (a step list) - and every allocating construct in what they reach through deps_reach's graph is a +# `falloc` line with the shortest chain from a root. A function under `@alloc_ok("why")` counts +# nothing and is not followed. ludic deps counts the lines (frame_allocs) and ratchets them. + +var g_alloc_ok_names: []pointer = new []pointer # functions and handlers under @alloc_ok("why") +var g_frame_fields: []pointer = new []pointer # record fields declared `@frame` +var g_fr_root: []bool = new []bool +var g_fr_from: []int = new []int # the node a reached one was first reached from + +function fr_phase_frame(ph: pointer) -> bool { + return (ph == "Input") or (ph == "FixedUpdate") or (ph == "Update") or (ph == "LateUpdate") or (ph == "Render") or (ph == "Overlay") +} + +function fr_is_frame_field(name: pointer) -> bool { + if (name == "tick") { return true } + var i = 0 + while i < len(g_frame_fields) { if (g_frame_fields[i] == name) { return true }; i += 1 } + return false +} + +function fr_alloc_ok(name: pointer) -> bool { + var i = 0 + while i < len(g_alloc_ok_names) { if (g_alloc_ok_names[i] == name) { return true }; i += 1 } + return false +} + +function fr_mark_root(name: pointer) -> void { + let k = dr_index(name) + if k >= 0 { g_fr_root[k] = true } +} + +# `x.tick = fn f`, `tick: fn f` in a record, and the same for an @frame field: f is a root +function fr_find_roots(n: Node) -> void { + if n == null { return } + if n.kind == S_ASSIGN and n.a != null and n.a.kind == E_MEMBER and n.a.s != null and fr_is_frame_field(n.a.s) and n.b != null and n.b.kind == E_FNREF { fr_mark_root(n.b.s) } + if n.kind == E_REC and n.kids != null { + var i = 0 + while i < len(n.kids) { + let f = n.kids[i] + if f.s != null and fr_is_frame_field(f.s) and f.a != null and f.a.kind == E_FNREF { fr_mark_root(f.a.s) } + i += 1 + } + } + fr_find_roots(n.a) + fr_find_roots(n.b) + fr_find_roots(n.c) + if n.kids != null { + var j = 0 + while j < len(n.kids) { fr_find_roots(n.kids[j]); j += 1 } + } +} + +function fr_is_textish(n: Node) -> bool { + if n == null { return false } + if n.kind == E_STR { return true } + if n.kind == E_CALL and n.a != null and n.a.kind == E_ID and (n.a.s == "string") { return true } + if n.kind == E_BIN and (n.s == "+") { return fr_is_textish(n.a) or fr_is_textish(n.b) } + return false +} + +function fr_alloc_kind(n: Node, text_above: bool) -> pointer { + if n.kind == E_NEW { return `new:{n.s}` } + if n.kind == E_LIST { return "list" } + if n.kind == E_BIN and (n.s == "+") and not text_above and fr_is_textish(n) { return "text" } + if n.kind == E_CALL and n.a != null and n.a.kind == E_ID and n.a.s != null { + let c = n.a.s + if (c == "push") { return "grow" } + if (c == "words") or (c == "floats") or (c == "doubles") or (c == "buffer") or (c == "bytes") or (c == "fixeds") or (c == "pointers") { return `{c}()` } + } + return null +} + +# the allocating constructs in one declaration, each a falloc line with its chain +function fr_allocs(f: pointer, n: Node, k: int, text_above: bool) -> int { + if n == null { return 0 } + var found = 0 + let kind = fr_alloc_kind(n, text_above) + if kind != null { + deps_line(f, `falloc {kind} {g_dr_node[k].file}:{itoa(n.line)} {fr_chain(k)}`) + found += 1 + } + let text = (n.kind == E_BIN) and (n.s == "+") and fr_is_textish(n) + found += fr_allocs(f, n.a, k, text) + found += fr_allocs(f, n.b, k, text) + found += fr_allocs(f, n.c, k, false) + if n.kids != null { + var i = 0 + while i < len(n.kids) { found += fr_allocs(f, n.kids[i], k, false); i += 1 } + } + return found +} + +# root > ... > this declaration, by the first way it was reached: the root, then the last six +function fr_chain(k: int) -> pointer { + var s = g_dr_name[k] + var at = g_fr_from[k] + var steps = 0 + var root = k + while at >= 0 { + if steps < 6 { s = `{g_dr_name[at]}>{s}` } + root = at + at = g_fr_from[at] + steps += 1 + } + if steps > 6 { s = `{g_dr_name[root]}>..>{s}` } + return s +} + +function deps_frame(f: pointer) -> void { + let n = len(g_dr_node) + g_fr_root = new []bool + g_fr_from = new []int + let seen = new []bool + var i = 0 + while i < n { push(g_fr_root, false); push(g_fr_from, -1); push(seen, false); i += 1 } + i = 0 + while i < len(prog) { + let d = prog[i] + if d.kind == N_SYS and d.s != null and fr_phase_frame(d.ty) { fr_mark_root(d.s) } + if d.kind == N_FN and d.s != null and str_starts(d.s, "ludic_reduce__") { fr_mark_root(d.s) } + fr_find_roots(d) + i += 1 + } + var j = 0 + while j < len(g_onlisten) { + fr_mark_root(`@On {g_onlisten[j].s}#{itoa(j)}`) + j += 1 + } + # breadth first from every root, so a chain is the shortest + let queue = new []int + i = 0 + while i < n { + if g_fr_root[i] and not fr_alloc_ok(g_dr_name[i]) { seen[i] = true; push(queue, i) } + i += 1 + } + var qh = 0 + while qh < len(queue) { + let k = queue[qh] + qh += 1 + let refs = g_dr_refs[k] + var r = 0 + while r < len(refs) { + let t = refs[r] + if not seen[t] and not fr_alloc_ok(g_dr_name[t]) { + seen[t] = true + g_fr_from[t] = k + push(queue, t) + } + r += 1 + } + } + var total = 0 + i = 0 + while i < len(queue) { + let k = queue[i] + let d = g_dr_node[k] + if (d.kind == N_FN or d.kind == N_SYS or d.kind == N_BLOCK) and d.file != null and not is_runtime_file(d.file) { total += fr_allocs(f, d.a, k, false) } + i += 1 + } + deps_line(f, `frame_allocs {itoa(total)}`) +} diff --git a/selfhost/frontend/parse.ludic b/selfhost/frontend/parse.ludic index ddafcaec..d0407832 100644 --- a/selfhost/frontend/parse.ludic +++ b/selfhost/frontend/parse.ludic @@ -925,6 +925,7 @@ function parse_one_decl() -> void { var esys_phase: pointer = null var ns_name: pointer = null # @Namespace(Name) — a package-provided Foo.* namespace (#62) var is_det = false # @deterministic — no floating point inside (emit_float.ludic) + var is_aok = false # @alloc_ok("why") — not counted by 25.2's frame allocations while is_op("@") { pi += 1; let a = eat_id() # collect a leading @annotation if a == "export" { is_export = true } @@ -953,6 +954,13 @@ function parse_one_decl() -> void { else if a == "EngineSystem" { eat_op("("); esys_comp = eat_id(); eat_op(","); esys_phase = eat_id(); eat_op(")") } # @EngineSystem(Comp, Phase) package engine system (#62) else if a == "Namespace" { eat_op("("); ns_name = eat_id(); eat_op(")") } # @Namespace(Name) package Foo.* namespace (#62) else if a == "deterministic" { is_det = true } + else if a == "alloc_ok" { # 25.2: @alloc_ok("why") - this allocates in a frame on purpose + eat_op("(") + let why = expr() + if why.kind != E_STR or len(why.s) == 0 { perr("@alloc_ok needs its reason: @alloc_ok(\"a memo miss, bounded by MM_CAP\")") } + eat_op(")") + is_aok = true + } else if a == "ClearColor" { # @ClearColor(colour) — Render auto-clear + auto-present (#86) eat_op("(") g_clear_color = expr() # a literal, a `const`, or a Color.Name — resolved when emitted @@ -1024,6 +1032,7 @@ function parse_one_decl() -> void { if is_id("handler") { let h = parse_system() if is_det { push(g_det_names, h.s) } + if is_aok { push(g_alloc_ok_names, h.s) } if (role != null) { if (role == "server") { h.ival = 1 } else { h.ival = 2 } } # N5: @Server=1 / @Predicted=2 if (on_event != null) { register_onlisten(on_event, h.a); return } # @On(Event) listener if (onspawn_model != null) { @@ -1094,6 +1103,7 @@ function parse_one_decl() -> void { let f = parse_fn() if is_unsafe { f.uns = 1 } if is_det { push(g_det_names, f.s) } + if is_aok { push(g_alloc_ok_names, f.s) } if is_export { f.ival = 1 } if (sys_phase != null) { push(g_mod_sys_fn, f.s); push(g_mod_sys_phase, sys_phase) } # #64: register at load if (esys_comp != null) { push(g_esys_comp, esys_comp); push(g_esys_fn, f.s); push(g_esys_phase, esys_phase) } # #62: package engine system diff --git a/tools/ludic-cli/deps.ludic b/tools/ludic-cli/deps.ludic index c2c28b8b..d00831b5 100644 --- a/tools/ludic-cli/deps.ludic +++ b/tools/ludic-cli/deps.ludic @@ -16,6 +16,7 @@ # ludic deps --widest N the N functions that take the most states, and what each reaches # ludic deps --reach N the same, the N that reach the most # ludic deps --wreach N the N that can come to CHANGE the most states +# ludic deps --allocs every allocation a frame can come to (25.2), with its chain from a root # # dependencies: module pairs (a, b) where a uses something of b, not counting b when b uses no # other module (a leaf); largest_cycle: the largest set of modules that all reach each other, not @@ -48,6 +49,8 @@ var dp_fn_at: []pointer = null # each of the program's own functions: whe var dp_fn_w: []int = null var dp_fn_r: []int = null var dp_fn_x: []int = null # ... and the states it can come to change +var dp_fallocs: []pointer = new []pointer # 25.2: `falloc `, what a frame can allocate +var dp_frame_allocs: int = 0 var dp_wreach_n: int = 0 var dp_wreach_at: pointer = "" @@ -86,6 +89,7 @@ function dp_load(path: pointer) -> bool { dp_reach_n = 0; dp_reach_at = ""; dp_reach_own = 0 dp_fn_at = new []pointer; dp_fn_w = new []int; dp_fn_r = new []int; dp_fn_x = new []int dp_wreach_n = 0; dp_wreach_at = "" + dp_fallocs = new []pointer; dp_frame_allocs = 0 dp_writes = new []pointer; dp_aliases = new []pointer; dp_wowner = new []pointer; dp_wname = new []pointer; dp_wfrom = new []pointer let lines = split_lines(text) for i in 0 .. len(lines) { @@ -107,6 +111,8 @@ function dp_load(path: pointer) -> bool { push(dp_en, w[4]) } if len(w) >= 6 and w[0] == "alias" { push(dp_aliases, lines[i]) } + if len(w) >= 4 and w[0] == "falloc" { push(dp_fallocs, lines[i]) } + if len(w) == 2 and w[0] == "frame_allocs" { dp_frame_allocs = s_to_int(w[1]) } if len(w) >= 5 and w[0] == "width" and w[4] == "0" and s_to_int(w[1]) > dp_wide_n { dp_wide_n = s_to_int(w[1]) dp_wide_at = `{dp_fn_name(w[2])} ({w[3]})` @@ -306,7 +312,7 @@ var dp_vals: []int = null var dp_cycle: []int = null var dp_cycle_all: []int = null # the same with the layers' own edges counted function dp_numbers() -> void { - dp_names = ["modules", "dependencies", "largest_cycle", "cross_writes", "globals_written_from_outside", "widest_function", "widest_reach", "widest_write_reach"] + dp_names = ["modules", "dependencies", "largest_cycle", "cross_writes", "globals_written_from_outside", "widest_function", "widest_reach", "widest_write_reach", "frame_allocs"] var mods = 0 for k in 0 .. len(dp_mods) { if dp_own(k) { mods += 1 } } var deps = 0 @@ -328,7 +334,7 @@ function dp_numbers() -> void { } } dp_reach_best() - dp_vals = [mods, deps, len(dp_cycle), writes, len(seen), dp_wide_n, dp_reach_n, dp_wreach_n] + dp_vals = [mods, deps, len(dp_cycle), writes, len(seen), dp_wide_n, dp_reach_n, dp_wreach_n, dp_frame_allocs] } # ---- the command -------------------------------------------------------------- @@ -482,6 +488,7 @@ function cmd_deps() -> int { if a == "--dot" { mode = "dot" } else if a == "--graph" { mode = "graph" } else if a == "--writes" { mode = "writes" } + else if a == "--allocs" { mode = "allocs" } else if a == "--uses" or a == "--check" or a == "--baseline" or a == "--widest" or a == "--reach" or a == "--wreach" { if ai + 1 >= arg_count() { err(`ludic deps: {a} needs an argument\n`); return 2 } mode = sslice(a, 2, slen(a)) @@ -523,6 +530,14 @@ function cmd_deps() -> int { return 0 } if mode == "uses" { dp_uses_of(arg2); return 0 } + if mode == "allocs" { # 25.2: every allocation a frame can come to, with its chain + for a in 0 .. len(dp_fallocs) { + let w = dp_words(dp_fallocs[a]) + print(`frame alloc: {w[2]} {w[1]} {w[3]}`) + } + print(`frame_allocs: {string(dp_frame_allocs)}`) + return 0 + } if mode == "widest" { dp_top(s_to_int(arg2), 0); return 0 } if mode == "reach" { dp_top(s_to_int(arg2), 1); return 0 } if mode == "wreach" { dp_top(s_to_int(arg2), 2); return 0 } diff --git a/tools/ludic-cli/selfhost.ludic b/tools/ludic-cli/selfhost.ludic index ca518737..1f698ded 100644 --- a/tools/ludic-cli/selfhost.ludic +++ b/tools/ludic-cli/selfhost.ludic @@ -44,6 +44,7 @@ function selfhost_frags() -> []pointer { push(f, "selfhost/backend/emit_core.ludic") push(f, "selfhost/backend/emit_fence.ludic") push(f, "selfhost/backend/emit_fence_ir.ludic") + push(f, "selfhost/backend/emit_frame.ludic") push(f, "selfhost/backend/emit_head.ludic") push(f, "selfhost/backend/emit_deps.ludic") push(f, "selfhost/backend/emit_addr.ludic")