Skip to content

Commit fe60190

Browse files
author
i2cjak
authored
Merge pull request #212 from i2cjak/backplane/visible-cat-cache
perf(window): cache cats requested by visible layouts
2 parents 04ea833 + 8dd8bc0 commit fe60190

7 files changed

Lines changed: 320 additions & 2 deletions

File tree

‎LAWS.bend‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import ./src/core/semver.bend as SV
5353
import ./src/core/clipath.bend as CPA
5454
import ./src/app/nui.bend as N
5555
import ./src/app/bots.bend as BV
56+
import ./src/app/cats.bend as VCAT
5657
import ./src/core/sexpr.bend as KSEX # kicad fork footprint placement
5758
import ./src/core/board.bend as B
5859
import ./src/core/hub.bend as H
@@ -10592,3 +10593,12 @@ law sidebar_row_paint_exact:
1059210593
for +b: SRT.SideRowK
1059310594
for h: {SR.Key.same(a, b) == True{} : Bool}
1059410595
{SR.Key.paint(ty, a) == SR.Key.paint(ty, b) : L.Out}
10596+
# Visible cat requests are additional to sidebar/header warm-up.
10597+
law visible_cats_empty_preserves_cache:
10598+
for +c: Map<&2, VCAT.CatF>
10599+
for +clock: Nat
10600+
{BV.Spot.warm(Nil{}, c, clock) == c : Map<&2, VCAT.CatF>}
10601+
10602+
law visible_sleep_cat_posed_once:
10603+
for +clock: Nat
10604+
{BV.Bv.warm.at("sleep", clock) == 0n : Nat}

‎PROOF.bend‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12307,3 +12307,9 @@ def Laws.sidebar_row_key_sound(a, b, h):
1230712307
def Laws.sidebar_row_paint_exact(ty, a, b, h):
1230812308
%Laws.sidebar_row_key_sound(a, b, h) : {SR.Key.paint(ty, a) == SR.Key.paint(ty, _) : L.Out}
1230912309
{==}
12310+
12311+
def Laws.visible_cats_empty_preserves_cache(c, clock):
12312+
{==}
12313+
12314+
def Laws.visible_sleep_cat_posed_once(clock):
12315+
{==}

‎src/app/bots.bend‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,14 @@ def Bv.warm.sel(sel: Bool, c: Map<&2, CA.CatF>, +look: Nat, +mood: String, +cloc
446446
def Bv.warm.at(+mood: String, +clock: Nat) -> Nat:
447447
Bool.pick(Nat, CA.Cats.moving(mood), clock, 0n)
448448

449+
# Layouts may request a mood/size beyond the sidebar and selected header.
450+
# Warm exactly those visible requests and retain them for subsequent frames.
451+
def Spot.warm(sps: List<&2, Spot>, c: Map<&2, CA.CatF>, +clock: Nat) -> Map<&2, CA.CatF>:
452+
match sps:
453+
case Nil{}: c
454+
case Spot{look, +mood, px, x, y, _} <> t:
455+
Spot.warm(t, CA.Cats.visible(c, look, mood, px, Bv.warm.at(mood, clock), x, y), clock)
456+
449457
# the cats on show, posed into the cache at the clock before a frame
450458
def Bv.warm.go(rs: List<&2, BU.BRow>, c: Map<&2, CA.CatF>, +clock: Nat) -> Map<&2, CA.CatF>:
451459
match rs:

‎src/app/cats.bend‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import ../gfx/draw.bend as D
1717
# px square at (Cats.at(), Cats.at()); ok is False for "not in the cache"
1818
type CatF is Data:
1919
CatF{ok: Bool, parts: List<&2, C.FPart>, clock: Nat, ops: List<&2, R.Op>}
20+
CatAt{parts: List<&2, C.FPart>, clock: Nat, ops: List<&2, R.Op>, x: U32, y: U32}
2021

2122
def Cats.none() -> Map<&2, CatF>:
2223
Map.new(&2, CatF)
@@ -59,6 +60,8 @@ def Cats.parts.k(f: CatF, +look: Nat, +mood: String) -> List<&2, C.FPart>:
5960
ps
6061
case CatF{False{}, _, _, _}:
6162
C.Cat.flat(look, mood)
63+
case CatAt{ps, _, _, _, _}:
64+
ps
6265

6366
def Cats.posed(+ps: List<&2, C.FPart>, +clock: Nat, +px: U32) -> CatF:
6467
CatF{True{}, ps, clock, Cats.pose(ps, clock, D.Clip.all(), U32.to_f32(Cats.at()), U32.to_f32(Cats.at()), U32.to_f32(px))}
@@ -67,6 +70,8 @@ def Cats.fresh(f: CatF, +clock: Nat) -> Bool:
6770
match f:
6871
case CatF{+ok, _, +k, _}:
6972
Bool.and(ok, Nat.is_eq(k, clock))
73+
case CatAt{_, k, _, _, _}:
74+
Nat.is_eq(k, clock)
7075

7176
def Cats.warm.k(fresh: Bool, c: Map<&2, CatF>, f: CatF, +look: Nat, +mood: String, +px: U32, +clock: Nat) -> Map<&2, CatF>:
7277
match fresh:
@@ -79,6 +84,28 @@ def Cats.warm.k(fresh: Bool, c: Map<&2, CatF>, f: CatF, +look: Nat, +mood: Strin
7984
def Cats.warm(+c: Map<&2, CatF>, +look: Nat, +mood: String, +px: U32, +clock: Nat) -> Map<&2, CatF>:
8085
Cats.warm.k(Cats.fresh(Cats.find(c, look, mood, px), clock), c, Cats.find(c, look, mood, px), look, mood, px, clock)
8186

87+
# A newly requested size is posed at its actual coordinates: posing at
88+
# 1000 then translating can round small coverage masks differently. There
89+
# is still only one entry per look/mood/size, even when the cat moves.
90+
def Cats.placed(+ps: List<&2, C.FPart>, +clock: Nat, +x: U32, +y: U32, +px: U32) -> CatF:
91+
CatAt{ps, clock, Cats.pose(ps, clock, D.Clip.all(), U32.to_f32(x), U32.to_f32(y), U32.to_f32(px)), x, y}
92+
93+
def Cats.visible.fresh(f: CatF, +clock: Nat, +x: U32, +y: U32) -> Bool:
94+
match f:
95+
case CatF{+ok, _, k, _}:
96+
Bool.and(ok, Nat.is_eq(k, clock))
97+
case CatAt{_, k, _, ox, oy}:
98+
Bool.and(Nat.is_eq(k, clock), Bool.and(U32.is_eq(x, ox), U32.is_eq(y, oy)))
99+
100+
def Cats.visible.keep(same: Bool, c: Map<&2, CatF>, f: CatF, +look: Nat, +mood: String, +px: U32, +clock: Nat, +x: U32, +y: U32) -> Map<&2, CatF>:
101+
match same:
102+
case True{}: c
103+
case False{}:
104+
Map.set(&2, CatF, c, Cats.key(look, mood, px), Cats.placed(Cats.parts.k(f, look, mood), clock, x, y, px))
105+
106+
def Cats.visible(+c: Map<&2, CatF>, +look: Nat, +mood: String, +px: U32, +clock: Nat, +x: U32, +y: U32) -> Map<&2, CatF>:
107+
Cats.visible.keep(Cats.visible.fresh(Cats.find(c, look, mood, px), clock, x, y), c, Cats.find(c, look, mood, px), look, mood, px, clock, x, y)
108+
82109
# Moving posed ops
83110
# ----------------
84111

@@ -116,12 +143,19 @@ def Cats.move(os: List<&2, R.Op>, +c: D.Clip, +ix: U32, +iy: U32, +dx: F32, +dy:
116143
case R.Op{x0, y0, x1, y1, +col, +a, s} <> t:
117144
D.Clip.cut(c, (x0 + ix : U32), (y0 + iy : U32), (x1 + ix : U32), (y1 + iy : U32), col, a, Cats.shape(s, ix, iy, dx, dy)) <> Cats.move(t, c, ix, iy, dx, dy)
118145

146+
def Cats.draw.at(same: Bool, os: List<&2, R.Op>, ps: List<&2, C.FPart>, +clock: Nat, +c: D.Clip, +x: U32, +y: U32, +px: U32) -> List<&2, R.Op>:
147+
match same:
148+
case True{}: Cats.move(os, c, 0, 0, 0.0, 0.0)
149+
case False{}: Cats.pose(ps, clock, c, U32.to_f32(x), U32.to_f32(y), U32.to_f32(px))
150+
119151
def Cats.draw.k(f: CatF, +look: Nat, +mood: String, +ms: Nat, +c: D.Clip, +x: U32, +y: U32, +px: U32) -> List<&2, R.Op>:
120152
match f:
121153
case CatF{True{}, _, _, os}:
122154
Cats.move(os, c, (x - Cats.at() : U32), (y - Cats.at() : U32), F32.sub(U32.to_f32(x), U32.to_f32(Cats.at())), F32.sub(U32.to_f32(y), U32.to_f32(Cats.at())))
123155
case CatF{False{}, _, _, _}:
124156
Cats.pose(C.Cat.flat(look, mood), ms, c, U32.to_f32(x), U32.to_f32(y), U32.to_f32(px))
157+
case CatAt{ps, clock, os, ox, oy}:
158+
Cats.draw.at(Bool.and(U32.is_eq(x, ox), U32.is_eq(y, oy)), os, ps, clock, c, x, y, px)
125159

126160
# a cat drawn from the cache (flattened and posed now, at ms, when the
127161
# cache lacks it)

‎src/app/main.bend‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2482,7 +2482,8 @@ def Shell.paint.fr(fr: BV.Fronted, +hs: List<&2, N.Hit>, +oh: List<&2, N.Hit>, +
24822482
def Shell.paint.out(o: L.Out, +oh: List<&2, N.Hit>, +n: N.Nui, w: Window, +q: Chan(ShellMsg), +i: Chan(S.Msg), +wa: Aux, +epoch: Nat, +ms: Nat, +t0: U32, +t2: U32) -> IO(Shell):
24832483
match o:
24842484
case L.Out{+ops, +hs}:
2485-
Shell.paint.fr(BV.Fronted.of(ops, hs, Aux.cats(wa), N.Nui.ms_of(n)), hs, oh, n, w, q, i, wa, epoch, ms, t0, t2)
2485+
+wc : Aux = Aux.with_cats(wa, BV.Spot.warm(BV.Spot.all(hs), Aux.cats(wa), BV.Bv.clock.of(n)))
2486+
Shell.paint.fr(BV.Fronted.of(ops, hs, Aux.cats(wc), N.Nui.ms_of(n)), hs, oh, n, w, q, i, wc, epoch, ms, t0, t2)
24862487

24872488
# how many ops a layout made (counting them also finishes the layout)
24882489
def Lay.nops(o: L.Out) -> Nat:
@@ -2808,7 +2809,7 @@ def Shell.tick.shown(r: Window & Image & Image, +n: N.Nui, +q: Chan(ShellMsg), +
28082809
def Shell.tick.fast(+sps: List<&2, BV.Spot>, +cs: List<&2, R.Op>, +n: N.Nui, w: Window, +q: Chan(ShellMsg), +i: Chan(S.Msg), +wa: Aux, +ms: Nat, +t0: U32) -> IO(Shell):
28092810
do IO<Shell>:
28102811
+n2 : N.Nui = BV.Bv.stamp(n, ms)
2811-
+cache : Map<&2, CA.CatF> = BV.Bv.warm(N.Nui.ui(n2), Aux.cats(wa), BV.Bv.clock.of(n2))
2812+
+cache : Map<&2, CA.CatF> = BV.Spot.warm(sps, BV.Bv.warm(N.Nui.ui(n2), Aux.cats(wa), BV.Bv.clock.of(n2)), BV.Bv.clock.of(n2))
28122813
+cs2 : List<&2, R.Op> = L.Theme.ops(V.Look.ui(N.Nui.look(n2)), BV.Spot.all.ops(sps, cache, N.Nui.ms_of(n2)))
28132814
+f : TI.Frames = TI.Frames.recat(Aux.frames(wa), cs, cs2)
28142815
+img : Image = Frames.image(f)

‎test/native/visible_cat_probe.bend‎

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import Base
2+
import ../../src/app/bots.bend as B
3+
import ../../src/app/cats.bend as C
4+
import ../../src/app/nui.bend as N
5+
import ../../src/gfx/draw.bend as D
6+
import ../../src/gfx/raster.bend as R
7+
import ../../src/gfx/text.bend as X
8+
import ../../src/server/perf.bend as P
9+
10+
# Same-binary ABBA: original cold sleep24 requests versus retained visible cache.
11+
# Synthetic component attribution, not a Mailroom or application benchmark.
12+
# No hub, provider, window, private log, or file writes. Cached and uncached
13+
# front preparation are timed separately over identical visible geometry.
14+
15+
def clip() -> D.Clip:
16+
D.Clip{0, 0, 1400, 900}
17+
18+
def y(+i: Nat) -> U32:
19+
(70 + U32.from_nat(Nat.mul(i, 36n)) : U32)
20+
21+
def spots(left: Nat, +i: Nat) -> List<&2, B.Spot>:
22+
match left:
23+
case 0n: Nil{}
24+
case 1n+k:
25+
B.Spot{Nat.add(12345n, i), "sleep", 24, 20, y(i), clip()} <> spots(k, 1n+i)
26+
27+
def anchors(ss: List<&2, B.Spot>) -> List<&2, R.Op>:
28+
match ss:
29+
case Nil{}: Nil{}
30+
case B.Spot{_, _, px, x, y, c} <> t:
31+
B.Spot.anchor(c, x, y, px) <> anchors(t)
32+
33+
def hits(ss: List<&2, B.Spot>) -> List<&2, N.Hit>:
34+
match ss:
35+
case Nil{}: Nil{}
36+
case B.Spot{lk, md, px, x, y, c} <> t:
37+
B.Spot.hit(lk, md, c, x, y, px) <> hits(t)
38+
39+
def rectangles(left: Nat, +i: Nat) -> List<&2, R.Op>:
40+
match left:
41+
case 0n: Nil{}
42+
case 1n+k:
43+
D.Draw.rect(clip(), (400 + U32.from_nat(Nat.mod(i, 500n)) : U32), (100 + U32.from_nat(Nat.mod(i, 600n)) : U32), 2, 2, 100, 255) <> rectangles(k, 1n+i)
44+
45+
def warm(ss: List<&2, B.Spot>, c: Map<&2, C.CatF>) -> Map<&2, C.CatF>:
46+
B.Spot.warm(ss, c, 0n)
47+
48+
type Fixture is Data:
49+
Fixture{ops: List<&2, R.Op>, hits: List<&2, N.Hit>, spots: List<&2, B.Spot>, caches: List<&2, Map<&2, C.CatF>>}
50+
51+
def cache(xs: List<&2, Map<&2, C.CatF>>) -> Map<&2, C.CatF>:
52+
match xs:
53+
case c <> _: c
54+
case Nil{}: C.Cats.none()
55+
56+
def cover(on: Bool, ops: List<&2, R.Op>) -> List<&2, R.Op>:
57+
match on:
58+
case False{}: ops
59+
case True{}: D.Draw.rect(clip(), 0, 0, 80, 900, 200, 255) <> ops
60+
61+
def fixture(+n: Nat, +covered: Bool) -> Fixture:
62+
+ss : List<&2, B.Spot> = spots(n, 0n)
63+
Fixture{cover(covered, List.append(&2, R.Op, anchors(ss), rectangles(400n, 0n))), hits(ss), ss, [warm(ss, C.Cats.none())]}
64+
65+
def force_edges(es: List<&2, R.Edge>, acc: U32) -> Nat:
66+
match es:
67+
case Nil{}: U32.to_nat(acc)
68+
case R.Edge{ax, ay, bx, by} <> t:
69+
force_edges(t, (acc + F32.bits(ax) + F32.bits(ay) + F32.bits(bx) + F32.bits(by) : U32))
70+
71+
# Force the complete coverage bitmap as well as vector coordinates.
72+
def force_mask(m: R.Mask) -> U32:
73+
match m:
74+
case R.MWord{w}: w
75+
case R.MPair{a, b}: (force_mask(a) + force_mask(b) : U32)
76+
case R.M16{a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p}:
77+
(a + b + c + d + e + f + g + h + i + j + k + l + m + n + o + p : U32)
78+
79+
def force_shape(s: R.Shape) -> Nat:
80+
match s:
81+
case R.SPoly{_, es}: force_edges(es, 0)
82+
case R.SMask{_, _, _, _, m}: U32.to_nat(force_mask(m))
83+
case R.SSeg{ax, ay, bx, by, hw}: U32.to_nat((F32.bits(ax) + F32.bits(ay) + F32.bits(bx) + F32.bits(by) + F32.bits(hw) : U32))
84+
case R.SDisc{cx, cy, r}: U32.to_nat((F32.bits(cx) + F32.bits(cy) + F32.bits(r) : U32))
85+
case R.STri{ax, ay, bx, by, cx, cy}: U32.to_nat((F32.bits(ax) + F32.bits(ay) + F32.bits(bx) + F32.bits(by) + F32.bits(cx) + F32.bits(cy) : U32))
86+
case _: 1n
87+
88+
def force(ops: List<&2, R.Op>, +acc: Nat) -> Nat:
89+
match ops:
90+
case Nil{}: acc
91+
case R.Op{x0, y0, x1, y1, _, _, s} <> t:
92+
force(t, Nat.add(acc, Nat.add(force_shape(s), U32.to_nat((x0 + y0 + x1 + y1 : U32)))))
93+
94+
def force_boxes(bs: List<&2, B.Box>, +acc: Nat) -> Nat:
95+
match bs:
96+
case Nil{}: acc
97+
case B.Box{k, x0, y0, x1, y1} <> t:
98+
force_boxes(t, Nat.add(acc, U32.to_nat((k + x0 + y0 + x1 + y1 : U32))))
99+
100+
def force_front(fr: B.Fronted) -> Nat:
101+
match fr:
102+
case B.Fronted{os, _, _}: force(os, 0n)
103+
104+
# Each result, including polygon coordinates, is consumed inside the timed
105+
# region. Fixture/cache preparation stays outside it. No UI records cross IO.
106+
def evaluate(kind: Nat, f: Fixture) -> Nat:
107+
match kind f:
108+
case 0n Fixture{os, _, _, _}:
109+
List.length(&2, X.Want, X.Text.wants(os, Nil{}))
110+
case 1n Fixture{_, _, ss, cs}:
111+
force_boxes(B.Box.all(ss, cache(cs), 0n), 0n)
112+
case 2n Fixture{os, _, ss, cs}:
113+
Bool.pick(Nat, B.Spot.covered(os, B.Box.all(ss, cache(cs), 0n)), 1n, 0n)
114+
case 3n Fixture{_, _, ss, cs}:
115+
force(B.Spot.all.ops(ss, cache(cs), 0n), 0n)
116+
case 4n Fixture{os, _, ss, cs}:
117+
force(B.Spot.inline(os, ss, cache(cs), 0n), 0n)
118+
case 5n Fixture{os, +hs, _, cs}:
119+
force_front(B.Fronted.of(os, hs, B.Spot.warm(B.Spot.all(hs), cache(cs), 0n), 0n))
120+
case _ Fixture{os, hs, _, _}:
121+
force_front(B.Fronted.of(os, hs, C.Cats.none(), 0n))
122+
123+
def name(kind: Nat) -> String:
124+
match kind:
125+
case 0n: "wants"
126+
case 1n: "boxes.warm"
127+
case 2n: "covered.warm"
128+
case 3n: "cats.ops.warm"
129+
case 4n: "inline.warm"
130+
case 5n: "front.warm"
131+
case _: "front.cold"
132+
133+
def one(+kind: Nat, +fs: List<&2, Fixture>) -> IO(Nat):
134+
match fs:
135+
case Nil{}: IO.pure(Nat, 0n)
136+
case +f <> _:
137+
do IO<Nat>:
138+
+t0 : U32 <- P.Perf.us()
139+
P.Perf.add("probe.checksum", U32.from_nat(Nat.mod(evaluate(kind, f), 1000000n)))
140+
+t1 : U32 <- P.Perf.us()
141+
return U32.to_nat((t1 - t0 : U32))
142+
143+
def total(xs: List<&2, Nat>, acc: Nat) -> Nat:
144+
match xs:
145+
case Nil{}: acc
146+
case x <> t: total(t, Nat.add(acc, x))
147+
148+
def at(xs: List<&2, Nat>, i: Nat) -> Nat:
149+
Maybe.default(&2, Nat, List.get(&2, Nat, xs, i), 0n)
150+
151+
def report(kind: Nat, +xs: List<&2, Nat>) -> IO(Unit):
152+
+sorted : List<&2, Nat> = List.sort(~Nat, ~Nat.is_lt, xs)
153+
+n : Nat = List.length(&2, Nat, xs)
154+
IO.print(name(kind) ++ " n=" ++ Nat.show(n) ++ " total_us=" ++ Nat.show(total(xs, 0n)) ++ " p50_us=" ++ Nat.show(at(sorted, Nat.div(n, 2n))) ++ " max_us=" ++ Nat.show(at(sorted, Nat.sub(n, 1n))))
155+
156+
def reps(left: Nat, +kind: Nat, +fs: List<&2, Fixture>, times: List<&2, Nat>) -> IO(Unit):
157+
match left:
158+
case 0n: report(kind, times)
159+
case 1n+k: IO.bind(Nat, Unit, one(kind, fs), us => reps(k, kind, fs, us <> times))
160+
161+
def stages(ks: List<&2, Nat>, +fs: List<&2, Fixture>) -> IO(Unit):
162+
match ks:
163+
case Nil{}: IO.pure(Unit, Unit{})
164+
case +k <> t:
165+
do IO<Unit>:
166+
reps(12n, k, fs, Nil{})
167+
stages(t, fs)
168+
169+
def front_count(fr: B.Fronted) -> Nat:
170+
match fr:
171+
case B.Fronted{_, ss, _}: List.length(&2, B.Spot, ss)
172+
173+
def ready(+n: Nat, covered: Bool, +f: Fixture) -> Bool:
174+
match covered f:
175+
case +on Fixture{+os, +hs, _, +cs}:
176+
Bool.and(Nat.is_eq(front_count(B.Fronted.of(os, hs, cache(cs), 0n)), Bool.pick(Nat, on, 0n, n)), Nat.is_gt(List.length(&2, R.Op, os), 400n))
177+
178+
def scenario_run(ok: Bool, +fs: List<&2, Fixture>) -> IO(Unit):
179+
match ok:
180+
case False{}: IO.print("FAIL scenario geometry/coverage")
181+
case True{}:
182+
do IO<Unit>:
183+
IO.print("PASS scenario geometry/coverage")
184+
stages([6n, 5n, 5n, 6n], fs)
185+
186+
def scenario(+n: Nat, +covered: Bool) -> IO(Unit):
187+
+f : Fixture = fixture(n, covered)
188+
do IO<Unit>:
189+
IO.print("cats=" ++ Nat.show(n) ++ " covered=" ++ Bool.show(covered))
190+
scenario_run(ready(n, covered, f), [f])
191+
192+
def main() -> IO(Unit):
193+
do IO<Unit>:
194+
scenario(1n, False{})
195+
scenario(11n, False{})
196+
scenario(11n, True{})

0 commit comments

Comments
 (0)