@amritk/nish-x86_64-linux 0.20.0 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/INSTALL.md +52 -10
  2. package/bin/nish +0 -0
  3. package/docs/AI.md +2777 -0
  4. package/package.json +2 -1
  5. package/runtime/browser.d.ts +6 -0
  6. package/runtime/browser.mjs +77 -0
  7. package/runtime/core/arrays.d.ts +36 -0
  8. package/runtime/core/arrays.mjs +153 -0
  9. package/runtime/core/checks.mjs +20 -0
  10. package/runtime/core/host.d.ts +80 -0
  11. package/runtime/core/host.mjs +325 -0
  12. package/runtime/core/numbers.d.ts +66 -0
  13. package/runtime/core/numbers.mjs +343 -0
  14. package/runtime/core/result.d.ts +45 -0
  15. package/runtime/core/result.mjs +140 -0
  16. package/runtime/core/strings.d.ts +14 -0
  17. package/runtime/core/strings.mjs +169 -0
  18. package/runtime/core.d.ts +9 -0
  19. package/runtime/core.mjs +55 -0
  20. package/runtime/host.d.ts +6 -0
  21. package/runtime/nish.d.ts +46 -2
  22. package/runtime/nish.h +75 -2
  23. package/runtime/nish.mjs +23 -3
  24. package/runtime/node.mjs +386 -0
  25. package/runtime/runtime-bytes.c +401 -0
  26. package/runtime/runtime-cross.c +1 -1
  27. package/runtime/runtime-host.c +1 -1
  28. package/runtime/runtime-net.c +12 -1
  29. package/runtime/runtime-os.c +1 -1
  30. package/runtime/runtime-parallel.c +1 -1
  31. package/runtime/runtime-simd.c +231 -7
  32. package/runtime/runtime-term.c +504 -0
  33. package/runtime/runtime-wasm.c +0 -11
  34. package/runtime/runtime.c +190 -6
  35. package/runtime/shim.mjs +6 -985
  36. package/runtime/sqlite/LICENSE.md +85 -0
  37. package/runtime/sqlite/README.md +45 -0
  38. package/runtime/sqlite/nish-sqlite.c +359 -0
  39. package/runtime/sqlite/sqlite3.c +271131 -0
  40. package/runtime/sqlite/sqlite3.h +14447 -0
  41. package/scripts/build.sh +311 -59
  42. package/std/LICENSE-fast-float +23 -0
  43. package/std/LICENSE-fdlibm +8 -0
  44. package/std/README.md +32 -12
  45. package/std/base64.ts +116 -0
  46. package/std/bytes.ts +256 -0
  47. package/std/cli.ts +344 -0
  48. package/std/collections.ts +6 -5
  49. package/std/crypto/LICENSE-crypt_blowfish +26 -0
  50. package/std/crypto/base64url.ts +125 -0
  51. package/std/crypto/bcrypt.ts +1557 -0
  52. package/std/crypto/hmac.ts +138 -2
  53. package/std/crypto/x509.ts +11 -122
  54. package/std/json-tokenizer.ts +2998 -57
  55. package/std/json-value.ts +22 -3
  56. package/std/json.ts +87 -8
  57. package/std/math.ts +608 -0
  58. package/std/net/http1-server.ts +282 -40
  59. package/std/net/http1.ts +40 -21
  60. package/std/net/http3-server.ts +50 -12
  61. package/std/net/postgres.ts +2294 -0
  62. package/std/net/prefork.ts +23 -3
  63. package/std/net/quic-listener.ts +20 -4
  64. package/std/random.ts +66 -4
  65. package/std/regex.ts +4274 -0
  66. package/std/sqlite.ts +391 -0
  67. package/std/term-io.ts +978 -0
  68. package/std/term-pixels.ts +874 -0
  69. package/std/term-probe.ts +459 -0
  70. package/std/term-rate.ts +993 -0
  71. package/std/term.ts +2453 -0
  72. package/std/text.ts +90 -7
  73. package/std/yaml-lines.ts +141 -0
  74. package/std/yaml.ts +4675 -0
@@ -0,0 +1,993 @@
1
+ /**
2
+ * `std/term-rate` — how fast the link to the terminal is, measured from frames
3
+ * sent and acknowledged, and the frame rate and drawing detail a program keeps
4
+ * to under it.
5
+ *
6
+ * The module is pure: no capability, no I/O and no clock. The caller feeds a
7
+ * `RateController` what happened — a frame of so many bytes, costing so many
8
+ * nanoseconds to build, sent at a time; an acknowledgement arriving at a time —
9
+ * and asks it whether a frame may be drawn now and in what shape
10
+ * (`RateDecision`). Every time is an `f64` count of milliseconds on the
11
+ * caller's clock, as `FixedStep`'s is in `nish/term`.
12
+ *
13
+ * **The acknowledgement.** A frame is acknowledged when the terminal answers a
14
+ * status query sent right after it: the terminal answers in order, so the
15
+ * answer means every byte before it has been read and drawn. The query to send
16
+ * is DSR 5, `CSI 5 n`, answered `CSI 0 n`. It is preferred over the cursor
17
+ * position report (DSR 6, `CSI 6 n`, answered `CSI row ; col R`) because its
18
+ * answer is one fixed string, and because a cursor report is the same bytes as
19
+ * F3 with modifiers in the legacy key encoding (`CSI 1 ; mods R`), which a key
20
+ * decoder would have to guess between. Two relays answer the query from their
21
+ * own emulator, not from the terminal at the end of the link:
22
+ *
23
+ * - tmux (commit 939e8e65) answers in `input.c` lines 1737–1747, the
24
+ * `INPUT_CSI_DSR` case, which replies `\033[0n` to 5 and the position to 6
25
+ * through `input_reply`, whose `input_send_reply` (lines 1177–1186) writes to
26
+ * the pane. Inside tmux the round trip measures tmux, not the outer
27
+ * terminal, and a frame tmux has taken is not yet on the screen.
28
+ * - mosh (commit decd9b70) answers in `src/terminal/terminalfunctions.cc`
29
+ * lines 471–488, `CSI_DSR`, which appends the reply to `terminal_to_host`;
30
+ * `src/frontend/mosh-server.cc` feeds the program's output to that emulator
31
+ * at line 874 and writes `terminal_to_host` back to the program at line 882.
32
+ * The answer comes from mosh-server on the remote host and never crosses the
33
+ * network, so mosh reads as a fast local link; its own frame rate limit
34
+ * (mosh sends at most about 50 frames a second) then does the pacing.
35
+ *
36
+ * Under either relay the controller still bounds the program to what the relay
37
+ * absorbs, which is the most it can know.
38
+ *
39
+ * **What is measured.** Four running averages (exponentially weighted, so
40
+ * recent frames count most and none is stored): the round trip from a frame's
41
+ * send to its acknowledgement (weight 1/8, as TCP smooths its round trip, so
42
+ * one late answer moves it little); the bytes a second the link delivered;
43
+ * the cost to build a frame and the bytes in one (weight 1/4 each, so a
44
+ * change of scene shows within a few frames).
45
+ * The bandwidth and the latency are kept apart, so neither can take the
46
+ * other's share of a round trip. A frame sent before the previous answer, and
47
+ * answered more than the latency after it, kept the link busy from that answer
48
+ * to its own: its bytes over that time measure the bandwidth. A frame sent
49
+ * onto an idle link makes a trip, kept with the last 16 and the probe's:
50
+ * against the quickest kept trip, one twice its bytes or half, 2 ms or more
51
+ * apart, measures the bandwidth from the difference, which the latency is no
52
+ * part of.
53
+ * Both kinds smooth the bytes and the time apart (weight 1/4 each), so answers
54
+ * arriving in a burst, each taking almost no time, do not read as unbounded
55
+ * bandwidth; answers under 2 ms apart measure nothing this way. A sample
56
+ * counts for the decisions for 5 seconds with no newer one, so a link that
57
+ * narrows is not held to its old rate; the bandwidth reported keeps it until
58
+ * a newer sample, which starts the average afresh, replaces it, or three
59
+ * trips in a row take over twice what it and the quickest trip kept when
60
+ * it was taken predict. The figure
61
+ * is at least the most of the last 16 answers' bytes over their whole round
62
+ * trips, since the bytes took no longer than that, so a link that widens shows on its first quick answer; but that
63
+ * bound counts the latency as bytes' time, so alone it never makes the
64
+ * bandwidth known to the decisions. The base latency is the least, over the
65
+ * kept trips, of the round trip less the trip's bytes at the bandwidth: the
66
+ * probe's round trip (`probeRttMs`: the status query alone is a few bytes, so
67
+ * its round trip is the latency) counts from the start, the frames' once the
68
+ * bandwidth is known. The kept trips roll over, so a changed route shows
69
+ * within 16 trips. With frames of one size sent one at a time onto
70
+ * an idle link and no probe the latency and the bytes cannot be told apart:
71
+ * the bandwidth is then not known, its load counts as 0, and the trip load
72
+ * governs on the frame's measured round trip: a cut is taken to shorten none
73
+ * of it and a restore to lengthen all of it, so detail is neither cut for a
74
+ * gain nor restored at a cost that may not be there.
75
+ * Acknowledgements match sent frames oldest first. A frame due while the
76
+ * unacknowledged limit is reached is dropped and counted; one not answered
77
+ * within `rateAckTimeoutMs` plus four round trips is given up and counted as
78
+ * lost, so a lost answer
79
+ * stalls drawing for seconds, not for ever. Its answer may still come late, and
80
+ * would otherwise be matched to the next frame, so after a give-up no frame is
81
+ * drawn until as many answers have arrived, or one more timeout has passed
82
+ * without them. A time, cost or size that is not a finite number, or is
83
+ * negative, is not measured, and an empty frame says nothing about bandwidth.
84
+ *
85
+ * **What is decided.** The frame rate is a tier: 120 (only with `allow120` and
86
+ * a `maxFps` of 120), 60, 30, 20, 15, 10 or 5. A tier fits when three loads
87
+ * are all at most 1: the frame cost over 70% of the frame time, the bytes a
88
+ * second the tier needs over 70% of the bytes a second the link delivers, and
89
+ * the tier's frames times the round trip over 70% of the time the
90
+ * unacknowledged limit allows (a link that answers in 100 ms carries at most
91
+ * ten frames a second one at a time). The 30% left over absorbs a slow frame
92
+ * without a dropped one. The rate falls to the best fitting tier after half a
93
+ * second continuously over, and rises one tier after three seconds
94
+ * continuously under the next tier's loads with a fifth to spare; after any
95
+ * change it holds for a second, so the averages see the new rate before the
96
+ * next decision. Falling fast and rising slowly keeps a flickering link from
97
+ * flipping the rate.
98
+ *
99
+ * Below the floor tier (the least tier at or above `minFps`, under
100
+ * `RATE_FIRST`) the controller cuts bytes instead of frames, one step at a
101
+ * time: half the resolution, a quarter, the backend capped at half blocks, 256
102
+ * colours. A step is taken only when it would cut the load by a fifth: a link
103
+ * whose round trip is all latency is not helped by smaller frames, so it gets a
104
+ * lower tier at full detail instead. Past the last step, tiers below `minFps`
105
+ * remain. `DETAIL_FIRST` swaps the order: every tier down to 5 first, then the
106
+ * steps. Recovery runs the same path backwards with the rising rule, predicting
107
+ * a step's bytes and cost from the factors in `rateLevelFactor`.
108
+ *
109
+ * A fixed rate — `fixedFps` from the program or `envFps`, the user's
110
+ * `NISH_TERM_FPS`, which wins — turns the choosing off: the rate is that one,
111
+ * at full detail, and only the unacknowledged limit still applies.
112
+ */
113
+ import { ATTR_INVERSE, COLOR_DEFAULT, FixedStep, Screen } from "nish/term"
114
+
115
+ /** `RateOptions.prefer`: keep the frame rate at `minFps` and cut detail first. */
116
+ export const RATE_FIRST: i32 = 0
117
+
118
+ /** `RateOptions.prefer`: keep full detail and drop the frame rate toward 5 first. */
119
+ export const DETAIL_FIRST: i32 = 1
120
+
121
+ /**
122
+ * `RateDecision.backendCap` and `TermStats.backend`: the richest drawing
123
+ * backend allowed. The values order the backends, richer higher, so a caller
124
+ * takes the lesser of this and what the terminal supports.
125
+ */
126
+ export const BACKEND_CAP_ASCII: i32 = 0
127
+
128
+ /** A backend cap of Unicode half blocks, two pixels to a cell. */
129
+ export const BACKEND_CAP_HALFBLOCK: i32 = 1
130
+
131
+ /** A backend cap of sixel graphics. */
132
+ export const BACKEND_CAP_SIXEL: i32 = 2
133
+
134
+ /** A backend cap of the kitty graphics protocol, the richest: no cap at all. */
135
+ export const BACKEND_CAP_KITTY: i32 = 3
136
+
137
+ /** `drawStatsOverlay`'s corner: the top left. */
138
+ export const CORNER_TOP_LEFT: i32 = 0
139
+
140
+ /** The top right corner. */
141
+ export const CORNER_TOP_RIGHT: i32 = 1
142
+
143
+ /** The bottom left corner. */
144
+ export const CORNER_BOTTOM_LEFT: i32 = 2
145
+
146
+ /** The bottom right corner. */
147
+ export const CORNER_BOTTOM_RIGHT: i32 = 3
148
+
149
+ /** The share of a frame's time, or of the link, that a tier may use. */
150
+ const rateBudget: f64 = 0.7
151
+
152
+ /** The load a candidate must stay under to rise to it: a fifth to spare. */
153
+ const rateRiseMargin: f64 = 0.8
154
+
155
+ /** How much a detail step must cut the load to be taken. */
156
+ const rateStepGain: f64 = 0.8
157
+
158
+ /** How long over budget before the rate falls, in milliseconds. */
159
+ const rateDropAfterMs: f64 = 500.0
160
+
161
+ /** How long under the next tier's budget before the rate rises, in milliseconds. */
162
+ const rateRiseAfterMs: f64 = 3000.0
163
+
164
+ /** How long nothing changes after a change, in milliseconds. */
165
+ const rateSettleMs: f64 = 1000.0
166
+
167
+ /** A probe round trip under this many milliseconds is a local terminal, whatever the environment says. */
168
+ const rateLocalRttMs: f64 = 5.0
169
+
170
+ /** The deepest detail step: 256 colours on top of the others. */
171
+ const rateMaxLevel: i32 = 4
172
+
173
+ /** The least time the link is taken to spend on a frame, so one fast answer cannot read as unbounded bandwidth. */
174
+ const rateMinBusyMs: f64 = 0.5
175
+
176
+ /** How many trips, from the probe and from frames sent onto an idle link, the latency is the least of. */
177
+ const rateTripCount: i32 = 16
178
+
179
+ /** Two trips measure the bandwidth from their difference only when it is at least this many milliseconds. */
180
+ const rateSplitMs: f64 = 2.0
181
+
182
+ /** How long a bandwidth sample counts with no newer one, in milliseconds, so a link that narrows is not held to its old rate. */
183
+ const rateFreshMs: f64 = 5000.0
184
+
185
+ /** A trip over this many times what an aged bandwidth sample predicts contradicts it. */
186
+ const rateStaleFactor: f64 = 2.0
187
+
188
+ /** How many trips in a row must contradict an aged sample before it goes, so one slow answer does not. */
189
+ const rateStaleTrips: i32 = 3
190
+
191
+ /** How long, past four round trips, an unanswered frame is waited for, in milliseconds. */
192
+ const rateAckTimeoutMs: f64 = 2000.0
193
+
194
+ /**
195
+ * How much a frame shrinks, in bytes and in cost, going from detail step
196
+ * `level - 1` to `level`. Half the resolution is a quarter of the pixels; half
197
+ * blocks against sixel or kitty images are taken as a quarter, and 256 colours
198
+ * as half, of the escapes. They are estimates, only for predicting a step not
199
+ * yet taken: once there, the frames are measured.
200
+ */
201
+ const rateLevelFactor = (level: i32): f64 => (level === rateMaxLevel ? 2.0 : 4.0)
202
+
203
+ /** How much smaller a frame at detail step `to` is than at step `from`, as a multiplier. */
204
+ const rateLevelScale = (from: i32, to: i32): f64 => {
205
+ let scale: f64 = 1.0
206
+ let level: i32 = from
207
+ while (level < to) {
208
+ level = level + 1
209
+ scale = scale / rateLevelFactor(level)
210
+ }
211
+ while (level > to) {
212
+ scale = scale * rateLevelFactor(level)
213
+ level = level - 1
214
+ }
215
+ return scale
216
+ }
217
+
218
+ /** Whether `x` is a number, not NaN or an infinity. */
219
+ const rateFinite = (x: f64): boolean => x - x === 0.0
220
+
221
+ /** `average` moved toward `sample` by `weight`, or `sample` itself for the first one. */
222
+ const rateSmooth = (average: f64, sample: f64, weight: f64, first: boolean): f64 =>
223
+ first ? sample : average + (sample - average) * weight
224
+
225
+ /** What a program asks of the controller. Every field has a default, so a program sets only what it means. */
226
+ export class RateOptions {
227
+ /** The round trip of a probe query sent at startup, in milliseconds; 0 when none was measured. */
228
+ probeRttMs: f64 = 0.0
229
+ /** The least frame rate worth keeping before detail is cut, under `RATE_FIRST`. */
230
+ minFps: i32 = 10
231
+ /** The most frames a second; 120 is reached only with `allow120` as well. */
232
+ maxFps: i32 = 60
233
+ /** A frame rate the program fixes, turning the choosing off; 0 leaves it on. */
234
+ fixedFps: i32 = 0
235
+ /** `RATE_FIRST` or `DETAIL_FIRST`: what gives way first when even `minFps` does not fit. */
236
+ prefer: i32 = 0
237
+ /** How many frames may be awaiting their acknowledgement at once: 1 or 2. */
238
+ maxUnacked: i32 = 1
239
+ /** The user's `NISH_TERM_FPS`, which fixes the rate over everything else; 0 when unset. */
240
+ envFps: i32 = 0
241
+ /** Whether the 120 tier may be used, for a terminal known to keep up with it. */
242
+ allow120: boolean = false
243
+ /** Whether the environment says the terminal is remote (an `SSH_*` variable, say). */
244
+ remoteHint: boolean = false
245
+
246
+ /** The defaults; set the fields to change them. */
247
+ constructor() {
248
+ this.minFps = 10
249
+ this.maxFps = 60
250
+ this.fixedFps = 0
251
+ this.allow120 = false
252
+ this.prefer = RATE_FIRST
253
+ this.maxUnacked = 1
254
+ this.remoteHint = false
255
+ this.probeRttMs = 0.0
256
+ this.envFps = 0
257
+ }
258
+ }
259
+
260
+ /** How to draw the next frame: its rate and how much detail. */
261
+ export class RateDecision {
262
+ /** Frames a second. */
263
+ fps: i32 = 30
264
+ /** Draw at the screen's resolution divided by this: 1, 2 or 4. */
265
+ resolutionDivisor: i32 = 1
266
+ /** The richest backend to draw with, a `BACKEND_CAP_*`. */
267
+ backendCap: i32 = 3
268
+ /** Whether to keep to the 256-colour palette. */
269
+ palette256: boolean = false
270
+
271
+ /** Full detail at 30 frames a second, until the controller says otherwise. */
272
+ constructor() {
273
+ this.fps = 30
274
+ this.resolutionDivisor = 1
275
+ this.backendCap = BACKEND_CAP_KITTY
276
+ this.palette256 = false
277
+ }
278
+ }
279
+
280
+ /** What the controller has measured, for a program to show or log. */
281
+ export class TermStats {
282
+ /** The frame rate in force. */
283
+ tier: i32 = 30
284
+ /** The backend cap in force, a `BACKEND_CAP_*`. */
285
+ backend: i32 = 3
286
+ /** The smoothed round trip from a frame's send to its acknowledgement, in milliseconds. */
287
+ rttMs: f64 = 0.0
288
+ /** The smoothed bytes a second the link delivers. */
289
+ bytesPerSec: f64 = 0.0
290
+ /** The smoothed cost to build a frame, in milliseconds. */
291
+ frameCostMs: f64 = 0.0
292
+ /** Due frames skipped because the unacknowledged limit was reached. */
293
+ dropped: i32 = 0
294
+ /** Frames sent and not yet acknowledged. */
295
+ unacked: i32 = 0
296
+ /** Frames given up on after waiting `rateAckTimeoutMs` past four round trips. */
297
+ lost: i32 = 0
298
+
299
+ /** Nothing measured yet. */
300
+ constructor() {
301
+ this.tier = 30
302
+ this.backend = BACKEND_CAP_KITTY
303
+ this.rttMs = 0.0
304
+ this.bytesPerSec = 0.0
305
+ this.frameCostMs = 0.0
306
+ this.dropped = 0
307
+ this.unacked = 0
308
+ this.lost = 0
309
+ }
310
+ }
311
+
312
+ /**
313
+ * The refresh controller. Each turn of a program's loop: feed it the
314
+ * acknowledgements that arrived (`ackReceived`), let it re-decide (`tick`),
315
+ * and draw only when `mayDraw` says so, reporting the frame with `frameSent`.
316
+ * `decision` says how to draw it. `decision` and `stats` answer the same object
317
+ * each call, updated in place, so a loop allocates nothing.
318
+ */
319
+ export class RateController {
320
+ /** The tiers allowed, fastest first, ending at 5. */
321
+ tiers: i32[]
322
+ /** Paces frames at `tier`. */
323
+ pacer: FixedStep
324
+ /** When the tier or step last changed. */
325
+ changedMs: f64
326
+ /** When the loads went over budget and stayed, or -1. */
327
+ overSinceMs: f64
328
+ /** When the next step up came within its margin and stayed, or -1. */
329
+ underSinceMs: f64
330
+ /** The send times of the unacknowledged frames, oldest at `head`. */
331
+ sentMs: f64[]
332
+ /** Their byte counts. */
333
+ sentBytes: i32[]
334
+ /** When the last acknowledgement arrived, or -1. */
335
+ lastAckMs: f64
336
+ /** The smoothed round trip, in milliseconds. */
337
+ rttMs: f64
338
+ /** The link's latency without bytes: the least, over the kept trips, of the round trip less the bytes' time. */
339
+ baseRttMs: f64
340
+ /** The latency as last worked out while the bandwidth was known, held once its samples age out. */
341
+ heldLatencyMs: f64
342
+ /** The round trips of the probe and of the latest frames sent onto an idle link, `rateTripCount` at most. */
343
+ tripMs: f64[]
344
+ /** Their byte counts, 0 for the probe. */
345
+ tripBytes: f64[]
346
+ /** The bytes a second each of the latest answers proves at least, its bytes over its round trip. */
347
+ floorPerSec: f64[]
348
+ /** The smoothed bytes of an answered frame, for the bandwidth. */
349
+ ackBytes: f64
350
+ /** The smoothed time the link spent on an answered frame, in milliseconds, for the bandwidth. */
351
+ ackBusyMs: f64
352
+ /**
353
+ * The bytes a second delivered: `ackBytes` over `ackBusyMs`, at least the most of `floorPerSec`, which alone is a lower
354
+ * bound. Samples older than `rateFreshMs` still count here while trips agree with them, but no decision uses them.
355
+ */
356
+ bytesPerSec: f64
357
+ /** When the bandwidth was last sampled, by a frame that waited behind another or by two trips. */
358
+ rateAtMs: f64
359
+ /** The quickest kept trip, or the round trip with none kept, when the bandwidth was last sampled: at least the latency then. */
360
+ rateQuickMs: f64
361
+ /** When frames were last given up on, while `stale` waits for their late answers. */
362
+ staleSinceMs: f64
363
+ /** The smoothed frame cost, in milliseconds. */
364
+ costMs: f64
365
+ /** The smoothed bytes per frame. */
366
+ frameBytes: f64
367
+ current: RateDecision
368
+ measured: TermStats
369
+ /** The least tier detail is cut to keep: the floor tier under `RATE_FIRST`, 5 under `DETAIL_FIRST`. */
370
+ floorFps: i32
371
+ /** The least tier above which `RATE_FIRST` keeps full detail. */
372
+ minTier: i32
373
+ prefer: i32
374
+ maxUnacked: i32
375
+ /** The fixed frame rate, or 0 when the controller chooses. */
376
+ fixedFps: i32
377
+ /** The frame rate in force. */
378
+ tier: i32
379
+ /** The detail step in force, 0 (full detail) to `rateMaxLevel`. */
380
+ level: i32
381
+ /** The oldest unacknowledged frame's index in `sentMs`. */
382
+ head: i32
383
+ dropped: i32
384
+ lost: i32
385
+ /** Frames given up on whose answers may still come: the next answers are theirs, and no frame is drawn meanwhile. */
386
+ stale: i32
387
+ /** Where the next trip goes in `tripMs` once it is full. */
388
+ tripNext: i32
389
+ /** How many trips in a row have contradicted the aged bandwidth sample. */
390
+ contradicted: i32
391
+ /** Where the next goes in `floorPerSec` once it is full. */
392
+ floorNext: i32
393
+ /** Whether a frame has been asked for, so the pacer's clock runs. */
394
+ paced: boolean
395
+ /** Whether a round trip, from the probe or an answer, is known. */
396
+ haveRtt: boolean
397
+ /** Whether a frame has been acknowledged, so decisions can start. */
398
+ haveAck: boolean
399
+ /**
400
+ * Whether the bandwidth is known, so decisions may use it: sampled by a
401
+ * frame that waited behind another or two trips of different sizes, within
402
+ * `rateFreshMs`. The answers' bytes over their round trips are only a lower
403
+ * bound and never make it known.
404
+ */
405
+ haveRate: boolean
406
+ /** Whether a frame has been sent, so cost and size are known. */
407
+ haveFrame: boolean
408
+
409
+ /** A controller for `options`, read once. */
410
+ constructor(options: RateOptions) {
411
+ this.prefer = options.prefer === DETAIL_FIRST ? DETAIL_FIRST : RATE_FIRST
412
+ this.maxUnacked = options.maxUnacked >= 2 ? 2 : 1
413
+ const fixed: i32 = options.envFps > 0 ? options.envFps : options.fixedFps
414
+ const capped: i32 = fixed > 240 ? 240 : fixed
415
+ this.fixedFps = capped > 0 ? capped : 0
416
+ const all: i32[] = [120, 60, 30, 20, 15, 10, 5]
417
+ this.tiers = []
418
+ for (const t of all) {
419
+ if (t === 5 || (t <= options.maxFps && (t !== 120 || options.allow120))) {
420
+ this.tiers.push(t)
421
+ }
422
+ }
423
+ // The least tier at or above `minFps`; the tiers end at 5, which is below every sensible floor.
424
+ let minTier: i32 = this.tiers[0]
425
+ for (const t of this.tiers) {
426
+ if (t >= options.minFps) {
427
+ minTier = t
428
+ }
429
+ }
430
+ this.minTier = minTier
431
+ this.floorFps = this.prefer === DETAIL_FIRST ? 5 : minTier
432
+ const remote: boolean =
433
+ options.remoteHint && !(options.probeRttMs > 0.0 && options.probeRttMs < rateLocalRttMs)
434
+ const want: i32 = remote ? 15 : 30
435
+ let start: i32 = this.tiers[this.tiers.length - 1]
436
+ for (const t of this.tiers) {
437
+ if (t <= want && t > start) {
438
+ start = t
439
+ }
440
+ }
441
+ const floored: i32 = start < this.floorFps ? this.floorFps : start
442
+ this.tier = this.fixedFps > 0 ? this.fixedFps : floored
443
+ this.level = 0
444
+ this.pacer = new FixedStep(this.tier, 1)
445
+ this.paced = false
446
+ this.changedMs = 0.0
447
+ this.overSinceMs = -1.0
448
+ this.underSinceMs = -1.0
449
+ this.sentMs = []
450
+ this.sentBytes = []
451
+ this.head = 0
452
+ this.lastAckMs = -1.0
453
+ // A probe time that is not a finite number says nothing.
454
+ this.haveRtt = options.probeRttMs > 0.0 && rateFinite(options.probeRttMs)
455
+ this.rttMs = this.haveRtt ? options.probeRttMs : 0.0
456
+ this.baseRttMs = this.rttMs
457
+ this.haveAck = false
458
+ this.haveRate = false
459
+ this.tripMs = []
460
+ this.tripBytes = []
461
+ this.tripNext = 0
462
+ this.contradicted = 0
463
+ this.floorPerSec = []
464
+ this.floorNext = 0
465
+ if (this.haveRtt) {
466
+ // The probe is a few bytes, so its round trip is the latency.
467
+ this.tripMs.push(this.rttMs)
468
+ this.tripBytes.push(toF64(0))
469
+ }
470
+ this.ackBytes = 0.0
471
+ this.ackBusyMs = 0.0
472
+ this.bytesPerSec = 0.0
473
+ this.rateAtMs = 0.0
474
+ this.rateQuickMs = 0.0
475
+ this.heldLatencyMs = 0.0
476
+ this.stale = 0
477
+ this.staleSinceMs = 0.0
478
+ this.costMs = 0.0
479
+ this.frameBytes = 0.0
480
+ this.haveFrame = false
481
+ this.dropped = 0
482
+ this.lost = 0
483
+ this.current = new RateDecision()
484
+ this.measured = new TermStats()
485
+ this.publish()
486
+ }
487
+
488
+ /** Frames sent and not yet acknowledged. */
489
+ unacked(): i32 {
490
+ return toI32(this.sentMs.length) - this.head
491
+ }
492
+
493
+ /**
494
+ * Whether a frame may be drawn at `nowMs`: one is due at the tier's pace and
495
+ * fewer than `maxUnacked` frames await their acknowledgement. The first call
496
+ * starts the pace and answers a frame at once. A due frame refused for the
497
+ * limit, or while the answers to frames given up on may still come, is
498
+ * counted as dropped, and the next waits for the next step. A time that is
499
+ * not a finite number is refused.
500
+ */
501
+ mayDraw(nowMs: f64): boolean {
502
+ if (!rateFinite(nowMs)) {
503
+ return false
504
+ }
505
+ let due: boolean = true
506
+ if (this.paced) {
507
+ due = this.pacer.advance(nowMs) > 0
508
+ } else {
509
+ this.paced = true
510
+ this.pacer.advance(nowMs)
511
+ }
512
+ if (!due) {
513
+ return false
514
+ }
515
+ if (this.unacked() >= this.maxUnacked || this.stale > 0) {
516
+ this.dropped = this.dropped + 1
517
+ return false
518
+ }
519
+ return true
520
+ }
521
+
522
+ /**
523
+ * A frame of `bytes` bytes, which took `costNs` nanoseconds to build, was
524
+ * sent at `nowMs`. A call whose time is not a finite number is ignored; a
525
+ * cost that is not a finite number or is negative is not measured, and
526
+ * negative bytes count as none.
527
+ */
528
+ frameSent(bytes: i32, costNs: f64, nowMs: f64): void {
529
+ if (!rateFinite(nowMs)) {
530
+ return
531
+ }
532
+ const size: i32 = bytes > 0 ? bytes : 0
533
+ const length: i32 = toI32(this.sentMs.length)
534
+ if (this.head > 0 && this.head * 2 >= length) {
535
+ // Drop the answered prefix once it is half the queue: a link always a frame behind never empties it.
536
+ for (let i: i32 = this.head; i < length; i++) {
537
+ this.sentMs[i - this.head] = this.sentMs[i]
538
+ this.sentBytes[i - this.head] = this.sentBytes[i]
539
+ }
540
+ for (let i: i32 = 0; i < this.head; i++) {
541
+ this.sentMs.pop()
542
+ this.sentBytes.pop()
543
+ }
544
+ this.head = 0
545
+ }
546
+ this.sentMs.push(nowMs)
547
+ this.sentBytes.push(size)
548
+ if (rateFinite(costNs) && costNs >= 0.0) {
549
+ const first: boolean = !this.haveFrame
550
+ this.costMs = rateSmooth(this.costMs, costNs / 1000000.0, 0.25, first)
551
+ this.frameBytes = rateSmooth(this.frameBytes, toF64(size), 0.25, first)
552
+ this.haveFrame = true
553
+ }
554
+ }
555
+
556
+ /**
557
+ * An acknowledgement arrived at `nowMs`; it answers the oldest frame not yet
558
+ * answered. While frames given up on may still be answered, it is theirs,
559
+ * since the terminal answers in order. One with no frame waiting (a stray
560
+ * answer) is ignored, and one whose time is not a finite number answers its
561
+ * frame and measures nothing.
562
+ */
563
+ ackReceived(nowMs: f64): void {
564
+ if (this.stale > 0) {
565
+ this.stale = this.stale - 1
566
+ return
567
+ }
568
+ if (this.unacked() === 0) {
569
+ return
570
+ }
571
+ const sent: f64 = this.sentMs[this.head]
572
+ const bytes: f64 = toF64(this.sentBytes[this.head])
573
+ this.head = this.head + 1
574
+ if (!rateFinite(nowMs)) {
575
+ return
576
+ }
577
+ this.haveAck = true
578
+ // A clock that stepped back, or an answer read in the millisecond its frame went, times nothing.
579
+ if (!(nowMs > sent)) {
580
+ return
581
+ }
582
+ const rtt: f64 = nowMs - sent
583
+ this.rttMs = rateSmooth(this.rttMs, rtt, 0.125, !this.haveRtt)
584
+ this.haveRtt = true
585
+ const previous: f64 = this.lastAckMs
586
+ this.lastAckMs = nowMs
587
+ if (previous >= 0.0 && previous > sent + this.baseRttMs) {
588
+ // The link was still busy with the frame ahead when this one reached it, so the gap between their answers is
589
+ // this frame's bytes alone, whatever the latency. Bytes and time are smoothed apart, not their ratio.
590
+ // Answers read in a burst arrive together, too close to time, and measure nothing this way.
591
+ const took: f64 = nowMs - previous
592
+ if (bytes > 0.0 && took >= rateSplitMs) {
593
+ this.measureRate(bytes, took)
594
+ }
595
+ } else {
596
+ this.measureTrip(rtt, bytes)
597
+ }
598
+ if (!this.haveRate && this.ackBusyMs > 0.0 && bytes > 0.0) {
599
+ // The aged sample still reported is kept while trips take what it predicts: the quickest trip when it was taken,
600
+ // which holds the whole latency and perhaps some bytes, so the prediction errs long, plus these bytes' time.
601
+ const expected: f64 = this.rateQuickMs + (bytes * this.ackBusyMs) / this.ackBytes
602
+ this.contradicted = rtt > rateStaleFactor * expected ? this.contradicted + 1 : 0
603
+ if (this.contradicted >= rateStaleTrips) {
604
+ this.ackBytes = 0.0
605
+ this.ackBusyMs = 0.0
606
+ this.contradicted = 0
607
+ this.updateRate()
608
+ }
609
+ }
610
+ if (bytes > 0.0 && rtt > rateMinBusyMs) {
611
+ // Whatever the latency and however late the answer was read, the bytes took no longer than the whole trip.
612
+ if (toI32(this.floorPerSec.length) < rateTripCount) {
613
+ this.floorPerSec.push((bytes * 1000.0) / rtt)
614
+ } else {
615
+ this.floorPerSec[this.floorNext] = (bytes * 1000.0) / rtt
616
+ this.floorNext = (this.floorNext + 1) % rateTripCount
617
+ }
618
+ this.updateRate()
619
+ }
620
+ this.baseRttMs = this.leastLatency()
621
+ }
622
+
623
+ /**
624
+ * A frame of `bytes` sent onto an idle link came back in `rtt`: its latency
625
+ * plus its bytes' time, which cannot be told apart alone. Against the
626
+ * quickest trip kept, a frame at least twice the size (or twice as small)
627
+ * whose round trip differs by `rateSplitMs` or more does tell them apart:
628
+ * the extra bytes took the extra time, and the latency drops out. Frames of
629
+ * one size measure nothing here, so small frames cannot drag the bandwidth
630
+ * down to their round trip.
631
+ */
632
+ measureTrip(rtt: f64, bytes: f64): void {
633
+ let quickest: i32 = -1
634
+ for (let i: i32 = 0; i < toI32(this.tripMs.length); i++) {
635
+ if (quickest < 0 || this.tripMs[i] < this.tripMs[quickest]) {
636
+ quickest = i
637
+ }
638
+ }
639
+ if (quickest >= 0) {
640
+ const otherMs: f64 = this.tripMs[quickest]
641
+ const otherBytes: f64 = this.tripBytes[quickest]
642
+ const big: boolean = bytes > otherBytes
643
+ const moreBytes: f64 = big ? bytes - otherBytes : otherBytes - bytes
644
+ const moreMs: f64 = big ? rtt - otherMs : otherMs - rtt
645
+ const least: f64 = big ? otherBytes : bytes
646
+ if (moreBytes >= least && moreMs >= rateSplitMs) {
647
+ this.measureRate(moreBytes, moreMs)
648
+ }
649
+ }
650
+ if (toI32(this.tripMs.length) < rateTripCount) {
651
+ this.tripMs.push(rtt)
652
+ this.tripBytes.push(bytes)
653
+ } else {
654
+ this.tripMs[this.tripNext] = rtt
655
+ this.tripBytes[this.tripNext] = bytes
656
+ this.tripNext = (this.tripNext + 1) % rateTripCount
657
+ }
658
+ }
659
+
660
+ /** The link spent `busyMs` on `bytes`: one sample of the bandwidth. */
661
+ measureRate(bytes: f64, busyMs: f64): void {
662
+ // A sample after the last aged out starts afresh, so a link that narrowed meanwhile is not averaged with its old rate.
663
+ const first: boolean = this.ackBusyMs <= 0.0 || !this.haveRate
664
+ this.rateAtMs = this.lastAckMs
665
+ let quickest: f64 = this.rttMs
666
+ for (let i: i32 = 0; i < toI32(this.tripMs.length); i++) {
667
+ if (i === 0 || this.tripMs[i] < quickest) {
668
+ quickest = this.tripMs[i]
669
+ }
670
+ }
671
+ this.rateQuickMs = quickest
672
+ this.ackBytes = rateSmooth(this.ackBytes, bytes, 0.25, first)
673
+ this.ackBusyMs = rateSmooth(this.ackBusyMs, busyMs, 0.25, first)
674
+ this.updateRate()
675
+ }
676
+
677
+ /**
678
+ * The bandwidth: the smoothed samples, but never under what the latest
679
+ * answers prove, so a link that widens, or whose answers are read late, is
680
+ * not taken for a narrow one. Only samples make it known, and only while
681
+ * the latest is within `rateFreshMs` of the latest answer: an answer's
682
+ * bytes over its whole round trip count the latency as bytes' time, and on
683
+ * a far link that reads the bandwidth several times too low.
684
+ */
685
+ updateRate(): void {
686
+ let rate: f64 = this.ackBusyMs > 0.0 ? (this.ackBytes * 1000.0) / this.ackBusyMs : 0.0
687
+ this.haveRate = rate > 0.0 && this.lastAckMs - this.rateAtMs <= rateFreshMs
688
+ for (let i: i32 = 0; i < toI32(this.floorPerSec.length); i++) {
689
+ if (this.floorPerSec[i] > rate) {
690
+ rate = this.floorPerSec[i]
691
+ }
692
+ }
693
+ this.bytesPerSec = rate
694
+ }
695
+
696
+ /**
697
+ * Stop deciding on the bandwidth once no sample is newer than `rateFreshMs`
698
+ * at `nowMs`. The figure stays in `bytesPerSec` until a sample replaces it or a trip contradicts it.
699
+ */
700
+ ageRate(nowMs: f64): void {
701
+ if (this.haveRate && nowMs - this.rateAtMs > rateFreshMs) {
702
+ this.heldLatencyMs = this.baseRttMs
703
+ this.haveRate = false
704
+ this.baseRttMs = this.leastLatency()
705
+ }
706
+ }
707
+
708
+ /**
709
+ * The latency: the least, over the kept trips, of the round trip less its
710
+ * bytes' time at the measured bandwidth. While the bandwidth is not known
711
+ * only an empty trip, the probe's, says anything; with none it is the
712
+ * latency held from when the bandwidth was last known, or 0 before that: a
713
+ * lower bound, which is what telling a waited frame from a trip needs, and
714
+ * one that a probe trip rolled out of the kept trips does not drop to 0, or
715
+ * every frame sent before the answer ahead of it would look waited. The bandwidth never comes from the latency on an idle link, so
716
+ * neither can drag the other down.
717
+ */
718
+ leastLatency(): f64 {
719
+ let least: f64 = -1.0
720
+ for (let i: i32 = 0; i < toI32(this.tripMs.length); i++) {
721
+ const bytes: f64 = this.tripBytes[i]
722
+ if (bytes <= 0.0 || this.haveRate) {
723
+ const left: f64 = bytes > 0.0 ? this.tripMs[i] - (bytes * 1000.0) / this.bytesPerSec : this.tripMs[i]
724
+ const latency: f64 = left > 0.0 ? left : 0.0
725
+ if (least < 0.0 || latency < least) {
726
+ least = latency
727
+ }
728
+ }
729
+ }
730
+ if (least < 0.0) {
731
+ return this.heldLatencyMs
732
+ }
733
+ return least > 0.0 ? least : 0.0
734
+ }
735
+
736
+ /**
737
+ * The part of a round trip of `rtt` that a detail change scaling the bytes
738
+ * by `scale` leaves alone: the latency once the bandwidth is known. While it
739
+ * is not, the guess is the cautious one each way: less detail shortens no
740
+ * more than the round trip's excess over the quickest trip kept, which is
741
+ * at least the latency, so detail is not cut for a gain that may not be
742
+ * there; more lengthens all of it, so detail is not restored on a link that
743
+ * may not carry it.
744
+ */
745
+ fixedPart(rtt: f64, scale: f64): f64 {
746
+ if (!this.haveRate) {
747
+ if (scale >= 1.0) {
748
+ return 0.0
749
+ }
750
+ let quickest: f64 = rtt
751
+ for (let i: i32 = 0; i < toI32(this.tripMs.length); i++) {
752
+ if (this.tripMs[i] < quickest) {
753
+ quickest = this.tripMs[i]
754
+ }
755
+ }
756
+ return quickest
757
+ }
758
+ return this.baseRttMs < rtt ? this.baseRttMs : rtt
759
+ }
760
+
761
+ /**
762
+ * The worst of the three loads (cost, bytes, round trip) at `fps` frames a
763
+ * second and detail step `level`, against the 70% budgets: at most 1 fits.
764
+ * A step other than the current one is predicted from the measured frame;
765
+ * `nowMs` lets a frame long unanswered count as a round trip at least that
766
+ * long. A load not yet measured counts as 0.
767
+ */
768
+ load(fps: i32, level: i32, nowMs: f64): f64 {
769
+ const scale: f64 = rateLevelScale(this.level, level)
770
+ const rate: f64 = toF64(fps)
771
+ let worst: f64 = 0.0
772
+ if (this.haveFrame) {
773
+ worst = (this.costMs * scale * rate) / (rateBudget * 1000.0)
774
+ }
775
+ if (this.haveRate && this.haveFrame) {
776
+ const bytes: f64 = (this.frameBytes * scale * rate) / (rateBudget * this.bytesPerSec)
777
+ worst = bytes > worst ? bytes : worst
778
+ }
779
+ if (this.haveRtt) {
780
+ let rtt: f64 = this.rttMs
781
+ if (this.unacked() > 0 && nowMs - this.sentMs[this.head] > rtt) {
782
+ rtt = nowMs - this.sentMs[this.head]
783
+ }
784
+ const base: f64 = this.fixedPart(rtt, scale)
785
+ const trip: f64 =
786
+ (rate * (base + (rtt - base) * scale)) / (rateBudget * 1000.0 * toF64(this.maxUnacked))
787
+ worst = trip > worst ? trip : worst
788
+ }
789
+ return worst
790
+ }
791
+
792
+ /** The fastest allowed tier slower than `fps` and no slower than `least`, or `least`. */
793
+ slowerTier(fps: i32, least: i32): i32 {
794
+ for (const t of this.tiers) {
795
+ if (t < fps && t >= least) {
796
+ return t
797
+ }
798
+ }
799
+ return least
800
+ }
801
+
802
+ /** The slowest allowed tier faster than `fps`, or `fps` at the top. */
803
+ fasterTier(fps: i32): i32 {
804
+ let faster: i32 = fps
805
+ for (const t of this.tiers) {
806
+ if (t > fps) {
807
+ faster = t
808
+ }
809
+ }
810
+ return faster
811
+ }
812
+
813
+ /** Move to `fps` at detail step `level` at `nowMs`, rescaling the frame averages to the step. */
814
+ moveTo(fps: i32, level: i32, nowMs: f64): void {
815
+ if (level !== this.level) {
816
+ const scale: f64 = rateLevelScale(this.level, level)
817
+ this.costMs = this.costMs * scale
818
+ this.frameBytes = this.frameBytes * scale
819
+ const base: f64 = this.fixedPart(this.rttMs, scale)
820
+ this.rttMs = base + (this.rttMs - base) * scale
821
+ this.level = level
822
+ }
823
+ if (fps !== this.tier) {
824
+ this.tier = fps
825
+ this.pacer.stepMs = 1000.0 / toF64(fps)
826
+ }
827
+ this.changedMs = nowMs
828
+ this.overSinceMs = -1.0
829
+ this.underSinceMs = -1.0
830
+ }
831
+
832
+ /** One step down the path at `nowMs`, when over budget: fewer frames, or less detail. */
833
+ stepDown(nowMs: f64): void {
834
+ const least: i32 = this.tiers[this.tiers.length - 1]
835
+ if (this.tier > this.floorFps) {
836
+ // Straight to the fastest tier down to the floor that fits.
837
+ let fps: i32 = this.slowerTier(this.tier, this.floorFps)
838
+ while (fps > this.floorFps && this.load(fps, this.level, nowMs) > 1.0) {
839
+ fps = this.slowerTier(fps, this.floorFps)
840
+ }
841
+ this.moveTo(fps, this.level, nowMs)
842
+ return
843
+ }
844
+ const here: f64 = this.load(this.tier, this.level, nowMs)
845
+ if (this.level < rateMaxLevel && this.load(this.tier, this.level + 1, nowMs) <= here * rateStepGain) {
846
+ this.moveTo(this.tier, this.level + 1, nowMs)
847
+ return
848
+ }
849
+ if (this.tier > least) {
850
+ let fps: i32 = this.slowerTier(this.tier, least)
851
+ while (fps > least && this.load(fps, this.level, nowMs) > 1.0) {
852
+ fps = this.slowerTier(fps, least)
853
+ }
854
+ this.moveTo(fps, this.level, nowMs)
855
+ }
856
+ }
857
+
858
+ /**
859
+ * The next state up the path, as `fps * 8 + level`, or -1 at the top. It is
860
+ * the descent backwards: tiers below the floor first, then detail, then
861
+ * tiers above it.
862
+ */
863
+ stepUp(): i32 {
864
+ if (this.tier < this.floorFps || (this.level === 0 && this.tier < this.tiers[0])) {
865
+ return this.fasterTier(this.tier) * 8 + this.level
866
+ }
867
+ if (this.level > 0) {
868
+ return this.tier * 8 + this.level - 1
869
+ }
870
+ return -1
871
+ }
872
+
873
+ /**
874
+ * Re-decide at `nowMs`: give up frames long unanswered, then fall, cut
875
+ * detail, restore it or rise, at most one change, with the hysteresis the
876
+ * module header describes. Nothing is decided before the first
877
+ * acknowledgement, nor at a fixed rate.
878
+ */
879
+ tick(nowMs: f64): void {
880
+ if (!rateFinite(nowMs)) {
881
+ this.publish()
882
+ return
883
+ }
884
+ const wait: f64 = rateAckTimeoutMs + 4.0 * this.rttMs
885
+ if (this.stale > 0 && nowMs - this.staleSinceMs > wait) {
886
+ // Their answers were lost too; with nothing drawn since, the next answer is a new frame's.
887
+ this.stale = 0
888
+ }
889
+ while (this.unacked() > 0 && nowMs - this.sentMs[this.head] > wait) {
890
+ this.head = this.head + 1
891
+ this.lost = this.lost + 1
892
+ this.stale = this.stale + 1
893
+ this.staleSinceMs = nowMs
894
+ }
895
+ this.ageRate(nowMs)
896
+ if (this.fixedFps === 0 && this.haveAck) {
897
+ this.decide(nowMs)
898
+ }
899
+ this.publish()
900
+ }
901
+
902
+ /** `tick`'s choosing, past its guards. */
903
+ decide(nowMs: f64): void {
904
+ const settled: boolean = nowMs - this.changedMs >= rateSettleMs
905
+ if (this.load(this.tier, this.level, nowMs) > 1.0) {
906
+ this.underSinceMs = -1.0
907
+ if (this.overSinceMs < 0.0) {
908
+ this.overSinceMs = nowMs
909
+ }
910
+ if (settled && nowMs - this.overSinceMs >= rateDropAfterMs) {
911
+ this.stepDown(nowMs)
912
+ }
913
+ return
914
+ }
915
+ this.overSinceMs = -1.0
916
+ const up: i32 = this.stepUp()
917
+ if (up < 0 || this.load((up / 8) | 0, up % 8, nowMs) > rateRiseMargin) {
918
+ this.underSinceMs = -1.0
919
+ return
920
+ }
921
+ if (this.underSinceMs < 0.0) {
922
+ this.underSinceMs = nowMs
923
+ }
924
+ if (settled && nowMs - this.underSinceMs >= rateRiseAfterMs) {
925
+ this.moveTo((up / 8) | 0, up % 8, nowMs)
926
+ }
927
+ }
928
+
929
+ /** Copy the state into `current` and `measured`. */
930
+ publish(): void {
931
+ const d: RateDecision = this.current
932
+ d.fps = this.tier
933
+ d.resolutionDivisor = this.level >= 2 ? 4 : this.level + 1
934
+ d.backendCap = this.level >= 3 ? BACKEND_CAP_HALFBLOCK : BACKEND_CAP_KITTY
935
+ d.palette256 = this.level >= rateMaxLevel
936
+ const s: TermStats = this.measured
937
+ s.tier = this.tier
938
+ s.backend = d.backendCap
939
+ s.rttMs = this.rttMs
940
+ s.bytesPerSec = this.bytesPerSec
941
+ s.frameCostMs = this.costMs
942
+ s.dropped = this.dropped
943
+ s.unacked = this.unacked()
944
+ s.lost = this.lost
945
+ }
946
+
947
+ /** How to draw the next frame, as of the last `tick`. */
948
+ decision(): RateDecision {
949
+ return this.current
950
+ }
951
+
952
+ /** What has been measured, as of the last `tick`. */
953
+ stats(): TermStats {
954
+ return this.measured
955
+ }
956
+ }
957
+
958
+ /** `x` rounded to a whole number, for the overlay. */
959
+ const rateWhole = (x: f64): i32 => {
960
+ if (!(x > -1000000000.0 && x < 1000000000.0)) {
961
+ // NaN and the far ends, which do not fit in an i32, show as 0 and the clamp.
962
+ return x > 0.0 ? 1000000000 : 0
963
+ }
964
+ return toI32(Math.round(x))
965
+ }
966
+
967
+ /** The short name of a `BACKEND_CAP_*`. */
968
+ const rateBackendName = (cap: i32): string => {
969
+ if (cap === BACKEND_CAP_HALFBLOCK) {
970
+ return "half"
971
+ }
972
+ if (cap === BACKEND_CAP_SIXEL) {
973
+ return "sixel"
974
+ }
975
+ return cap === BACKEND_CAP_KITTY ? "full" : "ascii"
976
+ }
977
+
978
+ /**
979
+ * Write `stats` as two short lines, inverse, into `corner` (a `CORNER_*`) of
980
+ * `screen`: the rate, round trip and kilobytes a second, then the frame cost,
981
+ * the frames dropped and the backend cap. A screen too small clips them.
982
+ */
983
+ export const drawStatsOverlay = (screen: Screen, stats: TermStats, corner: i32): void => {
984
+ const top: string = `${stats.tier}fps ${rateWhole(stats.rttMs)}ms ${rateWhole(stats.bytesPerSec / 1000.0)}kB/s`
985
+ const bottom: string = `${rateWhole(stats.frameCostMs)}ms drop ${stats.dropped} ${rateBackendName(stats.backend)}`
986
+ const width: i32 = toI32(top.length > bottom.length ? top.length : bottom.length)
987
+ const right: boolean = corner === CORNER_TOP_RIGHT || corner === CORNER_BOTTOM_RIGHT
988
+ const below: boolean = corner === CORNER_BOTTOM_LEFT || corner === CORNER_BOTTOM_RIGHT
989
+ const x: i32 = right && screen.width > width ? screen.width - width : 0
990
+ const y: i32 = below && screen.height > 2 ? screen.height - 2 : 0
991
+ screen.text(x, y, top, COLOR_DEFAULT, COLOR_DEFAULT, ATTR_INVERSE)
992
+ screen.text(x, y + 1, bottom, COLOR_DEFAULT, COLOR_DEFAULT, ATTR_INVERSE)
993
+ }