@nxtedition/scheduler 2.0.4 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/lib/index.d.ts +15 -11
  2. package/lib/index.js +116 -68
  3. package/package.json +2 -2
package/lib/index.d.ts CHANGED
@@ -1,25 +1,29 @@
1
+ export type Priority = -3 | -2 | -1 | 0 | 1 | 2 | 3 | 'lowest' | 'lower' | 'low' | 'normal' | 'high' | 'higher' | 'highest';
2
+ export declare function parsePriority(p: Priority | string | number): number;
1
3
  export declare class Scheduler {
2
4
  #private;
3
- static LOW: 0;
4
- static NORMAL: 1;
5
- static HIGH: 2;
5
+ static LOWEST: Priority;
6
+ static LOWER: Priority;
7
+ static LOW: Priority;
8
+ static NORMAL: Priority;
9
+ static HIGH: Priority;
10
+ static HIGHER: Priority;
11
+ static HIGHEST: Priority;
6
12
  get concurrency(): number;
7
13
  get stats(): {
8
14
  deferred: number;
9
15
  running: number;
10
16
  pending: number;
11
- lowCount: number;
12
- normalCount: number;
13
- highCount: number;
14
- lowLimit: number;
15
- normalLimit: number;
16
- highLimit: number;
17
+ queues: {
18
+ count: number;
19
+ age: number;
20
+ }[];
17
21
  };
18
22
  static makeSharedState(concurrency: number): SharedArrayBuffer;
19
23
  constructor(opts: SharedArrayBuffer | {
20
24
  concurrency?: number;
21
25
  });
22
- run<T, U>(fn: (opaque?: U) => Promise<T> | T, priority?: 0 | 1 | 2 | 'low' | 'normal' | 'high', opaque?: U): Promise<T>;
23
- acquire<U>(fn: (opaque?: U) => unknown, priority?: 0 | 1 | 2 | 'low' | 'normal' | 'high', opaque?: U): void;
26
+ run<T, U>(fn: (opaque?: U) => Promise<T> | T, priority?: Priority, opaque?: U): Promise<T>;
27
+ acquire<U>(fn: (opaque?: U) => unknown, priority?: Priority, opaque?: U): void;
24
28
  release(): void;
25
29
  }
package/lib/index.js CHANGED
@@ -1,17 +1,76 @@
1
1
  const RUNNING_INDEX = 0
2
2
  const CONCURRENCY_INDEX = 1
3
3
 
4
+ const maxInt = 2147483647
5
+
6
+ let fastNow = Date.now()
7
+ setInterval(() => {
8
+ fastNow = Date.now()
9
+ }, 1e3).unref()
10
+
4
11
  class FastQueue {
5
12
  idx = 0
6
13
  cnt = 0
7
14
  arr = []
8
- lim = 0
15
+ age = fastNow
16
+ }
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+
34
+ export function parsePriority(p ) {
35
+ if (typeof p === 'number') {
36
+ // Do nothing...
37
+ } else if (p === 'lowest') {
38
+ return -3
39
+ } else if (p === 'lower') {
40
+ return -2
41
+ } else if (p === 'low') {
42
+ return -1
43
+ } else if (p === 'normal') {
44
+ return 0
45
+ } else if (p === 'high') {
46
+ return 1
47
+ } else if (p === 'higher') {
48
+ return 2
49
+ } else if (p === 'highest') {
50
+ return 3
51
+ } else {
52
+ p = Number(p)
53
+ }
54
+
55
+ if (typeof p !== 'number' || Number.isNaN(p)) {
56
+ return 0
57
+ } else if (p < -3) {
58
+ return -3
59
+ } else if (p > 3) {
60
+ return 3
61
+ } else {
62
+ return Math.trunc(p)
63
+ }
9
64
  }
10
65
 
11
66
  export class Scheduler {
12
- static LOW = 0
13
- static NORMAL = 1
14
- static HIGH = 2
67
+ static LOWEST = -3
68
+ static LOWER = -2
69
+ static LOW = -1
70
+ static NORMAL = 0
71
+ static HIGH = 1
72
+ static HIGHER = 2
73
+ static HIGHEST = 3
15
74
 
16
75
  #concurrency
17
76
  #stateView
@@ -22,9 +81,15 @@ export class Scheduler {
22
81
  #releasing = false
23
82
  #deferred = 0
24
83
 
25
- #lowQueue = new FastQueue()
26
- #normalQueue = new FastQueue()
27
- #highQueue = new FastQueue()
84
+ #queues = [
85
+ new FastQueue(), // 0 lowest
86
+ new FastQueue(), // 1 lower
87
+ new FastQueue(), // 2 low
88
+ new FastQueue(), // 3 normal
89
+ new FastQueue(), // 4 high
90
+ new FastQueue(), // 5 higher
91
+ new FastQueue(), // 6 highest
92
+ ]
28
93
 
29
94
  get concurrency() {
30
95
  return this.#concurrency
@@ -35,12 +100,7 @@ export class Scheduler {
35
100
  deferred: this.#deferred,
36
101
  running: this.#running,
37
102
  pending: this.#pending,
38
- lowCount: this.#lowQueue.cnt,
39
- normalCount: this.#normalQueue.cnt,
40
- highCount: this.#highQueue.cnt,
41
- lowLimit: this.#lowQueue.lim,
42
- normalLimit: this.#normalQueue.lim,
43
- highLimit: this.#highQueue.lim,
103
+ queues: this.#queues.map((q) => ({ count: q.cnt, age: q.age })),
44
104
  }
45
105
  }
46
106
 
@@ -61,15 +121,11 @@ export class Scheduler {
61
121
  } else {
62
122
  this.#concurrency = opts?.concurrency || Infinity
63
123
  }
64
-
65
- this.#lowQueue.lim = this.#concurrency / 4
66
- this.#normalQueue.lim = this.#concurrency - this.#lowQueue.lim
67
- this.#highQueue.lim = this.#concurrency
68
124
  }
69
125
 
70
126
  run (
71
127
  fn ,
72
- priority = Scheduler.NORMAL,
128
+ priority = Scheduler.NORMAL,
73
129
  opaque ,
74
130
  ) {
75
131
  return new Promise ((resolve, reject) => {
@@ -89,46 +145,18 @@ export class Scheduler {
89
145
  })
90
146
  }
91
147
 
92
- acquire (
93
- fn ,
94
- priority = Scheduler.NORMAL,
95
- opaque ,
96
- ) {
97
- if (typeof priority === 'number' && Number.isInteger(priority) && priority >= 0) {
98
- // Do nothing
99
- } else if (priority == null) {
100
- priority = Scheduler.NORMAL
101
- } else if (typeof priority === 'string') {
102
- if (priority === 'low') {
103
- priority = Scheduler.LOW
104
- } else if (priority === 'normal') {
105
- priority = Scheduler.NORMAL
106
- } else if (priority === 'high') {
107
- priority = Scheduler.HIGH
108
- } else {
109
- throw new Error('Invalid priority')
110
- }
111
- } else {
112
- throw new Error('Invalid priority')
113
- }
114
-
115
- let queue
116
- if (priority > Scheduler.NORMAL) {
117
- queue = this.#highQueue
118
- } else if (priority < Scheduler.NORMAL) {
119
- queue = this.#lowQueue
120
- } else {
121
- queue = this.#normalQueue
122
- }
148
+ acquire (fn , priority = Scheduler.NORMAL, opaque ) {
149
+ const p = parsePriority(priority)
150
+ const queue = this.#queues[p + 3]
123
151
 
124
152
  if (this.#stateView) {
125
- if (this.#running < 1 || this.#stateView[RUNNING_INDEX] < queue.lim) {
153
+ if (this.#running < 1 || this.#stateView[RUNNING_INDEX] < this.#concurrency) {
126
154
  Atomics.add(this.#stateView, RUNNING_INDEX, 1)
127
155
  this.#running += 1
128
156
  fn(opaque)
129
157
  return
130
158
  }
131
- } else if (this.#running < 1 || this.#running < queue.lim) {
159
+ } else if ((this.#running < 1 && this.#concurrency > 0) || this.#running < this.#concurrency) {
132
160
  this.#running += 1
133
161
  fn(opaque)
134
162
  return
@@ -137,6 +165,10 @@ export class Scheduler {
137
165
  queue.arr.push(fn, opaque)
138
166
  queue.cnt += 1
139
167
 
168
+ if (queue.cnt === 1) {
169
+ queue.age = fastNow
170
+ }
171
+
140
172
  this.#deferred += 1
141
173
  this.#pending += 1
142
174
  }
@@ -153,38 +185,54 @@ export class Scheduler {
153
185
 
154
186
  try {
155
187
  this.#releasing = true
156
- while (this.#pending > 0) {
157
- let queue
158
- if (this.#highQueue.cnt > 0) {
159
- queue = this.#highQueue
160
- } else if (this.#normalQueue.cnt > 0) {
161
- queue = this.#normalQueue
162
- } else if (this.#lowQueue.cnt > 0) {
163
- queue = this.#lowQueue
164
- } else {
165
- throw new Error('Invariant violation: pending > 0 but no tasks in queues')
188
+ while (this.#pending > 0 && (this.#running === 0 || running < this.#concurrency)) {
189
+ let queue = null
190
+
191
+ let idx = this.#queues.length - 1
192
+
193
+ // Avoid starvation by randomizing the starting queue.
194
+ {
195
+ this.#counter = (this.#counter + 1) & maxInt
196
+ if (this.#counter & 0b0000001) {
197
+ idx = 6 // highest: 50%
198
+ } else if (this.#counter & 0b0000010) {
199
+ idx = 5 // higher: 25%
200
+ } else if (this.#counter & 0b0000100) {
201
+ idx = 4 // high: 12.5%
202
+ } else if (this.#counter & 0b0001000) {
203
+ idx = 3 // normal: 6.25%
204
+ } else if (this.#counter & 0b0010000) {
205
+ idx = 2 // low: 3.125%
206
+ } else if (this.#counter & 0b0100000) {
207
+ idx = 1 // lower: 1.5625%
208
+ } else if (this.#counter & 0b1000000) {
209
+ idx = 0 // lowest: 0.78%
210
+ }
211
+ }
212
+
213
+ for (let n = idx; n >= 0 && (queue == null || queue.cnt === 0); n--) {
214
+ queue = this.#queues[n]
166
215
  }
167
216
 
168
- if (this.#running > 0 && running >= queue.lim) {
169
- break
217
+ for (let n = this.#queues.length - 1; n > idx && (queue == null || queue.cnt === 0); n--) {
218
+ queue = this.#queues[n]
170
219
  }
171
220
 
172
- if ((this.#counter & 63) === 0 && this.#lowQueue.cnt > 0) {
173
- queue = this.#lowQueue
174
- } else if ((this.#counter & 15) === 0 && this.#normalQueue.cnt > 0) {
175
- queue = this.#normalQueue
221
+ if (queue == null || queue.cnt === 0) {
222
+ throw new Error('Invariant violation: pending > 0 but no tasks in queues')
176
223
  }
177
224
 
178
225
  const fn = queue.arr[queue.idx++]
179
226
  const opaque = queue.arr[queue.idx++]
180
227
  queue.cnt -= 1
228
+ queue.age = fastNow
181
229
 
182
230
  if (queue.cnt === 0) {
183
231
  queue.idx = 0
184
232
  queue.arr.length = 0
185
233
  } else if (queue.idx > 1024) {
186
- queue.arr.splice(0, queue.idx)
187
234
  queue.idx = 0
235
+ queue.arr.splice(0, queue.idx)
188
236
  }
189
237
 
190
238
  running = this.#stateView
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/scheduler",
3
- "version": "2.0.4",
3
+ "version": "3.0.1",
4
4
  "type": "module",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -20,5 +20,5 @@
20
20
  "rimraf": "^6.1.2",
21
21
  "typescript": "^5.9.3"
22
22
  },
23
- "gitHead": "aa8978d5b536477418eb6cc60255d4a36bb9d013"
23
+ "gitHead": "c88bafd0c4a7e250d9e4080d9f50a0a0a1a562ac"
24
24
  }