@amritk/nish-x86_64-linux 0.27.2 → 0.28.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 +13 -8
- package/bin/nish +0 -0
- package/docs/AI.md +19 -2
- package/package.json +1 -1
- package/runtime/core.d.ts +47 -0
- package/runtime/core.mjs +67 -0
- package/runtime/nish.d.ts +49 -0
- package/runtime/nish.h +50 -5
- package/runtime/nish.mjs +6 -0
- package/runtime/runtime-checksum.c +270 -0
- package/runtime/runtime-parallel.c +21 -0
- package/runtime/runtime-sandbox.c +470 -0
- package/runtime/runtime-sha256.c +239 -0
- package/scripts/build.sh +44 -23
- package/std/README.md +15 -10
- package/std/compress/checksum.ts +88 -75
- package/std/compress/deflate.ts +224 -0
- package/std/compress/gzip.ts +52 -0
- package/std/crypto/bcrypt.ts +816 -280
- package/std/json-equal.ts +335 -0
- package/std/json-format.ts +1070 -0
- package/std/json-number.ts +428 -0
- package/std/json-pointer.ts +245 -0
- package/std/json-value.ts +510 -17
- package/std/net/http-compress.ts +259 -16
- package/std/net/http1-client.ts +104 -19
- package/std/net/http1-server.ts +61 -12
- package/std/net/postgres.ts +405 -0
- package/std/net/websocket.ts +295 -14
- package/std/regex.ts +613 -52
- package/std/term-io.ts +35 -8
- package/std/yaml-json.ts +841 -0
package/std/net/http1-server.ts
CHANGED
|
@@ -115,7 +115,9 @@
|
|
|
115
115
|
import { Secret } from "nish:secret"
|
|
116
116
|
import { copyBytes } from "nish/bytes"
|
|
117
117
|
import {
|
|
118
|
+
HTTP_CODING_GZIP,
|
|
118
119
|
HTTP_CODING_IDENTITY,
|
|
120
|
+
HTTP_CODING_NONE,
|
|
119
121
|
HTTP_OFFER_GZIP,
|
|
120
122
|
HttpCompressor,
|
|
121
123
|
httpAcceptEncoding,
|
|
@@ -559,6 +561,11 @@ export class Http1Connection {
|
|
|
559
561
|
inputEnded: boolean = false
|
|
560
562
|
/** A head has begun to arrive and is not read whole yet: `headSince` is its deadline's start. */
|
|
561
563
|
headTimed: boolean = false
|
|
564
|
+
/**
|
|
565
|
+
* This connection's TCP maximum segment size, for `respondEncoded` under
|
|
566
|
+
* `HttpCompressor.segmentMode`; 0, the default, uses the compressor's `mss`.
|
|
567
|
+
*/
|
|
568
|
+
mss: i32 = 0
|
|
562
569
|
|
|
563
570
|
/** A connection under `config`. A cap outside what the connection can work with panics. */
|
|
564
571
|
constructor(config: Http1Config) {
|
|
@@ -1221,6 +1228,19 @@ export class Http1Connection {
|
|
|
1221
1228
|
* gzip. Allocates nothing.
|
|
1222
1229
|
*/
|
|
1223
1230
|
acceptEncoding(): i32 {
|
|
1231
|
+
return this.acceptEncodingOf(HTTP_OFFER_GZIP)
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/**
|
|
1235
|
+
* Whether the current request's `Accept-Encoding` refuses the body as it
|
|
1236
|
+
* is (`httpIdentityRefused`); `respondEncoded` then gzips any body it can.
|
|
1237
|
+
*/
|
|
1238
|
+
identityRefused(): boolean {
|
|
1239
|
+
return this.acceptEncodingOf(toI32(0)) === HTTP_CODING_NONE
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/** `httpAcceptEncoding` of the current request's `Accept-Encoding`, with `offered`. */
|
|
1243
|
+
acceptEncodingOf(offered: i32): i32 {
|
|
1224
1244
|
const parser: Http1Parser = this.parser
|
|
1225
1245
|
const i: i32 = parser.headerIndex("accept-encoding")
|
|
1226
1246
|
const spans: i32[] = parser.spans
|
|
@@ -1232,7 +1252,7 @@ export class Http1Connection {
|
|
|
1232
1252
|
if (start < 0 || start > end || end > toI32(parser.head.length)) {
|
|
1233
1253
|
return HTTP_CODING_IDENTITY
|
|
1234
1254
|
}
|
|
1235
|
-
return httpAcceptEncoding(parser.head, start, end,
|
|
1255
|
+
return httpAcceptEncoding(parser.head, start, end, offered)
|
|
1236
1256
|
}
|
|
1237
1257
|
|
|
1238
1258
|
/**
|
|
@@ -1240,9 +1260,15 @@ export class Http1Connection {
|
|
|
1240
1260
|
* call: `respond`, `write` and `end`, the body compressed by `enc` when
|
|
1241
1261
|
* `coding` (from `acceptEncoding`) is gzip and `enc.compress` takes it,
|
|
1242
1262
|
* with `Content-Encoding: gzip` and the compressed `Content-Length`, and
|
|
1243
|
-
* otherwise sent as it is; `Vary: Accept-Encoding`
|
|
1244
|
-
*
|
|
1245
|
-
*
|
|
1263
|
+
* otherwise sent as it is; `Vary: Accept-Encoding` while `enc.vary` is set
|
|
1264
|
+
* and the choice turned on the request (`HttpCompressor.varies`). Under
|
|
1265
|
+
* `enc.segmentMode` the compressed response goes out only when, head
|
|
1266
|
+
* included, it takes fewer segments of `mss` bytes (this connection's, or
|
|
1267
|
+
* `enc.mss`) than the body as it is; `enc.decision` says what was chosen.
|
|
1268
|
+
* A request that refuses identity (`identityRefused`) is sent gzip whatever
|
|
1269
|
+
* the length, the saving or the segments (`enc.compressAnyway`).
|
|
1270
|
+
* The program's fields may not name `Content-Encoding` or, with
|
|
1271
|
+
* `enc.vary`, `Vary`. The whole response is written or none of
|
|
1246
1272
|
* it: `H1_AGAIN` when the output has no room for it yet (`H1_WRITE` says
|
|
1247
1273
|
* when to try again, compressing again), `H1_INVALID` for a response
|
|
1248
1274
|
* larger than the output could ever hold or one `respond` refuses,
|
|
@@ -1263,7 +1289,10 @@ export class Http1Connection {
|
|
|
1263
1289
|
if (!this.answerable()) {
|
|
1264
1290
|
return H1_CLOSED
|
|
1265
1291
|
}
|
|
1266
|
-
const
|
|
1292
|
+
const required: boolean = coding === HTTP_CODING_GZIP && this.identityRefused()
|
|
1293
|
+
let packed: i32 = required
|
|
1294
|
+
? enc.compressAnyway(coding, body, off, len)
|
|
1295
|
+
: enc.compress(coding, body, off, len)
|
|
1267
1296
|
let n: i32 = len
|
|
1268
1297
|
if (packed >= 0) {
|
|
1269
1298
|
enc.addFields(names, values)
|
|
@@ -1278,18 +1307,28 @@ export class Http1Connection {
|
|
|
1278
1307
|
this.room()
|
|
1279
1308
|
const outputEnd: i32 = this.outputEnd
|
|
1280
1309
|
const closeAfter: boolean = this.closeAfter
|
|
1281
|
-
|
|
1310
|
+
let r: i32 = this.respond(status, enc.names, enc.values, n)
|
|
1282
1311
|
if (r !== 0) {
|
|
1283
1312
|
return r
|
|
1284
1313
|
}
|
|
1314
|
+
if (packed >= 0 && enc.segmentMode && !required) {
|
|
1315
|
+
// The head just written, and the one the body as it is would get.
|
|
1316
|
+
const head: i32 = this.outputEnd - outputEnd
|
|
1317
|
+
const plain: i32 = enc.http1IdentityHead(head, packed, len)
|
|
1318
|
+
if (!enc.fewerSegments(plain + len, head + packed, this.mss)) {
|
|
1319
|
+
this.unrespond(outputEnd, closeAfter)
|
|
1320
|
+
enc.identityFields(names, values)
|
|
1321
|
+
packed = -1
|
|
1322
|
+
n = len
|
|
1323
|
+
r = this.respond(status, enc.names, enc.values, n)
|
|
1324
|
+
if (r !== 0) {
|
|
1325
|
+
return r
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1285
1329
|
if (!this.responseEmpty && n > this.bodyRoom()) {
|
|
1286
1330
|
const fits: boolean = this.outputEnd - outputEnd + n <= this.config.outputSize - H1_CONTROL_RESERVE
|
|
1287
|
-
this.outputEnd
|
|
1288
|
-
this.closeAfter = closeAfter
|
|
1289
|
-
this.response = H1_R_NONE
|
|
1290
|
-
this.responseFraming = 0
|
|
1291
|
-
this.responseLeft = 0
|
|
1292
|
-
this.responseEmpty = false
|
|
1331
|
+
this.unrespond(outputEnd, closeAfter)
|
|
1293
1332
|
if (!fits) {
|
|
1294
1333
|
return H1_INVALID
|
|
1295
1334
|
}
|
|
@@ -1303,6 +1342,16 @@ export class Http1Connection {
|
|
|
1303
1342
|
return this.end()
|
|
1304
1343
|
}
|
|
1305
1344
|
|
|
1345
|
+
/** Takes back a head `respond` just queued: `outputEnd` and `closeAfter` as they were before it. */
|
|
1346
|
+
unrespond(outputEnd: i32, closeAfter: boolean): void {
|
|
1347
|
+
this.outputEnd = outputEnd
|
|
1348
|
+
this.closeAfter = closeAfter
|
|
1349
|
+
this.response = H1_R_NONE
|
|
1350
|
+
this.responseFraming = 0
|
|
1351
|
+
this.responseLeft = 0
|
|
1352
|
+
this.responseEmpty = false
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1306
1355
|
// ---- WebSocket ----------------------------------------------------------------
|
|
1307
1356
|
|
|
1308
1357
|
/**
|
package/std/net/postgres.ts
CHANGED
|
@@ -142,6 +142,17 @@
|
|
|
142
142
|
* take writes into may still grow, up to its `maxOutput`); when to take is
|
|
143
143
|
* the program's.
|
|
144
144
|
*
|
|
145
|
+
* **Batch timing.** `PgBatchClock` is one answer to when: given the time in
|
|
146
|
+
* microseconds, `check(now, inFlight)` says a batch is due once
|
|
147
|
+
* max(`minCount`, the moving average of recent batch sizes × `factor`) items
|
|
148
|
+
* wait, once the oldest has waited `maxWaitUs`, or, with the idle bypass on,
|
|
149
|
+
* at once when nothing is in flight and fewer than `lightCount` arrived in the
|
|
150
|
+
* last `lightWindowUs`; `average`, `lastSize`, `lastReason` and `startsFor`
|
|
151
|
+
* are its metrics. Its defaults are the fixed window. `PgBatcher.takeDue`
|
|
152
|
+
* takes only when its clock says so, and `PgBatchTurns` lets one free
|
|
153
|
+
* connection collect while the others execute, woken only when its batch is
|
|
154
|
+
* due. Neither reads a clock or allocates per item.
|
|
155
|
+
*
|
|
145
156
|
* Written from PostgreSQL's "Frontend/Backend Protocol" chapter (version 18)
|
|
146
157
|
* and RFC 5802 and RFC 7677; not ported from another implementation.
|
|
147
158
|
*/
|
|
@@ -2746,6 +2757,15 @@ export class PgPool {
|
|
|
2746
2757
|
return this.queueCount
|
|
2747
2758
|
}
|
|
2748
2759
|
|
|
2760
|
+
/** The number of groups in flight on every connection, sent and not yet done: the `inFlight` a `PgBatchClock` reads. */
|
|
2761
|
+
inFlight(): i32 {
|
|
2762
|
+
let n: i32 = 0
|
|
2763
|
+
for (const slot of this.slots) {
|
|
2764
|
+
n += slot.count
|
|
2765
|
+
}
|
|
2766
|
+
return n
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2749
2769
|
/**
|
|
2750
2770
|
* Hands the oldest waiting request to a ready connection with room (the
|
|
2751
2771
|
* one with the fewest in flight, or under `PgConfig.pack` the first), and
|
|
@@ -3299,6 +3319,29 @@ export class PgBatcher {
|
|
|
3299
3319
|
return true
|
|
3300
3320
|
}
|
|
3301
3321
|
|
|
3322
|
+
/**
|
|
3323
|
+
* `take(conn)` when `clock` says a batch is due at `nowUs`, with `inFlight`
|
|
3324
|
+
* of the program's executions or groups running (`PgPool.inFlight()`), and
|
|
3325
|
+
* the batch taken recorded in `clock`; -1 when it is not due or `take`
|
|
3326
|
+
* answers -1. The program calls `clock.arrived(now)` for each request it
|
|
3327
|
+
* adds. Panics when the clock's waiting count is not this batcher's, or its
|
|
3328
|
+
* `maxCount` is below `maxRequests`.
|
|
3329
|
+
*/
|
|
3330
|
+
takeDue(conn: PgConnection, clock: PgBatchClock, nowUs: i64, inFlight: i32): i32 {
|
|
3331
|
+
if (clock.waiting() !== this.count || clock.maxCount < this.maxRequests) {
|
|
3332
|
+
panic("nish/net/postgres: PgBatcher.takeDue with a clock that does not mirror the batcher")
|
|
3333
|
+
}
|
|
3334
|
+
if (clock.check(nowUs, inFlight) === PG_CLOCK_WAIT) {
|
|
3335
|
+
return -1
|
|
3336
|
+
}
|
|
3337
|
+
const before: i32 = this.count
|
|
3338
|
+
const r: i32 = this.take(conn)
|
|
3339
|
+
if (r >= 0) {
|
|
3340
|
+
clock.started(before - this.count, nowUs)
|
|
3341
|
+
}
|
|
3342
|
+
return r
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3302
3345
|
/**
|
|
3303
3346
|
* Binds the oldest waiting requests on `conn` as one execution of `sql`,
|
|
3304
3347
|
* prepared there already, and answers its run's handle, which names this
|
|
@@ -3553,3 +3596,365 @@ export class PgBatcher {
|
|
|
3553
3596
|
return PG_BATCH_NONE
|
|
3554
3597
|
}
|
|
3555
3598
|
}
|
|
3599
|
+
|
|
3600
|
+
// --- Batch timing --------------------------------------------------------------
|
|
3601
|
+
|
|
3602
|
+
/** `PgBatchClock.check`: not yet; wait for an item or for `deadline()`. */
|
|
3603
|
+
export const PG_CLOCK_WAIT: i32 = 0
|
|
3604
|
+
/** `PgBatchClock.check`: `minCount` items are waiting, and the average asks no more. */
|
|
3605
|
+
export const PG_CLOCK_COUNT: i32 = 1
|
|
3606
|
+
/** `PgBatchClock.check`: the moving average of recent batches, times `factor`, is waiting. */
|
|
3607
|
+
export const PG_CLOCK_AVERAGE: i32 = 2
|
|
3608
|
+
/** `PgBatchClock.check`: the oldest item has waited `maxWaitUs`. */
|
|
3609
|
+
export const PG_CLOCK_TIMER: i32 = 3
|
|
3610
|
+
/** `PgBatchClock.check`: nothing is in flight and little has arrived lately, so the item goes at once. */
|
|
3611
|
+
export const PG_CLOCK_IDLE: i32 = 4
|
|
3612
|
+
|
|
3613
|
+
const PG_CLOCK_MAX_COUNT: i32 = 1048576
|
|
3614
|
+
const PG_CLOCK_MAX_LIGHT: i32 = 1024
|
|
3615
|
+
// A minute: a longer wait is a stalled program, not a batching policy.
|
|
3616
|
+
const PG_CLOCK_MAX_WAIT: i64 = 60000000
|
|
3617
|
+
const PG_CLOCK_MAX_FACTOR: f64 = 64.0
|
|
3618
|
+
|
|
3619
|
+
/**
|
|
3620
|
+
* When a `PgBatchClock` starts a batch. The defaults are the fixed window:
|
|
3621
|
+
* a batch starts when `maxCount` items wait or the oldest has waited
|
|
3622
|
+
* `maxWaitUs`, whatever the load. To adapt, set `factor` (1.1 is a good
|
|
3623
|
+
* start), lower `minCount` (16), and turn on the idle bypass (`lightCount`
|
|
3624
|
+
* 8); `bench/pg-batch-clock.ts` measures each against the fixed windows.
|
|
3625
|
+
*/
|
|
3626
|
+
export class PgBatchClockConfig {
|
|
3627
|
+
/** The share of the recent batch size that must wait before a batch starts, 0 to 64; 0 leaves the average out, the fixed window. Above 1 batches grow until the timer bounds them (fewer statements, more wait); below 1 they shrink toward `minCount`. */
|
|
3628
|
+
factor: f64 = 0.0
|
|
3629
|
+
/** How far each batch moves the average, over 0 up to 1. Higher follows a change of load sooner and jumps more on one odd batch. */
|
|
3630
|
+
weight: f64 = 0.25
|
|
3631
|
+
/** The longest the oldest item waits for others, in microseconds, 0 to a minute: the most latency batching adds when the load is light. */
|
|
3632
|
+
maxWaitUs: i64 = 10000
|
|
3633
|
+
/** The window `lightCount` counts arrivals over, in microseconds, 0 to a minute. */
|
|
3634
|
+
lightWindowUs: i64 = 10000
|
|
3635
|
+
/** The most items one batch takes, `PgBatchConfig.maxRequests` for a batcher's clock. The count to start never passes it, so a backlog always starts by count. */
|
|
3636
|
+
maxCount: i32 = 32
|
|
3637
|
+
/** The fewest items that start a batch by count. Lower answers sooner under light load and spends more statements per item; `maxCount` is the fixed window. */
|
|
3638
|
+
minCount: i32 = 32
|
|
3639
|
+
/** The idle bypass: with nothing in flight and fewer than `lightCount` items arrived in the last `lightWindowUs`, an item goes at once. 0, the default, turns it off; up to 1024. */
|
|
3640
|
+
lightCount: i32 = 0
|
|
3641
|
+
/** The most items waiting, 1 to 2^20: `PgBatchConfig.queueSize` for a batcher's clock. */
|
|
3642
|
+
queueSize: i32 = 1024
|
|
3643
|
+
}
|
|
3644
|
+
|
|
3645
|
+
/**
|
|
3646
|
+
* Says when to start a batch, sans-IO like `PgBatcher`: the program passes
|
|
3647
|
+
* the time, in microseconds from any fixed point, and the clock reads none.
|
|
3648
|
+
* It keeps the arrival time of each waiting item, oldest first, and the
|
|
3649
|
+
* moving average (EWMA) of the batches started. `check` answers the reason a
|
|
3650
|
+
* batch should start now, or `PG_CLOCK_WAIT`: the target count is waiting
|
|
3651
|
+
* (the larger of `minCount` and the average times `factor`, never more than
|
|
3652
|
+
* `maxCount`), the oldest has waited `maxWaitUs`, or, with the idle bypass on,
|
|
3653
|
+
* nothing is in flight and the load is light. `deadline()` is when the timer
|
|
3654
|
+
* fires, for a poll's timeout.
|
|
3655
|
+
*
|
|
3656
|
+
* Under a backlog the target is already waiting, so the batch starts by count
|
|
3657
|
+
* the moment a connection is free, and nothing waits on the timer; a batch
|
|
3658
|
+
* started by count is at least the target, so the average does not fall
|
|
3659
|
+
* while the backlog lasts. Only a batch the timer or the bypass starts can
|
|
3660
|
+
* be smaller than the average and pull it down, as the load falls.
|
|
3661
|
+
*
|
|
3662
|
+
* The program mirrors its queue: `arrived(now)` for each item queued,
|
|
3663
|
+
* `started(size, now)` for each batch taken, oldest items first. A
|
|
3664
|
+
* `PgBatcher` does both itself through `takeDue`; a program forming its own
|
|
3665
|
+
* batches calls them by hand. The fields after a start are for metrics.
|
|
3666
|
+
* Everything is allocated in the constructor; nothing per item.
|
|
3667
|
+
*/
|
|
3668
|
+
export class PgBatchClock {
|
|
3669
|
+
/** The moving average of the batch sizes started, 0 before the first. */
|
|
3670
|
+
average: f64 = 0.0
|
|
3671
|
+
/** The size of the last batch started, 0 before the first. */
|
|
3672
|
+
lastSize: i32 = 0
|
|
3673
|
+
/** Why the last batch started: the reason `check` last answered, `PG_CLOCK_WAIT` when it started without one. */
|
|
3674
|
+
lastReason: i32 = 0
|
|
3675
|
+
/** How long the oldest item of the last batch waited, in microseconds. */
|
|
3676
|
+
lastWaitUs: i64 = 0
|
|
3677
|
+
readonly maxCount: i32
|
|
3678
|
+
private readonly minCount: i32
|
|
3679
|
+
private readonly factor: f64
|
|
3680
|
+
private readonly weight: f64
|
|
3681
|
+
private readonly maxWaitUs: i64
|
|
3682
|
+
private readonly lightWindowUs: i64
|
|
3683
|
+
// The waiting items' arrival times, a ring from `head`; and the last
|
|
3684
|
+
// `lightCount` arrivals', a ring of their own, `lightAt` the next to write.
|
|
3685
|
+
private readonly times: i64[]
|
|
3686
|
+
private readonly light: i64[]
|
|
3687
|
+
private readonly starts: i64[]
|
|
3688
|
+
private head: i32 = 0
|
|
3689
|
+
private count: i32 = 0
|
|
3690
|
+
private lightAt: i32 = 0
|
|
3691
|
+
private lightSeen: i32 = 0
|
|
3692
|
+
private last: i64 = 0
|
|
3693
|
+
private due: i32 = 0
|
|
3694
|
+
|
|
3695
|
+
/** A clock for `config`; panics when a setting is out of the range its field names. */
|
|
3696
|
+
constructor(config: PgBatchClockConfig) {
|
|
3697
|
+
const factor: f64 = config.factor
|
|
3698
|
+
const weight: f64 = config.weight
|
|
3699
|
+
if (
|
|
3700
|
+
config.maxCount < 1 ||
|
|
3701
|
+
config.maxCount > PG_CLOCK_MAX_COUNT ||
|
|
3702
|
+
config.minCount < 1 ||
|
|
3703
|
+
config.minCount > config.maxCount ||
|
|
3704
|
+
!(factor >= 0.0 && factor <= PG_CLOCK_MAX_FACTOR) ||
|
|
3705
|
+
!(weight > 0.0 && weight <= 1.0) ||
|
|
3706
|
+
config.maxWaitUs < 0 ||
|
|
3707
|
+
config.maxWaitUs > PG_CLOCK_MAX_WAIT ||
|
|
3708
|
+
config.lightCount < 0 ||
|
|
3709
|
+
config.lightCount > PG_CLOCK_MAX_LIGHT ||
|
|
3710
|
+
config.lightWindowUs < 0 ||
|
|
3711
|
+
config.lightWindowUs > PG_CLOCK_MAX_WAIT ||
|
|
3712
|
+
config.queueSize < 1 ||
|
|
3713
|
+
config.queueSize > PG_BATCH_MAX_QUEUE
|
|
3714
|
+
) {
|
|
3715
|
+
panic("nish/net/postgres: a PgBatchClockConfig setting out of range")
|
|
3716
|
+
}
|
|
3717
|
+
this.maxCount = config.maxCount
|
|
3718
|
+
this.minCount = config.minCount
|
|
3719
|
+
this.factor = factor
|
|
3720
|
+
this.weight = weight
|
|
3721
|
+
this.maxWaitUs = config.maxWaitUs
|
|
3722
|
+
this.lightWindowUs = config.lightWindowUs
|
|
3723
|
+
this.times = new Array<i64>(config.queueSize)
|
|
3724
|
+
this.light = new Array<i64>(config.lightCount)
|
|
3725
|
+
this.starts = new Array<i64>(5)
|
|
3726
|
+
}
|
|
3727
|
+
|
|
3728
|
+
/** The number of items waiting. */
|
|
3729
|
+
waiting(): i32 {
|
|
3730
|
+
return this.count
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
/**
|
|
3734
|
+
* Records an item queued at `nowUs`; false when `queueSize` are waiting. A
|
|
3735
|
+
* time before the last one given counts as the last one, so a clock that
|
|
3736
|
+
* steps back cannot make an item older than its neighbours.
|
|
3737
|
+
*/
|
|
3738
|
+
arrived(nowUs: i64): boolean {
|
|
3739
|
+
const ring: i32 = toI32(this.times.length)
|
|
3740
|
+
if (this.count === ring) {
|
|
3741
|
+
return false
|
|
3742
|
+
}
|
|
3743
|
+
const now: i64 = nowUs < this.last ? this.last : nowUs
|
|
3744
|
+
this.last = now
|
|
3745
|
+
this.times[(this.head + this.count) % ring] = now
|
|
3746
|
+
this.count += 1
|
|
3747
|
+
const lights: i32 = toI32(this.light.length)
|
|
3748
|
+
if (lights > 0) {
|
|
3749
|
+
this.light[this.lightAt] = now
|
|
3750
|
+
this.lightAt = (this.lightAt + 1) % lights
|
|
3751
|
+
if (this.lightSeen < lights) {
|
|
3752
|
+
this.lightSeen += 1
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
return true
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3758
|
+
/** The count that starts a batch now: the larger of `minCount` and the average times `factor`, at most `maxCount`. */
|
|
3759
|
+
target(): i32 {
|
|
3760
|
+
const scaled: f64 = Math.ceil(this.average * this.factor)
|
|
3761
|
+
let target: i32 = this.minCount
|
|
3762
|
+
if (scaled > toF64(target)) {
|
|
3763
|
+
target = toI32(scaled)
|
|
3764
|
+
}
|
|
3765
|
+
return target < this.maxCount ? target : this.maxCount
|
|
3766
|
+
}
|
|
3767
|
+
|
|
3768
|
+
/**
|
|
3769
|
+
* The reason a batch should start at `nowUs`, with `inFlight` batches (or
|
|
3770
|
+
* groups) still running, or `PG_CLOCK_WAIT`. It only says; the program
|
|
3771
|
+
* starts the batch when a connection is free, then calls `started`.
|
|
3772
|
+
*/
|
|
3773
|
+
check(nowUs: i64, inFlight: i32): i32 {
|
|
3774
|
+
let reason: i32 = PG_CLOCK_WAIT
|
|
3775
|
+
if (this.count > 0) {
|
|
3776
|
+
const target: i32 = this.target()
|
|
3777
|
+
if (this.count >= target) {
|
|
3778
|
+
reason = target > this.minCount ? PG_CLOCK_AVERAGE : PG_CLOCK_COUNT
|
|
3779
|
+
} else if (nowUs - this.times[this.head] >= this.maxWaitUs) {
|
|
3780
|
+
reason = PG_CLOCK_TIMER
|
|
3781
|
+
} else if (inFlight <= 0 && this.quiet(nowUs)) {
|
|
3782
|
+
reason = PG_CLOCK_IDLE
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
this.due = reason
|
|
3786
|
+
return reason
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3789
|
+
/** When the oldest waiting item has waited `maxWaitUs`, in the caller's microseconds; -1 when none waits. */
|
|
3790
|
+
deadline(): i64 {
|
|
3791
|
+
if (this.count === 0) {
|
|
3792
|
+
return -1
|
|
3793
|
+
}
|
|
3794
|
+
return this.times[this.head] + this.maxWaitUs
|
|
3795
|
+
}
|
|
3796
|
+
|
|
3797
|
+
/**
|
|
3798
|
+
* Records a batch of the `size` oldest items started at `nowUs`, and moves
|
|
3799
|
+
* the average toward it; the first batch sets it. Panics when `size` is
|
|
3800
|
+
* below 1 or more than are waiting or than `maxCount`.
|
|
3801
|
+
*/
|
|
3802
|
+
started(size: i32, nowUs: i64): void {
|
|
3803
|
+
if (size < 1 || size > this.count || size > this.maxCount) {
|
|
3804
|
+
panic("nish/net/postgres: PgBatchClock.started with a size out of range")
|
|
3805
|
+
}
|
|
3806
|
+
this.lastWaitUs = nowUs - this.times[this.head]
|
|
3807
|
+
if (this.lastWaitUs < 0) {
|
|
3808
|
+
this.lastWaitUs = 0
|
|
3809
|
+
}
|
|
3810
|
+
this.head = (this.head + size) % toI32(this.times.length)
|
|
3811
|
+
this.count -= size
|
|
3812
|
+
const n: f64 = toF64(size)
|
|
3813
|
+
this.average = this.lastSize === 0 ? n : this.average + this.weight * (n - this.average)
|
|
3814
|
+
this.lastSize = size
|
|
3815
|
+
this.lastReason = this.due
|
|
3816
|
+
this.starts[this.due] += 1
|
|
3817
|
+
this.due = PG_CLOCK_WAIT
|
|
3818
|
+
}
|
|
3819
|
+
|
|
3820
|
+
/** How many batches have started for `reason`, one of the `PG_CLOCK_*` (`PG_CLOCK_WAIT` counts those started without a due `check`). */
|
|
3821
|
+
startsFor(reason: i32): i64 {
|
|
3822
|
+
if (reason < 0 || reason > PG_CLOCK_IDLE) {
|
|
3823
|
+
panic("nish/net/postgres: PgBatchClock.startsFor with a reason out of range")
|
|
3824
|
+
}
|
|
3825
|
+
return this.starts[reason]
|
|
3826
|
+
}
|
|
3827
|
+
|
|
3828
|
+
/** Whether fewer than `lightCount` items arrived in the `lightWindowUs` before `nowUs`. */
|
|
3829
|
+
private quiet(nowUs: i64): boolean {
|
|
3830
|
+
const lights: i32 = toI32(this.light.length)
|
|
3831
|
+
if (lights === 0) {
|
|
3832
|
+
return false
|
|
3833
|
+
}
|
|
3834
|
+
if (this.lightSeen < lights) {
|
|
3835
|
+
return true
|
|
3836
|
+
}
|
|
3837
|
+
// `lightAt` is the next to write, so it holds the oldest of the last `lightCount`.
|
|
3838
|
+
return nowUs - this.light[this.lightAt] >= this.lightWindowUs
|
|
3839
|
+
}
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
/** `PgBatchTurns`: no slot to wake. */
|
|
3843
|
+
export const PG_TURNS_NONE: i32 = -1
|
|
3844
|
+
/** `PgBatchTurns.push`: the item was not recorded, `queueSize` are waiting. */
|
|
3845
|
+
export const PG_TURNS_FULL: i32 = -2
|
|
3846
|
+
|
|
3847
|
+
/**
|
|
3848
|
+
* Turn-taking for a program with several connections that forms batches
|
|
3849
|
+
* with one `PgBatchClock`: one free connection, the collector, gathers while
|
|
3850
|
+
* the others execute, and a push wakes it only when its batch is due, not on
|
|
3851
|
+
* every item. Sans-IO like the clock: a slot is the program's own number for
|
|
3852
|
+
* a connection, 0 to `size - 1`, and the answer to each call is the slot to
|
|
3853
|
+
* wake, or `PG_TURNS_NONE`.
|
|
3854
|
+
*
|
|
3855
|
+
* The collector is the lowest-numbered free slot. `push(now)` records an item
|
|
3856
|
+
* and answers the collector when this item makes the batch due; `poll(now)`
|
|
3857
|
+
* answers it when the timer has made it due, at `deadline()`. Once a slot is
|
|
3858
|
+
* named, no other is until it calls `started(slot, size, now)` with the batch
|
|
3859
|
+
* it took, or `release(slot)` when it took none. `finished(slot, now)` frees
|
|
3860
|
+
* a slot after its batch. Both `started` and `finished` answer the next slot
|
|
3861
|
+
* to wake when a batch is already due, as under a backlog: nothing waits on
|
|
3862
|
+
* the timer then. The in-flight count the idle bypass reads is the slots
|
|
3863
|
+
* holding a batch.
|
|
3864
|
+
*/
|
|
3865
|
+
export class PgBatchTurns {
|
|
3866
|
+
/** The clock the turns read and record into. */
|
|
3867
|
+
readonly clock: PgBatchClock
|
|
3868
|
+
/** The number of slots. */
|
|
3869
|
+
readonly size: i32
|
|
3870
|
+
private readonly busy: boolean[]
|
|
3871
|
+
private running: i32 = 0
|
|
3872
|
+
private woken: i32 = -1
|
|
3873
|
+
|
|
3874
|
+
/** Turns over `size` slots (1 to 1024), all free; panics on a size out of range. */
|
|
3875
|
+
constructor(clock: PgBatchClock, size: i32) {
|
|
3876
|
+
if (size < 1 || size > PG_POOL_MAX_SLOTS) {
|
|
3877
|
+
panic("nish/net/postgres: a PgBatchTurns size out of range")
|
|
3878
|
+
}
|
|
3879
|
+
this.clock = clock
|
|
3880
|
+
this.size = size
|
|
3881
|
+
this.busy = new Array<boolean>(size)
|
|
3882
|
+
}
|
|
3883
|
+
|
|
3884
|
+
/** The slot that collects: the lowest-numbered free one, or `PG_TURNS_NONE` when every slot holds a batch. */
|
|
3885
|
+
collector(): i32 {
|
|
3886
|
+
for (let i: i32 = 0; i < this.size; i++) {
|
|
3887
|
+
if (!this.busy[i]) {
|
|
3888
|
+
return i
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3891
|
+
return PG_TURNS_NONE
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
/** The number of slots holding a batch. */
|
|
3895
|
+
inFlight(): i32 {
|
|
3896
|
+
return this.running
|
|
3897
|
+
}
|
|
3898
|
+
|
|
3899
|
+
/** When the collector's timer fires, `PgBatchClock.deadline()`; -1 when nothing waits. */
|
|
3900
|
+
deadline(): i64 {
|
|
3901
|
+
return this.clock.deadline()
|
|
3902
|
+
}
|
|
3903
|
+
|
|
3904
|
+
/** Records an item queued at `nowUs` and answers the slot to wake, or `PG_TURNS_FULL` when the clock is full. */
|
|
3905
|
+
push(nowUs: i64): i32 {
|
|
3906
|
+
if (!this.clock.arrived(nowUs)) {
|
|
3907
|
+
return PG_TURNS_FULL
|
|
3908
|
+
}
|
|
3909
|
+
return this.poll(nowUs)
|
|
3910
|
+
}
|
|
3911
|
+
|
|
3912
|
+
/** The slot to wake at `nowUs`, when a batch is due and no slot is named already. */
|
|
3913
|
+
poll(nowUs: i64): i32 {
|
|
3914
|
+
if (this.woken >= 0) {
|
|
3915
|
+
return PG_TURNS_NONE
|
|
3916
|
+
}
|
|
3917
|
+
const slot: i32 = this.collector()
|
|
3918
|
+
if (slot < 0 || this.clock.check(nowUs, this.running) === PG_CLOCK_WAIT) {
|
|
3919
|
+
return PG_TURNS_NONE
|
|
3920
|
+
}
|
|
3921
|
+
this.woken = slot
|
|
3922
|
+
return slot
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3925
|
+
/** Records `slot` starting a batch of `size` items at `nowUs`, and answers the next slot to wake. Panics on a slot out of range or holding a batch already, or as `PgBatchClock.started`. */
|
|
3926
|
+
started(slot: i32, size: i32, nowUs: i64): i32 {
|
|
3927
|
+
this.free(slot)
|
|
3928
|
+
this.clock.started(size, nowUs)
|
|
3929
|
+
this.busy[slot] = true
|
|
3930
|
+
this.running += 1
|
|
3931
|
+
if (this.woken === slot) {
|
|
3932
|
+
this.woken = -1
|
|
3933
|
+
}
|
|
3934
|
+
return this.poll(nowUs)
|
|
3935
|
+
}
|
|
3936
|
+
|
|
3937
|
+
/** Records `slot`'s batch done at `nowUs`, and answers the slot to wake. Panics on a slot out of range or holding no batch. */
|
|
3938
|
+
finished(slot: i32, nowUs: i64): i32 {
|
|
3939
|
+
if (slot < 0 || slot >= this.size || !this.busy[slot]) {
|
|
3940
|
+
panic("nish/net/postgres: PgBatchTurns.finished on a slot holding no batch")
|
|
3941
|
+
}
|
|
3942
|
+
this.busy[slot] = false
|
|
3943
|
+
this.running -= 1
|
|
3944
|
+
return this.poll(nowUs)
|
|
3945
|
+
}
|
|
3946
|
+
|
|
3947
|
+
/** Gives back a wake that started nothing, so the next push or poll can wake a slot again. */
|
|
3948
|
+
release(slot: i32): void {
|
|
3949
|
+
this.free(slot)
|
|
3950
|
+
if (this.woken === slot) {
|
|
3951
|
+
this.woken = -1
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
|
|
3955
|
+
private free(slot: i32): void {
|
|
3956
|
+
if (slot < 0 || slot >= this.size || this.busy[slot]) {
|
|
3957
|
+
panic("nish/net/postgres: PgBatchTurns slot out of range or holding a batch")
|
|
3958
|
+
}
|
|
3959
|
+
}
|
|
3960
|
+
}
|