@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.
@@ -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, HTTP_OFFER_GZIP)
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` either way while
1244
- * `enc.vary` is set. The program's fields may not name `Content-Encoding`
1245
- * or, with `enc.vary`, `Vary`. The whole response is written or none of
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 packed: i32 = enc.compress(coding, body, off, len)
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
- const r: i32 = this.respond(status, enc.names, enc.values, n)
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 = 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
  /**
@@ -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
+ }