1 import Base 2 import ./json.bend as Json 3 import ./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 10 type 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. 14 type Sel is Data: 15 SelOk{slot: String, warns: List<&2, String>} 16 SelNone{warns: List<&2, String>, exhausted: Bool} 17 18 type Pick is Data: 19 Pick{slot: String, h: U32, found: Bool} 20 21 type BestOut is Data: 22 BPick{slot: String} 23 BNote{note: String} 24 25 def 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 32 def 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 39 def 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 50 def 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. 57 def 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 64 def sel_n(+c: Bool, +x: Nat, +y: Nat) -> Nat: 65 match c: 66 case True{}: 67 x 68 case False{}: 69 y 70 71 def 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 78 def 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. 83 def rotated(+xs: List<&2, String>, +anchor: Maybe<&2, String>) -> List<&2, String>: 84 rotated2(xs, index_of(xs, SSeq.str_or(anchor, ""), 0n)) 85 86 def 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 91 def sel_pick(+c: Bool, +x: Pick, +y: Pick) -> Pick: 92 match c: 93 case True{}: 94 x 95 case False{}: 96 y 97 98 def 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 107 def 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 116 def 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 127 def 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 132 def 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. 143 def 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 150 def 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). 160 def pct_hundredths(+t: String) -> U32: 161 pct2(String.split(t, '.')) 162 163 def 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 170 def pct_of(+w: Json.Json) -> Maybe<&2, U32>: 171 pct_of2(Json.get("pct", w)) 172 173 def 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 180 def 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 187 def 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 194 def 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 201 def 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 208 def 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 215 def window_pct(+row: Json.Json, +key: String) -> Maybe<&2, U32>: 216 window_pct2(Json.get(key, row)) 217 218 def 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"). 227 def 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 231 def 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 238 def min_u32(+a: U32, +b: U32) -> U32: 239 SSeq.sel.u(U32.is_lt(a, b), a, b) 240 241 def 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`. 249 def 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. 253 def 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 256 def 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