swpick.bend

cswap-bend-cli@0.1.0.0 · 261 lines · 40 definitions

1import Base
2import cswap-bend-cli@0.1.0.0/json.bend as Json
3import cswap-bend-cli@0.1.0.0/store/seq.bend as SSeq
4
5# Switch, part 3: choosing the target and reading usage headroom (pure).
6
7# One rotation candidate. `headroom` is 100 minus the busiest window, in
8# hundredths of a percent (None when no usage is known).
9
10type Cand is Data:
11  Cand{slot: String, disabled: Bool, switchable: Bool, headroom: Maybe<&2, U32>}
12
13# The first candidate to switch to, with a warning for each one skipped on the way.
14type Sel is Data:
15  SelOk{slot: String, warns: List<&2, String>}
16  SelNone{warns: List<&2, String>, exhausted: Bool}
17
18type Pick is Data:
19  Pick{slot: String, h: U32, found: Bool}
20
21type BestOut is Data:
22  BPick{slot: String}
23  BNote{note: String}
24
25def skip_with(+w: String, +ex: Bool, +r: Sel) -> Sel:
26  match r:
27    case SelOk{s, ws}:
28      SelOk{s, w <> ws}
29    case SelNone{ws, e}:
30      SelNone{w <> ws, e || ex}
31
32def at_limit(+h: Maybe<&2, U32>) -> Bool:
33  match h:
34    case Some{x}:
35      U32.is_eq(x, 0)
36    case None{}:
37      False{}
38
39def sel_step2(+dis: Bool, +sw: Bool, +limit: Bool, +slot: String, +r: Sel) -> Sel:
40  match dis sw limit:
41    case True{} _ _:
42      skip_with("Skipped Account-" ++ slot ++ " (disabled)", False{}, r)
43    case False{} False{} _:
44      skip_with("Skipped Account-" ++ slot ++ " (no stored credentials/config)", False{}, r)
45    case False{} True{} True{}:
46      skip_with("Skipped Account-" ++ slot ++ " (at 5h/7d limit)", True{}, r)
47    case False{} True{} False{}:
48      SelOk{slot, []}
49
50def sel_step(+avail: Bool, +c: Cand, +r: Sel) -> Sel:
51  match c:
52    case Cand{slot, dis, sw, hr}:
53      sel_step2(dis, sw, avail && at_limit(hr), slot, r)
54
55# Walk `cs` (already in rotation order, current slot excluded) as switch() does.
56# `avail` is next-available: skip accounts with no headroom.
57def select_next(+cs: List<&2, Cand>, +avail: Bool) -> Sel:
58  match cs:
59    case Nil{}:
60      SelNone{[], False{}}
61    case Con{c, rest}:
62      sel_step(avail, c, select_next(rest, avail))
63
64def sel_n(+c: Bool, +x: Nat, +y: Nat) -> Nat:
65  match c:
66    case True{}:
67      x
68    case False{}:
69      y
70
71def index_of(+xs: List<&2, String>, +x: String, +i: Nat) -> Nat:
72  match xs:
73    case Nil{}:
74      0n
75    case Con{h, t}:
76      sel_n(String.eq(h, x), i, index_of(t, x, 1n+i))
77
78def rotated2(+xs: List<&2, String>, +idx: Nat) -> List<&2, String>:
79  List.append(&2, String, List.drop(&2, String, xs, 1n+idx), List.take(&2, String, xs, idx))
80
81# The other slots in rotation order after `anchor`: offsets 1..n-1 of the
82# cyclic sequence. An unknown anchor counts as index 0, as in the reference.
83def rotated(+xs: List<&2, String>, +anchor: Maybe<&2, String>) -> List<&2, String>:
84  rotated2(xs, index_of(xs, SSeq.str_or(anchor, ""), 0n))
85
86def better(+found: Bool, +h: U32, +cur: Pick) -> Bool:
87  match cur:
88    case Pick{s, ch, f}:
89      Bool.not(f) || U32.is_gt(h, ch)
90
91def sel_pick(+c: Bool, +x: Pick, +y: Pick) -> Pick:
92  match c:
93    case True{}:
94      x
95    case False{}:
96      y
97
98def best_known(+cs: List<&2, Cand>, +cur: Pick) -> Pick:
99  match cs:
100    case Nil{}:
101      cur
102    case Con{Cand{slot, d, sw, Some{h}}, t}:
103      best_known(t, sel_pick(better(True{}, h, cur), Pick{slot, h, True{}}, cur))
104    case Con{c, t}:
105      best_known(t, cur)
106
107def any_unknown(+cs: List<&2, Cand>) -> Bool:
108  match cs:
109    case Nil{}:
110      False{}
111    case Con{Cand{s, d, sw, None{}}, t}:
112      True{}
113    case Con{c, t}:
114      any_unknown(t)
115
116def best4(+wins: Bool, +s: String, +unknown: Bool, +exhausted: Bool) -> BestOut:
117  match wins unknown exhausted:
118    case True{} _ _:
119      BPick{s}
120    case False{} True{} _:
121      BNote{"incomplete-comparison"}
122    case False{} False{} True{}:
123      BNote{"exhausted"}
124    case False{} False{} False{}:
125      BNote{"stay"}
126
127def best3(+p: Pick, +cur: U32, +unknown: Bool) -> BestOut:
128  match p:
129    case Pick{s, h, f}:
130      best4(U32.is_gt(h, cur), s, unknown, U32.is_eq(cur, 0))
131
132def best2(+cur: Maybe<&2, U32>, +others: List<&2, Cand>, +p: Pick) -> BestOut:
133  match cur p:
134    case None{} _:
135      BNote{"current-unavailable"}
136    case Some{c} Pick{s, h, False{}}:
137      BNote{"no-comparison"}
138    case Some{c} Pick{s, h, True{}}:
139      best3(Pick{s, h, True{}}, c, any_unknown(others))
140
141# `_select_best_switchable`: `others` are the switchable, enabled slots but the
142# current one. Switch only to strictly more headroom.
143def select_best(+cur: Maybe<&2, U32>, +others: List<&2, Cand>) -> BestOut:
144  match others:
145    case Nil{}:
146      BNote{"none"}
147    case Con{o, t}:
148      best2(cur, Con{o, t}, best_known(Con{o, t}, Pick{"", 0, False{}}))
149
150def pct2(+parts: List<&2, String>) -> U32:
151  match parts:
152    case Con{i, Con{f, Nil{}}}:
153      (SSeq.str_u32(i) * 100 + SSeq.str_u32(String.take(f ++ "00", 2n)) : U32)
154    case Con{i, Nil{}}:
155      (SSeq.str_u32(i) * 100 : U32)
156    case _:
157      0
158
159# "90.0" -> 9000, "12.34" -> 1234 (hundredths of a percent).
160def pct_hundredths(+t: String) -> U32:
161  pct2(String.split(t, '.'))
162
163def pct_of2(+m: Maybe<&2, Json.Json>) -> Maybe<&2, U32>:
164  match m:
165    case Some{Json.JNum{t}}:
166      Some{pct_hundredths(t)}
167    case _:
168      None{}
169
170def pct_of(+w: Json.Json) -> Maybe<&2, U32>:
171  pct_of2(Json.get("pct", w))
172
173def push_pct(+m: Maybe<&2, U32>, +rest: List<&2, U32>) -> List<&2, U32>:
174  match m:
175    case Some{p}:
176      p <> rest
177    case None{}:
178      rest
179
180def lower_all(+xs: List<&2, String>) -> List<&2, String>:
181  match xs:
182    case Nil{}:
183      []
184    case Con{h, t}:
185      String.to_lower(h) <> lower_all(t)
186
187def mem(+x: String, +xs: List<&2, String>) -> Bool:
188  match xs:
189    case Nil{}:
190      False{}
191    case Con{h, t}:
192      String.eq(h, x) || mem(x, t)
193
194def scoped_pick(+hit: Bool, +m: Maybe<&2, U32>, +rest: List<&2, U32>) -> List<&2, U32>:
195  match hit:
196    case True{}:
197      push_pct(m, rest)
198    case False{}:
199      rest
200
201def scoped_pcts(+items: Json.Json, +wanted: List<&2, String>) -> List<&2, U32>:
202  match items:
203    case Json.JCons{s, t}:
204      scoped_pick(mem("all", wanted) || mem(String.to_lower(SSeq.field_str(s, "name")), wanted), pct_of(s), scoped_pcts(t, wanted))
205    case _:
206      []
207
208def window_pct2(+m: Maybe<&2, Json.Json>) -> Maybe<&2, U32>:
209  match m:
210    case Some{w}:
211      pct_of(w)
212    case None{}:
213      None{}
214
215def window_pct(+row: Json.Json, +key: String) -> Maybe<&2, U32>:
216  window_pct2(Json.get(key, row))
217
218def sel_l(+c: Bool, +x: List<&2, U32>, +y: List<&2, U32>) -> List<&2, U32>:
219  match c:
220    case True{}:
221      x
222    case False{}:
223      y
224
225# The percentages `relevant_windows` looks at: 5h, 7d and, with models, the
226# scoped windows named by them (or all of them for "all").
227def pcts(+row: Json.Json, +models: List<&2, String>) -> List<&2, U32>:
228  push_pct(window_pct(row, "five_hour"), push_pct(window_pct(row, "seven_day"),
229    sel_l(List.is_empty(&2, String, models), [], scoped_pcts(SSeq.unwrap_arr(SSeq.get_or("scoped", row, Json.JNull{})), lower_all(models)))))
230
231def max_pct(+xs: List<&2, U32>, +acc: U32) -> U32:
232  match xs:
233    case Nil{}:
234      acc
235    case Con{h, t}:
236      max_pct(t, SSeq.max_u32(acc, h))
237
238def min_u32(+a: U32, +b: U32) -> U32:
239  SSeq.sel.u(U32.is_lt(a, b), a, b)
240
241def headroom2(+xs: List<&2, U32>) -> Maybe<&2, U32>:
242  match xs:
243    case Nil{}:
244      None{}
245    case Con{h, t}:
246      Some{(10000 - min_u32(10000, max_pct(Con{h, t}, 0)) : U32)}
247
248# 100 - the busiest relevant window, from a usage row's `lastGood`.
249def headroom(+row: Json.Json, +models: List<&2, String>) -> Maybe<&2, U32>:
250  headroom2(pcts(row, models))
251
252# usage.json -> the lastGood object of a slot.
253def usage_row(+usage: Json.Json, +slot: String) -> Json.Json:
254  SSeq.get_or("lastGood", SSeq.get_or(slot, SSeq.accounts_of(usage), Json.JNull{}), Json.JNull{})
255
256def sel_lc(+c: Bool, +x: List<&2, Cand>, +y: List<&2, Cand>) -> List<&2, Cand>:
257  match c:
258    case True{}:
259      x
260    case False{}:
261      y