@nxtedition/scheduler 1.0.8 → 2.0.2

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/lib/index.d.ts CHANGED
@@ -4,9 +4,21 @@ export declare class Scheduler {
4
4
  static NORMAL: 1;
5
5
  static HIGH: 2;
6
6
  get concurrency(): number;
7
- static makeSharedState(concurrency?: number): SharedArrayBuffer;
8
- constructor(opts?: SharedArrayBuffer | {
7
+ get stats(): {
8
+ deferred: number;
9
+ running: number;
10
+ pending: number;
11
+ lowCount: number;
12
+ normalCount: number;
13
+ highCount: number;
14
+ lowLimit: number;
15
+ normalLimit: number;
16
+ highLimit: number;
17
+ };
18
+ static makeSharedState(concurrency: number): SharedArrayBuffer;
19
+ constructor(opts: SharedArrayBuffer | {
9
20
  concurrency?: number;
10
21
  });
11
- schedule(fn: (next: Function) => any, priority?: 0 | 1 | 2 | 'low' | 'normal' | 'high'): any;
22
+ acquire(fn: (opaque?: any) => any, priority?: 0 | 1 | 2 | 'low' | 'normal' | 'high', opaque?: any): any;
23
+ release(): void;
12
24
  }
package/lib/index.js CHANGED
@@ -1,4 +1,12 @@
1
- import { FixedQueue } from './fixed-queue.js'
1
+ const RUNNING_INDEX = 0
2
+ const CONCURRENCY_INDEX = 1
3
+
4
+ class FastQueue {
5
+ idx = 0
6
+ cnt = 0
7
+ arr = []
8
+ lim = 0
9
+ }
2
10
 
3
11
  export class Scheduler {
4
12
  static LOW = 0
@@ -8,51 +16,65 @@ export class Scheduler {
8
16
  #concurrency
9
17
  #stateView
10
18
 
11
- static #RUNNING_INDEX = 0
12
- static #COUNTER_INDEX = 1
13
- static #CONCURRENCY_INDEX = 2
14
-
15
19
  #running = 0
16
20
  #pending = 0
17
21
  #counter = 0
18
- #actived = false
22
+ #releasing = false
23
+ #deferred = 0
19
24
 
20
- #lowQueue = new FixedQueue()
21
- #normalQueue = new FixedQueue()
22
- #highQueue = new FixedQueue()
25
+ #lowQueue = new FastQueue()
26
+ #normalQueue = new FastQueue()
27
+ #highQueue = new FastQueue()
23
28
 
24
29
  get concurrency() {
25
30
  return this.#concurrency
26
31
  }
27
32
 
28
- static makeSharedState(concurrency = 0) {
29
- if (concurrency < 0 || !Number.isInteger(concurrency)) {
33
+ get stats() {
34
+ return {
35
+ deferred: this.#deferred,
36
+ running: this.#running,
37
+ 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,
44
+ }
45
+ }
46
+
47
+ static makeSharedState(concurrency ) {
48
+ if (concurrency != null && (concurrency < 0 || !Number.isInteger(concurrency))) {
30
49
  throw new Error('Invalid concurrency')
31
50
  }
32
51
  const stateBuffer = new SharedArrayBuffer(64)
33
52
  const stateView = new Uint32Array(stateBuffer)
34
- Atomics.store(stateView, Scheduler.#CONCURRENCY_INDEX, concurrency ?? 0)
53
+ Atomics.store(stateView, CONCURRENCY_INDEX, concurrency ?? 0)
35
54
  return stateBuffer
36
55
  }
37
56
 
38
- constructor(opts = {}) {
57
+ constructor(opts ) {
39
58
  if (opts instanceof SharedArrayBuffer) {
40
59
  this.#stateView = new Uint32Array(opts)
41
- this.#concurrency = Atomics.load(this.#stateView, Scheduler.#CONCURRENCY_INDEX)
60
+ this.#concurrency = Atomics.load(this.#stateView, CONCURRENCY_INDEX) || Infinity
42
61
  } else {
43
- this.#concurrency = opts?.concurrency ?? 0
62
+ this.#concurrency = opts?.concurrency || Infinity
44
63
  }
64
+
65
+ this.#lowQueue.lim = this.#concurrency / 4
66
+ this.#normalQueue.lim = this.#concurrency - this.#lowQueue.lim
67
+ this.#highQueue.lim = this.#concurrency
45
68
  }
46
69
 
47
- schedule(
48
- fn ,
70
+ acquire(
71
+ fn ,
49
72
  priority = Scheduler.NORMAL,
73
+ opaque ,
50
74
  ) {
51
- if (typeof fn !== 'function') {
52
- throw new TypeError('First argument must be a function')
53
- }
54
-
55
- if (priority == null) {
75
+ if (typeof priority === 'number' && Number.isInteger(priority) && priority >= 0) {
76
+ // Do nothing
77
+ } else if (priority == null) {
56
78
  priority = Scheduler.NORMAL
57
79
  } else if (typeof priority === 'string') {
58
80
  if (priority === 'low') {
@@ -64,63 +86,93 @@ export class Scheduler {
64
86
  } else {
65
87
  throw new Error('Invalid priority')
66
88
  }
67
- } else if (!Number.isInteger(priority)) {
89
+ } else {
68
90
  throw new Error('Invalid priority')
69
91
  }
70
92
 
71
- const running = this.#stateView
72
- ? Atomics.load(this.#stateView, Scheduler.#RUNNING_INDEX)
73
- : this.#running
93
+ let queue
94
+ if (priority > Scheduler.NORMAL) {
95
+ queue = this.#highQueue
96
+ } else if (priority < Scheduler.NORMAL) {
97
+ queue = this.#lowQueue
98
+ } else {
99
+ queue = this.#normalQueue
100
+ }
74
101
 
75
- if (this.#running < 1 || running < this.#concurrency) {
76
- if (this.#stateView) {
77
- Atomics.add(this.#stateView, Scheduler.#RUNNING_INDEX, 1)
102
+ if (this.#stateView) {
103
+ if (this.#running < 1 || this.#stateView[RUNNING_INDEX] < queue.lim) {
104
+ Atomics.add(this.#stateView, RUNNING_INDEX, 1)
105
+ this.#running += 1
106
+ return fn(opaque)
78
107
  }
108
+ } else if (this.#running < 1 || this.#running < queue.lim) {
79
109
  this.#running += 1
80
- return fn(this.#next)
110
+ return fn(opaque)
81
111
  }
82
112
 
83
- if (priority > Scheduler.NORMAL) {
84
- this.#highQueue.push(fn)
85
- } else if (priority < Scheduler.NORMAL) {
86
- this.#lowQueue.push(fn)
87
- } else {
88
- this.#normalQueue.push(fn)
89
- }
113
+ queue.arr.push(fn, opaque)
114
+ queue.cnt += 1
115
+
116
+ this.#deferred += 1
90
117
  this.#pending += 1
91
118
  }
92
119
 
93
- #next = () => {
94
- try {
95
- let running = this.#stateView
96
- ? Atomics.sub(this.#stateView, Scheduler.#RUNNING_INDEX, 1) - 1
97
- : this.#running - 1
98
- this.#running -= 1
120
+ release() {
121
+ let running = this.#stateView
122
+ ? Atomics.sub(this.#stateView, RUNNING_INDEX, 1) - 1
123
+ : this.#running - 1
124
+ this.#running -= 1
99
125
 
100
- if (this.#actived) {
101
- return
102
- }
126
+ if (this.#pending === 0 || this.#releasing) {
127
+ return
128
+ }
103
129
 
104
- this.#actived = true
105
- while (this.#pending > 0 && (this.#running < 1 || running < this.#concurrency)) {
106
- const counter = this.#stateView
107
- ? Atomics.add(this.#stateView, Scheduler.#COUNTER_INDEX, 1)
108
- : this.#counter++
109
- const fn =
110
- ((counter & 63) === 0 && this.#lowQueue.shift()) ||
111
- ((counter & 15) === 0 && this.#normalQueue.shift()) ||
112
- this.#highQueue.shift() ||
113
- this.#normalQueue.shift() ||
114
- this.#lowQueue.shift()
130
+ try {
131
+ this.#releasing = true
132
+ while (this.#pending > 0) {
133
+ let queue
134
+ if (this.#highQueue.cnt > 0) {
135
+ queue = this.#highQueue
136
+ } else if (this.#normalQueue.cnt > 0) {
137
+ queue = this.#normalQueue
138
+ } else if (this.#lowQueue.cnt > 0) {
139
+ queue = this.#lowQueue
140
+ } else {
141
+ throw new Error('Invariant violation: pending > 0 but no tasks in queues')
142
+ }
143
+
144
+ if (this.#running > 0 && running >= queue.lim) {
145
+ break
146
+ }
147
+
148
+ if ((this.#counter & 63) === 0) {
149
+ queue = this.#lowQueue
150
+ } else if ((this.#counter & 15) === 0) {
151
+ queue = this.#normalQueue
152
+ }
153
+
154
+ const fn = queue.arr[queue.idx++]
155
+ const opaque = queue.arr[queue.idx++]
156
+ queue.cnt -= 1
157
+
158
+ if (queue.cnt === 0) {
159
+ queue.idx = 0
160
+ queue.arr.length = 0
161
+ } else if (queue.idx > 1024) {
162
+ queue.arr.splice(0, queue.idx)
163
+ queue.idx = 0
164
+ }
115
165
 
116
166
  running = this.#stateView
117
- ? Atomics.add(this.#stateView, Scheduler.#RUNNING_INDEX, 1) + 1
167
+ ? Atomics.add(this.#stateView, RUNNING_INDEX, 1) + 1
118
168
  : this.#running + 1
119
- this.#running += 1
169
+
170
+ this.#counter += 1
120
171
  this.#pending -= 1
121
- fn(this.#next)
172
+ this.#running += 1
173
+ fn(opaque)
122
174
  }
123
- this.#actived = false
175
+ this.#releasing = false
124
176
  } catch (err) {
125
177
  // Throwing here is undefined behavior...
126
178
  queueMicrotask(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/scheduler",
3
- "version": "1.0.8",
3
+ "version": "2.0.2",
4
4
  "type": "module",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -9,7 +9,7 @@
9
9
  ],
10
10
  "license": "UNLICENSED",
11
11
  "scripts": {
12
- "build": "rimraf lib && tsc && amaroc ./src/index.ts && mv src/index.js lib/ && cp src/fixed-queue.js lib/",
12
+ "build": "rimraf lib && tsc && amaroc ./src/index.ts && mv src/index.js lib/",
13
13
  "prepublishOnly": "yarn build",
14
14
  "typecheck": "tsc --noEmit",
15
15
  "test": "node --test",
@@ -20,5 +20,5 @@
20
20
  "rimraf": "^6.1.2",
21
21
  "typescript": "^5.9.3"
22
22
  },
23
- "gitHead": "ba7f0417c8dbf2a8d6e38f5848f5e8ec162f4eb7"
23
+ "gitHead": "ee95b62bb1f5bc494fa4fcff603f5a44b44723ac"
24
24
  }
@@ -1,8 +0,0 @@
1
- export class FixedQueue {
2
- head: any;
3
- tail: any;
4
- size: number;
5
- isEmpty(): any;
6
- push(data: any): void;
7
- shift(): any;
8
- }
@@ -1,123 +0,0 @@
1
- // Extracted from node/lib/internal/fixed_queue.js
2
-
3
- // Currently optimal queue size, tested on V8 6.0 - 6.6. Must be power of two.
4
- const kSize = 2048
5
- const kMask = kSize - 1
6
-
7
- // The FixedQueue is implemented as a singly-linked list of fixed-size
8
- // circular buffers. It looks something like this:
9
- //
10
- // head tail
11
- // | |
12
- // v v
13
- // +-----------+ <-----\ +-----------+ <------\ +-----------+
14
- // | [null] | \----- | next | \------- | next |
15
- // +-----------+ +-----------+ +-----------+
16
- // | item | <-- bottom | item | <-- bottom | [empty] |
17
- // | item | | item | | [empty] |
18
- // | item | | item | | [empty] |
19
- // | item | | item | | [empty] |
20
- // | item | | item | bottom --> | item |
21
- // | item | | item | | item |
22
- // | ... | | ... | | ... |
23
- // | item | | item | | item |
24
- // | item | | item | | item |
25
- // | [empty] | <-- top | item | | item |
26
- // | [empty] | | item | | item |
27
- // | [empty] | | [empty] | <-- top top --> | [empty] |
28
- // +-----------+ +-----------+ +-----------+
29
- //
30
- // Or, if there is only one circular buffer, it looks something
31
- // like either of these:
32
- //
33
- // head tail head tail
34
- // | | | |
35
- // v v v v
36
- // +-----------+ +-----------+
37
- // | [null] | | [null] |
38
- // +-----------+ +-----------+
39
- // | [empty] | | item |
40
- // | [empty] | | item |
41
- // | item | <-- bottom top --> | [empty] |
42
- // | item | | [empty] |
43
- // | [empty] | <-- top bottom --> | item |
44
- // | [empty] | | item |
45
- // +-----------+ +-----------+
46
- //
47
- // Adding a value means moving `top` forward by one, removing means
48
- // moving `bottom` forward by one. After reaching the end, the queue
49
- // wraps around.
50
- //
51
- // When `top === bottom` the current queue is empty and when
52
- // `top + 1 === bottom` it's full. This wastes a single space of storage
53
- // but allows much quicker checks.
54
-
55
- class FixedCircularBuffer {
56
- constructor() {
57
- this.bottom = 0
58
- this.top = 0
59
- this.list = new Array(kSize).fill(undefined)
60
- this.next = null
61
- }
62
-
63
- isEmpty() {
64
- return this.top === this.bottom
65
- }
66
-
67
- isFull() {
68
- return ((this.top + 1) & kMask) === this.bottom
69
- }
70
-
71
- push(data) {
72
- this.list[this.top] = data
73
- this.top = (this.top + 1) & kMask
74
- }
75
-
76
- shift() {
77
- const nextItem = this.list[this.bottom]
78
- if (nextItem === undefined) {
79
- return null
80
- }
81
- this.list[this.bottom] = undefined
82
- this.bottom = (this.bottom + 1) & kMask
83
- return nextItem
84
- }
85
- }
86
-
87
- const POOL = []
88
-
89
- export class FixedQueue {
90
- constructor() {
91
- this.head = this.tail = POOL.pop() ?? new FixedCircularBuffer()
92
- this.size = 0
93
- }
94
-
95
- isEmpty() {
96
- return this.head.isEmpty()
97
- }
98
-
99
- push(data) {
100
- if (this.head.isFull()) {
101
- // Head is full: Creates a new queue, sets the old queue's `.next` to it,
102
- // and sets it as the new main queue.
103
- this.head = this.head.next = POOL.pop() ?? new FixedCircularBuffer()
104
- }
105
- this.head.push(data)
106
- }
107
-
108
- shift() {
109
- const tail = this.tail
110
- const next = tail.shift()
111
- if (tail.isEmpty() && tail.next !== null) {
112
- // If there is another queue, it forms the new tail.
113
- this.tail = tail.next
114
- tail.next = null
115
- tail.bottom = 0
116
- tail.top = 0
117
- if (POOL.length < 64) {
118
- POOL.push(tail)
119
- }
120
- }
121
- return next
122
- }
123
- }