@amritk/nish-x86_64-linux 0.18.0 → 0.21.0
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.
- package/INSTALL.md +58 -28
- package/bin/nish +0 -0
- package/docs/AI.md +2759 -0
- package/package.json +3 -2
- package/runtime/browser.d.ts +14 -0
- package/runtime/browser.mjs +88 -0
- package/runtime/core/arrays.d.ts +18 -0
- package/runtime/core/arrays.mjs +125 -0
- package/runtime/core/checks.mjs +20 -0
- package/runtime/core/host.d.ts +70 -0
- package/runtime/core/host.mjs +311 -0
- package/runtime/core/numbers.d.ts +53 -0
- package/runtime/core/numbers.mjs +316 -0
- package/runtime/core/result.d.ts +41 -0
- package/runtime/core/result.mjs +127 -0
- package/runtime/core/strings.d.ts +14 -0
- package/runtime/core/strings.mjs +169 -0
- package/runtime/core.d.ts +9 -0
- package/runtime/core.mjs +55 -0
- package/runtime/host.d.ts +6 -0
- package/runtime/nish.d.ts +99 -5
- package/runtime/nish.h +289 -19
- package/runtime/nish.mjs +39 -3
- package/runtime/node.mjs +383 -0
- package/runtime/runtime-aes.c +490 -0
- package/runtime/runtime-bytes.c +401 -0
- package/runtime/runtime-cross.c +366 -0
- package/runtime/runtime-host.c +8 -7
- package/runtime/runtime-net.c +93 -34
- package/runtime/runtime-os.c +214 -16
- package/runtime/runtime-parallel.c +590 -107
- package/runtime/runtime-simd.c +235 -11
- package/runtime/runtime-term.c +504 -0
- package/runtime/runtime-wasm.c +5 -14
- package/runtime/runtime.c +257 -17
- package/runtime/shim.mjs +6 -915
- package/runtime/sqlite/LICENSE.md +85 -0
- package/runtime/sqlite/README.md +45 -0
- package/runtime/sqlite/nish-sqlite.c +359 -0
- package/runtime/sqlite/sqlite3.c +271131 -0
- package/runtime/sqlite/sqlite3.h +14447 -0
- package/scripts/build.sh +437 -28
- package/std/LICENSE-fast-float +23 -0
- package/std/LICENSE-fdlibm +8 -0
- package/std/README.md +108 -58
- package/std/base64.ts +116 -0
- package/std/bytes.ts +419 -0
- package/std/cli.ts +328 -0
- package/std/collections.ts +6 -5
- package/std/crypto/LICENSE-crypt_blowfish +26 -0
- package/std/crypto/LICENSE-libsecp256k1 +19 -0
- package/std/crypto/aes.ts +238 -76
- package/std/crypto/base64url.ts +125 -0
- package/std/crypto/bcrypt.ts +760 -0
- package/std/crypto/chacha20poly1305.ts +43 -9
- package/std/crypto/ed25519.ts +840 -0
- package/std/crypto/hkdf.ts +304 -12
- package/std/crypto/hmac.ts +382 -54
- package/std/crypto/p256.ts +932 -61
- package/std/crypto/x25519.ts +55 -37
- package/std/crypto/x509-random.ts +71 -0
- package/std/crypto/x509.ts +20 -131
- package/std/json-tokenizer.ts +4237 -0
- package/std/json-typed.ts +1014 -0
- package/std/json-value.ts +1182 -0
- package/std/json.ts +1202 -34
- package/std/math.ts +608 -0
- package/std/net/buffer-pool.ts +151 -0
- package/std/net/hpack.ts +416 -112
- package/std/net/http-fields.ts +93 -0
- package/std/net/http1-server.ts +501 -62
- package/std/net/http1.ts +31 -6
- package/std/net/http2-tls.ts +112 -24
- package/std/net/http2.ts +333 -75
- package/std/net/http3-server.ts +261 -43
- package/std/net/http3.ts +187 -28
- package/std/net/postgres.ts +1875 -0
- package/std/net/prefork.ts +412 -0
- package/std/net/quic-listener.ts +108 -11
- package/std/net/quic-packet.ts +243 -82
- package/std/net/quic-recovery.ts +361 -30
- package/std/net/quic-stream.ts +424 -24
- package/std/net/quic.ts +519 -167
- package/std/net/router.ts +676 -0
- package/std/net/tls/codec.ts +426 -27
- package/std/net/tls/record-server.ts +478 -132
- package/std/net/tls/record.ts +133 -42
- package/std/net/tls/schedule.ts +464 -55
- package/std/net/tls/ticket.ts +610 -0
- package/std/net/tls-tcp.ts +196 -53
- package/std/net/tls.ts +1018 -260
- package/std/net/url.ts +571 -0
- package/std/random.ts +94 -0
- package/std/regex.ts +4274 -0
- package/std/sqlite.ts +391 -0
- package/std/term-io.ts +978 -0
- package/std/term-pixels.ts +874 -0
- package/std/term-probe.ts +459 -0
- package/std/term-rate.ts +993 -0
- package/std/term.ts +2453 -0
- package/std/text.ts +90 -7
- package/std/threads.ts +438 -14
- package/std/yaml-lines.ts +141 -0
- package/std/yaml.ts +4675 -0
package/std/term-rate.ts
ADDED
|
@@ -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
|
+
}
|