@loadmap/sim 0.1.0 → 0.2.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.
- package/dist/index.d.ts +18 -4
- package/dist/index.js +86 -25
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -47,8 +47,15 @@ interface Particle {
|
|
|
47
47
|
to: Place;
|
|
48
48
|
/** Set on a copy made at a fan-out: the branch stage it is heading to. */
|
|
49
49
|
branch?: string;
|
|
50
|
-
/**
|
|
51
|
-
|
|
50
|
+
/** Set on a cache hit heading back to where it came from, answered. */
|
|
51
|
+
back?: boolean;
|
|
52
|
+
/** Which attempt this is: 0 for the first, 1 for the first retry, and so on. */
|
|
53
|
+
attempt?: number;
|
|
54
|
+
/**
|
|
55
|
+
* superseded: merged into a newer unit of its key; unread: a fan-out that made no copy; answered: a cache hit back
|
|
56
|
+
* where it came from.
|
|
57
|
+
*/
|
|
58
|
+
outcome?: 'delivered' | 'dropped' | 'rejected' | 'superseded' | 'unread' | 'answered';
|
|
52
59
|
}
|
|
53
60
|
interface QueueState {
|
|
54
61
|
/** Units waiting. */
|
|
@@ -91,10 +98,17 @@ export declare class Sim {
|
|
|
91
98
|
queue(stage: string, flow: string): QueueState;
|
|
92
99
|
/** How busy a stage is, 0–1, smoothed: for a workers stage, the share of its workers at work. */
|
|
93
100
|
busy(stage: string): number;
|
|
94
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* The share a pass, limit or cache stage (of what reaches it) or a fan-out (of its copies) rejects because its
|
|
103
|
+
* component is past capacity.
|
|
104
|
+
*/
|
|
95
105
|
shed(stage: string): number;
|
|
96
|
-
/** How busy worker `i` of a workers stage is, 0–1. */
|
|
106
|
+
/** How busy worker `i` of a workers stage is, 0–1; 0 in a pod an autoscaler has not started. */
|
|
97
107
|
slotBusy(stage: string, i: number): number;
|
|
108
|
+
/** Units per second actually reaching a stage on the flows' routes, after what stages before it turned away. */
|
|
109
|
+
arriving(stage: string): number;
|
|
110
|
+
/** The pods a workers stage runs: fewer than it draws while an autoscaler holds it small. */
|
|
111
|
+
running(stage: string): number;
|
|
98
112
|
}
|
|
99
113
|
export declare function createSim(model: Model, ev: Evaluation, o?: SimOptions): Sim;
|
|
100
114
|
export type { Fill, Particle, Place, QueueState, SimOptions };
|
package/dist/index.js
CHANGED
|
@@ -132,37 +132,41 @@ var Sim = class {
|
|
|
132
132
|
return false;
|
|
133
133
|
};
|
|
134
134
|
this.particles = this.particles.filter((d) => fits(d.from, d.flow) && fits(d.to, d.flow));
|
|
135
|
-
|
|
135
|
+
this.#fills.clear();
|
|
136
136
|
}
|
|
137
137
|
const full = this.#model.thresholds.full;
|
|
138
|
+
const stages = Object.values(ev.stages);
|
|
138
139
|
this.#shed.clear();
|
|
140
|
+
for (const s of stages) this.#shed.set(s.id, "shed" in s ? s.shed : 0);
|
|
139
141
|
this.#inflow.clear();
|
|
140
|
-
for (const s of Object.values(ev.stages)) {
|
|
141
|
-
const u = (s.kind === "pass" || s.kind === "fanout") && s.component ? this.#util(s.component) : null;
|
|
142
|
-
this.#shed.set(s.id, u != null && u > full ? 1 - full / u : 0);
|
|
143
|
-
}
|
|
144
142
|
for (const f of this.#model.flowIds) {
|
|
145
143
|
let rate = ev.flows[f] ?? 0;
|
|
146
144
|
for (const st of ev.routes[f] ?? []) {
|
|
147
145
|
this.#inflow.set(`${st}/${f}`, rate);
|
|
148
|
-
|
|
146
|
+
const s = ev.stages[st];
|
|
147
|
+
if (s.kind !== "fanout") rate *= 1 - (this.#shed.get(st) ?? 0);
|
|
148
|
+
if (s.kind === "cache") rate *= 1 - s.hit;
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
this.#capacity.clear();
|
|
152
|
-
const
|
|
153
|
-
for (const s of
|
|
152
|
+
const atFull = /* @__PURE__ */ new Map();
|
|
153
|
+
for (const s of stages) {
|
|
154
154
|
const c = s.component ? ev.components[s.component] : void 0;
|
|
155
155
|
let cap = Number.POSITIVE_INFINITY;
|
|
156
156
|
if (c && c.util != null && c.full != null && Number.isFinite(c.full)) {
|
|
157
157
|
if (c.full > 0) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
158
|
+
let at = atFull.get(c.full);
|
|
159
|
+
if (!at) {
|
|
160
|
+
at = this.#model.evaluate({
|
|
161
|
+
...ev.values,
|
|
162
|
+
load: c.full
|
|
163
|
+
});
|
|
164
|
+
atFull.set(c.full, at);
|
|
165
|
+
}
|
|
166
|
+
cap = this.#model.flowIds.reduce((a, f) => a + (at.at[s.id]?.[f] ?? 0), 0);
|
|
163
167
|
} else {
|
|
164
|
-
const
|
|
165
|
-
cap = c.util > 0 ?
|
|
168
|
+
const offered = this.#model.flowIds.reduce((x, f) => x + (ev.at[s.id]?.[f] ?? 0), 0);
|
|
169
|
+
cap = c.util > 0 ? offered * full / c.util : 0;
|
|
166
170
|
}
|
|
167
171
|
}
|
|
168
172
|
this.#capacity.set(s.id, cap);
|
|
@@ -188,14 +192,33 @@ var Sim = class {
|
|
|
188
192
|
busy(stage) {
|
|
189
193
|
return this.#busy.get(stage) ?? this.#steadyBusy(stage);
|
|
190
194
|
}
|
|
191
|
-
/**
|
|
195
|
+
/**
|
|
196
|
+
* The share a pass, limit or cache stage (of what reaches it) or a fan-out (of its copies) rejects because its
|
|
197
|
+
* component is past capacity.
|
|
198
|
+
*/
|
|
192
199
|
shed(stage) {
|
|
193
200
|
return this.#shed.get(stage) ?? 0;
|
|
194
201
|
}
|
|
195
|
-
/** How busy worker `i` of a workers stage is, 0–1. */
|
|
202
|
+
/** How busy worker `i` of a workers stage is, 0–1; 0 in a pod an autoscaler has not started. */
|
|
196
203
|
slotBusy(stage, i) {
|
|
197
|
-
const
|
|
198
|
-
|
|
204
|
+
const s = this.#ev.stages[stage];
|
|
205
|
+
if (s?.kind !== "workers" || i >= s.running * s.perPod) return 0;
|
|
206
|
+
const key = `${stage}/${s.running}`;
|
|
207
|
+
let f = this.#fills.get(key);
|
|
208
|
+
if (!f) {
|
|
209
|
+
f = fillOrder(s.running, s.perPod, s.fill, this.#o.seed);
|
|
210
|
+
this.#fills.set(key, f);
|
|
211
|
+
}
|
|
212
|
+
return busyOf(i, f, this.busy(stage));
|
|
213
|
+
}
|
|
214
|
+
/** Units per second actually reaching a stage on the flows' routes, after what stages before it turned away. */
|
|
215
|
+
arriving(stage) {
|
|
216
|
+
return this.#model.flowIds.reduce((a, f) => a + (this.#inflow.get(`${stage}/${f}`) ?? 0), 0);
|
|
217
|
+
}
|
|
218
|
+
/** The pods a workers stage runs: fewer than it draws while an autoscaler holds it small. */
|
|
219
|
+
running(stage) {
|
|
220
|
+
const s = this.#ev.stages[stage];
|
|
221
|
+
return s?.kind === "workers" ? s.running : 0;
|
|
199
222
|
}
|
|
200
223
|
#util(component) {
|
|
201
224
|
return this.#ev.components[component]?.util ?? null;
|
|
@@ -203,7 +226,12 @@ var Sim = class {
|
|
|
203
226
|
#steadyBusy(stage) {
|
|
204
227
|
const s = this.#ev.stages[stage];
|
|
205
228
|
const u = s?.component ? this.#util(s.component) : null;
|
|
206
|
-
return Math.min(1, (u ?? 0) / this.#model.thresholds.full);
|
|
229
|
+
return Math.min(1, (u ?? 0) / this.#model.thresholds.full * this.#scaled(stage));
|
|
230
|
+
}
|
|
231
|
+
/** The workers drawn over the workers running: the busy share of the running ones is that much higher. */
|
|
232
|
+
#scaled(stage) {
|
|
233
|
+
const s = this.#ev.stages[stage];
|
|
234
|
+
return s?.kind === "workers" && s.running > 0 ? s.pods / s.running : 1;
|
|
207
235
|
}
|
|
208
236
|
#stepQueues(dt) {
|
|
209
237
|
const ev = this.#ev;
|
|
@@ -278,14 +306,14 @@ var Sim = class {
|
|
|
278
306
|
for (const s of Object.values(ev.stages)) {
|
|
279
307
|
if (s.kind !== "workers") continue;
|
|
280
308
|
const cap = this.#capacity.get(s.id) ?? Number.POSITIVE_INFINITY;
|
|
281
|
-
const now = taken.has(s.id) && Number.isFinite(cap) && cap > 0 ? taken.get(s.id) / (cap * dt) : this.#steadyBusy(s.id);
|
|
309
|
+
const now = taken.has(s.id) && Number.isFinite(cap) && cap > 0 ? taken.get(s.id) / (cap * dt) * this.#scaled(s.id) : this.#steadyBusy(s.id);
|
|
282
310
|
this.#busy.set(s.id, ema(this.#busy.get(s.id) ?? now, Math.min(1, now), dt));
|
|
283
311
|
}
|
|
284
312
|
}
|
|
285
313
|
#spawnDots(dtReal) {
|
|
286
314
|
const { minDots, maxDots, legSeconds, maxParticles } = this.#o;
|
|
287
315
|
for (const f of this.#model.flowIds) {
|
|
288
|
-
const rate = this.#ev.flows[f] ?? 0;
|
|
316
|
+
const rate = this.#ev.retry[f]?.rate ?? this.#ev.flows[f] ?? 0;
|
|
289
317
|
if (rate <= 0) continue;
|
|
290
318
|
const perSec = Math.min(maxDots, Math.max(minDots, rate / this.dotScale));
|
|
291
319
|
let acc = (this.#spawn.get(f) ?? 0) + perSec * dtReal;
|
|
@@ -307,6 +335,23 @@ var Sim = class {
|
|
|
307
335
|
this.#spawn.set(f, acc);
|
|
308
336
|
}
|
|
309
337
|
}
|
|
338
|
+
/** A unit turned away, sent again from its source while it has retries left and the budget allows. */
|
|
339
|
+
#retry(d) {
|
|
340
|
+
const r = this.#ev.retry[d.flow];
|
|
341
|
+
const attempt = (d.attempt ?? 0) + 1;
|
|
342
|
+
if (!r || attempt > r.times || this.#r() >= r.retried) return null;
|
|
343
|
+
return {
|
|
344
|
+
id: this.#nextId++,
|
|
345
|
+
flow: d.flow,
|
|
346
|
+
leg: 0,
|
|
347
|
+
phase: "move",
|
|
348
|
+
t: 0,
|
|
349
|
+
dur: this.#o.legSeconds,
|
|
350
|
+
from: { stage: null },
|
|
351
|
+
to: this.#place(this.#ev.routes[d.flow][0], d.flow),
|
|
352
|
+
attempt
|
|
353
|
+
};
|
|
354
|
+
}
|
|
310
355
|
#moveDots(dtReal) {
|
|
311
356
|
const ev = this.#ev;
|
|
312
357
|
const fade = (d, outcome, dur) => {
|
|
@@ -327,9 +372,18 @@ var Sim = class {
|
|
|
327
372
|
d.to = this.#place(route[d.leg], d.flow);
|
|
328
373
|
};
|
|
329
374
|
const born = [];
|
|
375
|
+
const reject = (d) => {
|
|
376
|
+
fade(d, "rejected", .6);
|
|
377
|
+
const again = this.particles.length + born.length < this.#o.maxParticles ? this.#retry(d) : null;
|
|
378
|
+
if (again) born.push(again);
|
|
379
|
+
};
|
|
330
380
|
for (const d of this.particles) {
|
|
331
381
|
d.t += dtReal;
|
|
332
382
|
if (d.t < d.dur) continue;
|
|
383
|
+
if (d.phase === "move" && d.back) {
|
|
384
|
+
fade(d, "answered", .35);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
333
387
|
const stage = ev.stages[d.to.stage ?? ""];
|
|
334
388
|
if (d.phase === "fade" || !stage) {
|
|
335
389
|
d.phase = "fade";
|
|
@@ -356,8 +410,15 @@ var Sim = class {
|
|
|
356
410
|
if (made) fade(d, "delivered", 0);
|
|
357
411
|
else fade(d, "unread", .6);
|
|
358
412
|
} else if (d.phase === "move") {
|
|
359
|
-
if (stage.kind === "pass" && this.#r() < this.shed(stage.id))
|
|
360
|
-
else if (stage.kind === "
|
|
413
|
+
if ((stage.kind === "pass" || stage.kind === "limit" || stage.kind === "cache") && this.#r() < this.shed(stage.id)) reject(d);
|
|
414
|
+
else if (stage.kind === "cache" && this.#r() < stage.hit) {
|
|
415
|
+
const to = d.from;
|
|
416
|
+
d.from = d.to;
|
|
417
|
+
d.to = to;
|
|
418
|
+
d.t = 0;
|
|
419
|
+
d.dur = this.#o.legSeconds;
|
|
420
|
+
d.back = true;
|
|
421
|
+
} else if (stage.kind === "queue") {
|
|
361
422
|
const lag = this.queue(stage.id, d.flow).lag;
|
|
362
423
|
d.phase = "hold";
|
|
363
424
|
d.t = 0;
|
|
@@ -389,7 +450,7 @@ var Sim = class {
|
|
|
389
450
|
}
|
|
390
451
|
if (s?.kind === "workers") return {
|
|
391
452
|
stage,
|
|
392
|
-
cell: this.#pickWorker(s.id, s.
|
|
453
|
+
cell: this.#pickWorker(s.id, s.running * s.perPod)
|
|
393
454
|
};
|
|
394
455
|
return { stage };
|
|
395
456
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loadmap/sim",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "The fluid queue simulation behind loadmap's moving dots: priorities, drop rules, worker fill, seeded randomness.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"loadmap",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"dist"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@loadmap/core": "0.
|
|
31
|
+
"@loadmap/core": "0.2.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {},
|
|
34
34
|
"scripts": {
|