@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 +15 -3
- package/lib/index.js +113 -61
- package/package.json +3 -3
- package/lib/fixed-queue.d.ts +0 -8
- package/lib/fixed-queue.js +0 -123
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
22
|
+
#releasing = false
|
|
23
|
+
#deferred = 0
|
|
19
24
|
|
|
20
|
-
#lowQueue = new
|
|
21
|
-
#normalQueue = new
|
|
22
|
-
#highQueue = new
|
|
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
|
-
|
|
29
|
-
|
|
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,
|
|
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,
|
|
60
|
+
this.#concurrency = Atomics.load(this.#stateView, CONCURRENCY_INDEX) || Infinity
|
|
42
61
|
} else {
|
|
43
|
-
this.#concurrency = opts?.concurrency
|
|
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
|
-
|
|
48
|
-
fn
|
|
70
|
+
acquire(
|
|
71
|
+
fn ,
|
|
49
72
|
priority = Scheduler.NORMAL,
|
|
73
|
+
opaque ,
|
|
50
74
|
) {
|
|
51
|
-
if (typeof
|
|
52
|
-
|
|
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
|
|
89
|
+
} else {
|
|
68
90
|
throw new Error('Invalid priority')
|
|
69
91
|
}
|
|
70
92
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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.#
|
|
76
|
-
if (this.#stateView) {
|
|
77
|
-
Atomics.add(this.#stateView,
|
|
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(
|
|
110
|
+
return fn(opaque)
|
|
81
111
|
}
|
|
82
112
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
126
|
+
if (this.#pending === 0 || this.#releasing) {
|
|
127
|
+
return
|
|
128
|
+
}
|
|
103
129
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
this.#
|
|
114
|
-
|
|
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,
|
|
167
|
+
? Atomics.add(this.#stateView, RUNNING_INDEX, 1) + 1
|
|
118
168
|
: this.#running + 1
|
|
119
|
-
|
|
169
|
+
|
|
170
|
+
this.#counter += 1
|
|
120
171
|
this.#pending -= 1
|
|
121
|
-
|
|
172
|
+
this.#running += 1
|
|
173
|
+
fn(opaque)
|
|
122
174
|
}
|
|
123
|
-
this.#
|
|
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": "
|
|
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/
|
|
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": "
|
|
23
|
+
"gitHead": "ee95b62bb1f5bc494fa4fcff603f5a44b44723ac"
|
|
24
24
|
}
|
package/lib/fixed-queue.d.ts
DELETED
package/lib/fixed-queue.js
DELETED
|
@@ -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
|
-
}
|