@amritk/nish-x86_64-linux 0.26.0 → 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.
@@ -81,7 +81,11 @@
81
81
  * A `text` column is the same bytes in either format, so `columnText` and the
82
82
  * column windows read it unchanged. The program knows which of its binds
83
83
  * asked for which, and reads each execution's rows with the matching
84
- * decoders.
84
+ * decoders. A `PgBatcher` with `binary` set opens each batch this way and
85
+ * reads its routing ordinal with `binaryInt`, so its rows are binary too.
86
+ * `PgBinaryArray` walks a one-dimensional `int2[]`, `int4[]` or `int8[]`
87
+ * column in its binary layout, and refuses any other array (a `text[]` or a
88
+ * multi-dimensional one among them).
85
89
  *
86
90
  * **Authentication.** SCRAM-SHA-256 (RFC 5802, RFC 7677) or none (`trust`).
87
91
  * The password arrives as a `Secret<u8[]>`; the connection keeps a copy in a
@@ -138,6 +142,17 @@
138
142
  * take writes into may still grow, up to its `maxOutput`); when to take is
139
143
  * the program's.
140
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
+ *
141
156
  * Written from PostgreSQL's "Frontend/Backend Protocol" chapter (version 18)
142
157
  * and RFC 5802 and RFC 7677; not ported from another implementation.
143
158
  */
@@ -237,6 +252,15 @@ const PG_TAIL: i32 = 17
237
252
  const PG_BINARY_TAIL: i32 = 2
238
253
  // The result format code for binary.
239
254
  const PG_FORMAT_BINARY: i32 = 1
255
+ // The element types `PgBinaryArray` reads, by OID: int8, int2 and int4.
256
+ const PG_OID_INT8: i32 = 20
257
+ const PG_OID_INT2: i32 = 21
258
+ const PG_OID_INT4: i32 = 23
259
+ // The bytes a binary array's header takes before its dimensions: ndim,
260
+ // hasnull and the element OID, one i32 each; one dimension adds its length
261
+ // and lower bound, another two i32s.
262
+ const PG_ARRAY_HEADER: i32 = 12
263
+ const PG_ARRAY_DIM: i32 = 8
240
264
  // The largest `maxMessage` and `maxOutput`. The input buffer doubles up to one
241
265
  // past them; the output buffer doubles only while twice its size fits in an i32.
242
266
  const PG_MAX_CAP: i32 = 1073741823
@@ -858,6 +882,139 @@ export class PgBinary {
858
882
  }
859
883
  }
860
884
 
885
+ /**
886
+ * A walk over a one-dimensional `int2[]`, `int4[]` or `int8[]` column in
887
+ * PostgreSQL's binary array layout, in place: `begin` the column, then
888
+ * `next()` until `PG_ARRAY_END` or `PG_ARRAY_BAD`, and `int()` decodes the
889
+ * current element through `binary`. The header is `ndim`, `hasnull` and the
890
+ * element OID, then for `ndim` 1 the dimension's length and lower bound (the
891
+ * bound is read and not used); each element is a length word and its bytes,
892
+ * and a NULL is a length of -1 with no bytes. `ndim` 0 is the empty array.
893
+ * Every refusal sets `binary.failed`: an `ndim` above 1, another element
894
+ * type, an element whose length is not its type's size, a NULL when
895
+ * `hasnull` is 0, a column cut short, or bytes after the last element. The
896
+ * walk allocates nothing, and it reads nothing outside the column.
897
+ */
898
+ export class PgBinaryArray {
899
+ /** The number of elements the header gives: 0 for an empty array. */
900
+ count: i32 = 0
901
+ /** The element type's OID, 20, 21 or 23, once `begin` has read a header it accepts. */
902
+ elementOid: i32 = 0
903
+ /** The current element's first byte in the buffer. */
904
+ start: i32 = 0
905
+ /** The current element's length: its type's size, or 0 for a NULL. */
906
+ length: i32 = 0
907
+ private binary: PgBinary
908
+ private b: u8[]
909
+ private pos: i32 = 0
910
+ private end: i32 = 0
911
+ private size: i32 = 0
912
+ private seen: i32 = 0
913
+ private hasNull: boolean = false
914
+ private bad: boolean = true
915
+
916
+ constructor(binary: PgBinary) {
917
+ this.binary = binary
918
+ this.b = []
919
+ }
920
+
921
+ /** Starts a walk over `b[start, start + length)`; a header this walker does not read is refused. */
922
+ begin(b: u8[], start: i32, length: i32): void {
923
+ this.b = b
924
+ this.count = 0
925
+ this.elementOid = 0
926
+ this.seen = 0
927
+ this.start = 0
928
+ this.length = 0
929
+ this.bad = !(start >= 0 && length >= PG_ARRAY_HEADER && start <= toI32(b.length) - length)
930
+ if (this.bad) {
931
+ this.refuse()
932
+ return
933
+ }
934
+ const ndim: i32 = getI32BE(b, start)
935
+ const hasNull: i32 = getI32BE(b, start + 4)
936
+ const oid: i32 = getI32BE(b, start + 8)
937
+ this.size = 0
938
+ if (oid === PG_OID_INT8) {
939
+ this.size = 8
940
+ } else if (oid === PG_OID_INT2) {
941
+ this.size = 2
942
+ } else if (oid === PG_OID_INT4) {
943
+ this.size = 4
944
+ }
945
+ this.bad = (ndim !== 0 && ndim !== 1) || (hasNull !== 0 && hasNull !== 1) || this.size === 0
946
+ if (ndim === 1 && !this.bad) {
947
+ this.bad = length < PG_ARRAY_HEADER + PG_ARRAY_DIM
948
+ }
949
+ if (this.bad) {
950
+ this.refuse()
951
+ return
952
+ }
953
+ this.elementOid = oid
954
+ this.hasNull = hasNull === 1
955
+ this.pos = start + PG_ARRAY_HEADER
956
+ this.end = start + length
957
+ if (ndim === 1) {
958
+ const dim: i32 = getI32BE(b, start + PG_ARRAY_HEADER)
959
+ if (dim < 0) {
960
+ this.refuse()
961
+ return
962
+ }
963
+ this.count = dim
964
+ this.pos += PG_ARRAY_DIM
965
+ }
966
+ }
967
+
968
+ private refuse(): i32 {
969
+ this.bad = true
970
+ this.binary.failed = true
971
+ return PG_ARRAY_BAD
972
+ }
973
+
974
+ /** The next element: `PG_ARRAY_ELEMENT`, `PG_ARRAY_NULL`, `PG_ARRAY_END` or `PG_ARRAY_BAD`. */
975
+ next(): i32 {
976
+ if (this.bad) {
977
+ return PG_ARRAY_BAD
978
+ }
979
+ if (this.seen === this.count) {
980
+ if (this.pos === this.end) {
981
+ return PG_ARRAY_END
982
+ }
983
+ return this.refuse()
984
+ }
985
+ if (this.end - this.pos < 4) {
986
+ return this.refuse()
987
+ }
988
+ const n: i32 = getI32BE(this.b, this.pos)
989
+ this.pos += 4
990
+ this.seen += 1
991
+ if (n === -1) {
992
+ if (!this.hasNull) {
993
+ return this.refuse()
994
+ }
995
+ this.start = this.pos
996
+ this.length = 0
997
+ return PG_ARRAY_NULL
998
+ }
999
+ if (n !== this.size || this.end - this.pos < n) {
1000
+ return this.refuse()
1001
+ }
1002
+ this.start = this.pos
1003
+ this.length = n
1004
+ this.pos += n
1005
+ return PG_ARRAY_ELEMENT
1006
+ }
1007
+
1008
+ /** The current element as an integer, through `PgBinary.int`; a NULL, or a walk that was refused, sets `binary.failed`. */
1009
+ int(): i64 {
1010
+ if (this.bad) {
1011
+ this.binary.failed = true
1012
+ return 0
1013
+ }
1014
+ return this.binary.int(this.b, this.start, this.length)
1015
+ }
1016
+ }
1017
+
861
1018
  /**
862
1019
  * A walk over a one-dimensional `text[]` column in place: `start` the
863
1020
  * column, then `next()` until `PG_ARRAY_END` or `PG_ARRAY_BAD`. Each element
@@ -1935,10 +2092,13 @@ export class PgConnection {
1935
2092
  * format rather than text: fixed-size integers, booleans, floats and
1936
2093
  * times, read in place by `binaryInt`, `binaryBool`, `binaryFloat` and
1937
2094
  * `binary.timestamp`. A `text` column's bytes are the same in either
1938
- * format, so `columnText` and the column windows read it as before; a
1939
- * `text[]` or `numeric` column comes in PostgreSQL's binary layout, which
1940
- * no decoder here reads. Its `bindEnd()` writes two bytes more, kept free
1941
- * under `maxOutput` from here on. False as `prepare` is.
2095
+ * format, so `columnText` and the column windows read it as before. An
2096
+ * `int2[]`, `int4[]` or `int8[]` column comes in PostgreSQL's binary layout,
2097
+ * which `PgBinaryArray` reads; a `text[]` or `numeric` column comes in that
2098
+ * layout too, and no decoder here reads it. `PgBatcher` opens its batches
2099
+ * with this when its `binary` is set, and reads their routing ordinals with
2100
+ * `binaryInt`. Its `bindEnd()` writes two bytes more, kept free under
2101
+ * `maxOutput` from here on. False as `prepare` is.
1942
2102
  */
1943
2103
  bindStartBinary(name: string): boolean {
1944
2104
  return this.openBind(name, true)
@@ -2597,6 +2757,15 @@ export class PgPool {
2597
2757
  return this.queueCount
2598
2758
  }
2599
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
+
2600
2769
  /**
2601
2770
  * Hands the oldest waiting request to a ready connection with room (the
2602
2771
  * one with the fewest in flight, or under `PgConfig.pack` the first), and
@@ -2863,6 +3032,8 @@ export class PgBatchConfig {
2863
3032
  queueBytes: i32 = 65536
2864
3033
  /** The most executions in flight at once. */
2865
3034
  runs: i32 = 16
3035
+ /** Whether every row of a batch comes in binary format, read with the binary decoders and its ordinal by `binaryInt`; false for text. */
3036
+ binary: boolean = false
2866
3037
  }
2867
3038
 
2868
3039
  /** One execution's requests, grouped by key, and how far its rows have been routed. */
@@ -2913,7 +3084,10 @@ class PgBatchRun {
2913
3084
  * connection to `fail(run)`; after each, `next(run)` answers events until
2914
3085
  * `PG_BATCH_NONE`, `tag` naming the request each is about. Every row goes to
2915
3086
  * every request for its key, and every request gets one last event:
2916
- * `PG_BATCH_DONE`, `PG_BATCH_MISSING` or `PG_BATCH_FAILED`.
3087
+ * `PG_BATCH_DONE`, `PG_BATCH_MISSING` or `PG_BATCH_FAILED`. A batch with
3088
+ * `binary` set comes in binary format: its rows are read with the binary
3089
+ * decoders, `binaryInt` for each ordinal, and the caller's `binary.failed`
3090
+ * is left as it was.
2917
3091
  *
2918
3092
  * Nothing outlives its execution: a run is free again once the events
2919
3093
  * `end` or `fail` owe are answered, and its handle is then spent. What the
@@ -2960,6 +3134,7 @@ export class PgBatcher {
2960
3134
  private readonly keyType: i32
2961
3135
  private readonly maxRequests: i32
2962
3136
  private readonly maxBytes: i32
3137
+ private readonly binary: boolean
2963
3138
  private storeEnd: i32 = 0
2964
3139
  private head: i32 = 0
2965
3140
  private count: i32 = 0
@@ -2997,6 +3172,7 @@ export class PgBatcher {
2997
3172
  this.keyType = keyType
2998
3173
  this.maxRequests = maxRequests
2999
3174
  this.maxBytes = config.maxBytes
3175
+ this.binary = config.binary
3000
3176
  this.name = config.name
3001
3177
  const type: string = keyType === PG_BATCH_INT ? "int8" : "text"
3002
3178
  const order: string = config.orderBy.length > 0 ? `, ${config.orderBy}` : ""
@@ -3143,6 +3319,29 @@ export class PgBatcher {
3143
3319
  return true
3144
3320
  }
3145
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
+
3146
3345
  /**
3147
3346
  * Binds the oldest waiting requests on `conn` as one execution of `sql`,
3148
3347
  * prepared there already, and answers its run's handle, which names this
@@ -3164,7 +3363,7 @@ export class PgBatcher {
3164
3363
  break
3165
3364
  }
3166
3365
  }
3167
- if (r < 0 || !conn.bindStart(this.name)) {
3366
+ if (r < 0 || !(this.binary ? conn.bindStartBinary(this.name) : conn.bindStart(this.name))) {
3168
3367
  return -1
3169
3368
  }
3170
3369
  if (!conn.arrayStart()) {
@@ -3296,7 +3495,14 @@ export class PgBatcher {
3296
3495
  const last: i32 = conn.columns - 1
3297
3496
  let ordinal: i64 = 0
3298
3497
  let bad: boolean = last < 0 || conn.isNull(last)
3299
- if (!bad) {
3498
+ if (!bad && this.binary) {
3499
+ // Read without touching `binary.failed`, which belongs to the program's own decoding.
3500
+ const failed: boolean = conn.binary.failed
3501
+ conn.binary.failed = false
3502
+ ordinal = conn.binaryInt(last)
3503
+ bad = conn.binary.failed
3504
+ conn.binary.failed = failed
3505
+ } else if (!bad) {
3300
3506
  // Read without touching `text.failed`, which belongs to the program's own decoding.
3301
3507
  const failed: boolean = conn.text.failed
3302
3508
  conn.text.failed = false
@@ -3390,3 +3596,365 @@ export class PgBatcher {
3390
3596
  return PG_BATCH_NONE
3391
3597
  }
3392
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
+ }