@cloudbitmaps/tools 0.0.0-stage → 0.20.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/LICENSE +202 -0
- package/NOTICE +12 -0
- package/README.md +96 -2
- package/dist/cost.d.ts +353 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +454 -0
- package/dist/index.js.map +7 -0
- package/dist/store-defaults.d.ts +12 -0
- package/package.json +58 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
// src/cost.ts
|
|
2
|
+
import { ValidationError } from "@cloudbitmaps/core";
|
|
3
|
+
|
|
4
|
+
// src/store-defaults.ts
|
|
5
|
+
var LIST_COLLECTION_CADENCE = 16;
|
|
6
|
+
var DEFAULT_CURRENT_GEN_TTL_MS = 2e3;
|
|
7
|
+
var DEFAULT_MAX_OPEN_SEGMENTS = 1024;
|
|
8
|
+
|
|
9
|
+
// src/cost.ts
|
|
10
|
+
function deepFreeze(value) {
|
|
11
|
+
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
12
|
+
Object.freeze(value);
|
|
13
|
+
for (const inner of Object.values(value)) deepFreeze(inner);
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
var ELASTICACHE_REDIS_US_EAST_1_ONDEMAND = deepFreeze({
|
|
18
|
+
source: "ElastiCache for Redis OSS, us-east-1 on-demand, AWS price list 20260914063714",
|
|
19
|
+
replicasPerShard: 2,
|
|
20
|
+
reservedMemoryFraction: 0.25,
|
|
21
|
+
nodeTypes: [
|
|
22
|
+
{ name: "cache.t4g.micro", memoryGiB: 0.5, hourlyUSD: 0.016, maxShards: 1 },
|
|
23
|
+
{ name: "cache.t4g.small", memoryGiB: 1.37, hourlyUSD: 0.032, maxShards: 1 },
|
|
24
|
+
{ name: "cache.t4g.medium", memoryGiB: 3.09, hourlyUSD: 0.065, maxShards: 1 },
|
|
25
|
+
{ name: "cache.m6g.large", memoryGiB: 6.38, hourlyUSD: 0.149 },
|
|
26
|
+
{ name: "cache.r6g.large", memoryGiB: 13.07, hourlyUSD: 0.206 },
|
|
27
|
+
{ name: "cache.r6g.xlarge", memoryGiB: 26.32, hourlyUSD: 0.411 },
|
|
28
|
+
{ name: "cache.r6g.2xlarge", memoryGiB: 52.82, hourlyUSD: 0.821 },
|
|
29
|
+
{ name: "cache.r6g.4xlarge", memoryGiB: 105.81, hourlyUSD: 1.642 },
|
|
30
|
+
{ name: "cache.r6g.8xlarge", memoryGiB: 209.55, hourlyUSD: 3.284 },
|
|
31
|
+
{ name: "cache.r6g.12xlarge", memoryGiB: 317.77, hourlyUSD: 4.925 },
|
|
32
|
+
{ name: "cache.r6g.16xlarge", memoryGiB: 419.09, hourlyUSD: 6.567 },
|
|
33
|
+
{ name: "cache.r6gd.xlarge", memoryGiB: 26.32, ssdGiB: 99.33, hourlyUSD: 0.781 },
|
|
34
|
+
{ name: "cache.r6gd.2xlarge", memoryGiB: 52.82, ssdGiB: 199.07, hourlyUSD: 1.56 },
|
|
35
|
+
{ name: "cache.r6gd.4xlarge", memoryGiB: 105.81, ssdGiB: 398.14, hourlyUSD: 3.12 },
|
|
36
|
+
{ name: "cache.r6gd.8xlarge", memoryGiB: 209.55, ssdGiB: 796.28, hourlyUSD: 6.24 },
|
|
37
|
+
{ name: "cache.r6gd.12xlarge", memoryGiB: 317.77, ssdGiB: 1194.42, hourlyUSD: 9.358 },
|
|
38
|
+
{ name: "cache.r6gd.16xlarge", memoryGiB: 419.09, ssdGiB: 1592.56, hourlyUSD: 12.477 }
|
|
39
|
+
]
|
|
40
|
+
});
|
|
41
|
+
var ONE_REDIS_HA_CLUSTER = deepFreeze({ monthlyUSD: 346 });
|
|
42
|
+
var AWS_US_EAST_1_ONDEMAND = deepFreeze({
|
|
43
|
+
name: "aws-us-east-1-ondemand",
|
|
44
|
+
storage: { getPerMillion: 0.4, putPerMillion: 5, storagePerGiBMonth: 0.023 },
|
|
45
|
+
redis: { sizedToData: ELASTICACHE_REDIS_US_EAST_1_ONDEMAND }
|
|
46
|
+
});
|
|
47
|
+
var HOURS_PER_MONTH = 730;
|
|
48
|
+
var SECONDS_PER_MONTH = HOURS_PER_MONTH * 3600;
|
|
49
|
+
var GIB = 1024 ** 3;
|
|
50
|
+
var STORE_LOAD_PUT_CLASS = 1 + 1 / LIST_COLLECTION_CADENCE;
|
|
51
|
+
var STORE_LOAD_POINTER_READS = 2 + 2 / LIST_COLLECTION_CADENCE;
|
|
52
|
+
var STORE_LOAD_EXISTENCE_CHECKS = 2 - 1 / LIST_COLLECTION_CADENCE;
|
|
53
|
+
var RETENTION_SWEEP_REQUESTS = deepFreeze({
|
|
54
|
+
conditionalDelete: {
|
|
55
|
+
retirement: { reads: 7, writes: 2, deletes: 1 },
|
|
56
|
+
purge: { reads: 4, writes: 0, deletes: 2 }
|
|
57
|
+
},
|
|
58
|
+
tombstoning: {
|
|
59
|
+
retirement: { reads: 6, writes: 2, deletes: 0 },
|
|
60
|
+
purge: { reads: 3, writes: 1, deletes: 0 }
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
var shown = (n) => String(Number(n.toFixed(2)));
|
|
64
|
+
function requireFiniteNonNeg(n, field) {
|
|
65
|
+
if (n === void 0 || !Number.isFinite(n) || n < 0) {
|
|
66
|
+
throw new ValidationError(`${field} must be a finite number >= 0; got ${n}`);
|
|
67
|
+
}
|
|
68
|
+
return n;
|
|
69
|
+
}
|
|
70
|
+
function clamp01(n) {
|
|
71
|
+
return n < 0 ? 0 : n > 1 ? 1 : n;
|
|
72
|
+
}
|
|
73
|
+
var TIE_USD = 1e-6;
|
|
74
|
+
function usableGiB(sizing, nodeType) {
|
|
75
|
+
return nodeType.memoryGiB * (1 - sizing.reservedMemoryFraction) + (nodeType.ssdGiB ?? 0);
|
|
76
|
+
}
|
|
77
|
+
function validateSizing(sizing) {
|
|
78
|
+
const at = "pricing.redis.sizedToData";
|
|
79
|
+
if (sizing === null || typeof sizing !== "object") {
|
|
80
|
+
throw new ValidationError(`${at} must be a RedisSizing`);
|
|
81
|
+
}
|
|
82
|
+
if (typeof sizing.source !== "string" || sizing.source === "") {
|
|
83
|
+
throw new ValidationError(`${at}.source must say where the prices come from`);
|
|
84
|
+
}
|
|
85
|
+
if (!Array.isArray(sizing.nodeTypes) || sizing.nodeTypes.length === 0) {
|
|
86
|
+
throw new ValidationError(`${at}.nodeTypes must list at least one node type`);
|
|
87
|
+
}
|
|
88
|
+
if (!Number.isInteger(sizing.replicasPerShard) || sizing.replicasPerShard < 0) {
|
|
89
|
+
throw new ValidationError(
|
|
90
|
+
`${at}.replicasPerShard must be an integer >= 0; got ${sizing.replicasPerShard}`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const reserved = requireFiniteNonNeg(
|
|
94
|
+
sizing.reservedMemoryFraction,
|
|
95
|
+
`${at}.reservedMemoryFraction`
|
|
96
|
+
);
|
|
97
|
+
if (reserved >= 1) {
|
|
98
|
+
throw new ValidationError(`${at}.reservedMemoryFraction must be below 1; got ${reserved}`);
|
|
99
|
+
}
|
|
100
|
+
const names = /* @__PURE__ */ new Set();
|
|
101
|
+
for (const n of sizing.nodeTypes) {
|
|
102
|
+
if (n === null || typeof n !== "object" || typeof n.name !== "string" || n.name === "") {
|
|
103
|
+
throw new ValidationError(`${at}.nodeTypes: every node type needs a name`);
|
|
104
|
+
}
|
|
105
|
+
const row = `${at}.nodeTypes[${JSON.stringify(n.name)}]`;
|
|
106
|
+
if (names.has(n.name)) throw new ValidationError(`${row} is listed twice`);
|
|
107
|
+
names.add(n.name);
|
|
108
|
+
if (!(requireFiniteNonNeg(n.memoryGiB, `${row}.memoryGiB`) > 0)) {
|
|
109
|
+
throw new ValidationError(`${row}.memoryGiB must be above 0`);
|
|
110
|
+
}
|
|
111
|
+
requireFiniteNonNeg(n.ssdGiB ?? 0, `${row}.ssdGiB`);
|
|
112
|
+
requireFiniteNonNeg(n.hourlyUSD, `${row}.hourlyUSD`);
|
|
113
|
+
if (n.maxShards !== void 0 && (!Number.isInteger(n.maxShards) || n.maxShards < 1)) {
|
|
114
|
+
throw new ValidationError(`${row}.maxShards must be an integer >= 1; got ${n.maxShards}`);
|
|
115
|
+
}
|
|
116
|
+
if (!(usableGiB(sizing, n) > 0)) {
|
|
117
|
+
throw new ValidationError(
|
|
118
|
+
`${row} leaves no memory for data once ${reserved} of it is reserved`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function cheapestCluster(sizing, dataGiB) {
|
|
124
|
+
const fits = [];
|
|
125
|
+
for (const nodeType of sizing.nodeTypes) {
|
|
126
|
+
const usable = usableGiB(sizing, nodeType);
|
|
127
|
+
let shards = Math.max(1, Math.ceil(dataGiB / usable));
|
|
128
|
+
if (shards > 1 && (shards - 1) * usable >= dataGiB) shards -= 1;
|
|
129
|
+
if (nodeType.maxShards !== void 0 && shards > nodeType.maxShards) continue;
|
|
130
|
+
const nodes = shards * (1 + sizing.replicasPerShard);
|
|
131
|
+
fits.push({
|
|
132
|
+
nodeType,
|
|
133
|
+
shards,
|
|
134
|
+
nodes,
|
|
135
|
+
monthlyUSD: nodes * nodeType.hourlyUSD * HOURS_PER_MONTH
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
const size = bytesWords(dataGiB * GIB);
|
|
139
|
+
if (fits.length === 0) {
|
|
140
|
+
throw new ValidationError(
|
|
141
|
+
`no node type in pricing.redis.sizedToData.nodeTypes holds ${size} within its maxShards`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const cheapest = fits.reduce((m, c) => Math.min(m, c.monthlyUSD), Infinity);
|
|
145
|
+
const [best] = fits.filter((c) => c.monthlyUSD <= cheapest + TIE_USD).sort(
|
|
146
|
+
(a, b) => a.nodes - b.nodes || a.monthlyUSD - b.monthlyUSD || (a.nodeType.name < b.nodeType.name ? -1 : a.nodeType.name > b.nodeType.name ? 1 : 0)
|
|
147
|
+
);
|
|
148
|
+
if (best === void 0 || !Number.isFinite(best.monthlyUSD)) {
|
|
149
|
+
throw new ValidationError(
|
|
150
|
+
`pricing.redis.sizedToData prices ${size} at no finite cost; check its node types`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return best;
|
|
154
|
+
}
|
|
155
|
+
function snapshotSizing(sizing) {
|
|
156
|
+
if (sizing === null || typeof sizing !== "object") return sizing;
|
|
157
|
+
const { source, nodeTypes, replicasPerShard, reservedMemoryFraction } = sizing;
|
|
158
|
+
const row = (n) => {
|
|
159
|
+
if (n === null || typeof n !== "object") return n;
|
|
160
|
+
const { name, memoryGiB, ssdGiB, hourlyUSD, maxShards } = n;
|
|
161
|
+
return {
|
|
162
|
+
name,
|
|
163
|
+
memoryGiB,
|
|
164
|
+
hourlyUSD,
|
|
165
|
+
...ssdGiB === void 0 ? {} : { ssdGiB },
|
|
166
|
+
...maxShards === void 0 ? {} : { maxShards }
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
return {
|
|
170
|
+
source,
|
|
171
|
+
replicasPerShard,
|
|
172
|
+
reservedMemoryFraction,
|
|
173
|
+
nodeTypes: Array.isArray(nodeTypes) ? Array.from(nodeTypes, row) : nodeTypes
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function resolveBaseline(redis, storedGiB) {
|
|
177
|
+
if (redis === null || typeof redis !== "object") {
|
|
178
|
+
throw new ValidationError("pricing.redis must be { monthlyUSD } or { sizedToData }");
|
|
179
|
+
}
|
|
180
|
+
const { monthlyUSD, sizedToData } = redis;
|
|
181
|
+
const fixed = monthlyUSD !== void 0;
|
|
182
|
+
const sized = sizedToData !== void 0;
|
|
183
|
+
if (fixed === sized) {
|
|
184
|
+
throw new ValidationError(
|
|
185
|
+
"pricing.redis must be exactly one of { monthlyUSD } and { sizedToData }" + (fixed ? ", not both: spreading AWS_US_EAST_1_ONDEMAND.redis keeps its sizedToData, so pass redis: { monthlyUSD } alone" : "")
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
if (sized) {
|
|
189
|
+
const sizing = snapshotSizing(sizedToData);
|
|
190
|
+
validateSizing(sizing);
|
|
191
|
+
const cluster = cheapestCluster(sizing, storedGiB);
|
|
192
|
+
return { monthlyUSD: cluster.monthlyUSD, sized: { sizing, cluster } };
|
|
193
|
+
}
|
|
194
|
+
return { monthlyUSD: requireFiniteNonNeg(monthlyUSD, "pricing.redis.monthlyUSD") };
|
|
195
|
+
}
|
|
196
|
+
function bytesWords(bytes) {
|
|
197
|
+
const units = ["bytes", "KiB", "MiB", "GiB", "TiB", "PiB"];
|
|
198
|
+
let value = bytes;
|
|
199
|
+
let unit = 0;
|
|
200
|
+
const shown2 = (v, u) => u === 0 ? Math.round(v) : Number(v.toFixed(2));
|
|
201
|
+
while (unit < units.length - 1 && shown2(value, unit) >= 1024) {
|
|
202
|
+
value /= 1024;
|
|
203
|
+
unit += 1;
|
|
204
|
+
}
|
|
205
|
+
if (unit > 0) return `${value.toFixed(2)} ${units[unit]}`;
|
|
206
|
+
const n = Math.round(value);
|
|
207
|
+
return `${n} byte${n === 1 ? "" : "s"}`;
|
|
208
|
+
}
|
|
209
|
+
function clusterWords(c) {
|
|
210
|
+
const count = (n) => n.toLocaleString("en-US");
|
|
211
|
+
const nodes = `${count(c.nodes)} ${c.nodeType.name} node${c.nodes === 1 ? "" : "s"}`;
|
|
212
|
+
return c.shards === 1 ? `1 shard of ${nodes}` : `${count(c.shards)} shards, ${nodes}`;
|
|
213
|
+
}
|
|
214
|
+
function sizingBytes(spec) {
|
|
215
|
+
if (spec.sizeBytes !== void 0) return requireFiniteNonNeg(spec.sizeBytes, "segment.sizeBytes");
|
|
216
|
+
if (spec.cardinality !== void 0) {
|
|
217
|
+
return requireFiniteNonNeg(spec.cardinality, "segment.cardinality") * 2;
|
|
218
|
+
}
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
function buildReport(input) {
|
|
222
|
+
const { pricing, grounded } = input;
|
|
223
|
+
const { storage, redis } = pricing;
|
|
224
|
+
const S = SECONDS_PER_MONTH;
|
|
225
|
+
requireFiniteNonNeg(storage.getPerMillion, "pricing.storage.getPerMillion");
|
|
226
|
+
requireFiniteNonNeg(storage.putPerMillion, "pricing.storage.putPerMillion");
|
|
227
|
+
requireFiniteNonNeg(storage.storagePerGiBMonth, "pricing.storage.storagePerGiBMonth");
|
|
228
|
+
const pointerRead = requireFiniteNonNeg(
|
|
229
|
+
storage.requestsPerPointerRead ?? 1,
|
|
230
|
+
"pricing.storage.requestsPerPointerRead"
|
|
231
|
+
);
|
|
232
|
+
const sizedRead = requireFiniteNonNeg(
|
|
233
|
+
storage.requestsPerSizedRead ?? 1,
|
|
234
|
+
"pricing.storage.requestsPerSizedRead"
|
|
235
|
+
);
|
|
236
|
+
const storageBytes = requireFiniteNonNeg(input.storageBytes, "storageBytes");
|
|
237
|
+
const storedGiB = storageBytes / GIB;
|
|
238
|
+
const baseline = resolveBaseline(redis, storedGiB);
|
|
239
|
+
const baselineUSD = baseline.monthlyUSD;
|
|
240
|
+
if ((input.workload.cacheHitRate ?? 0) > 1) {
|
|
241
|
+
throw new ValidationError(
|
|
242
|
+
`cacheHitRate must be a fraction from 0 to 1; got ${String(input.workload.cacheHitRate)}`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
const cacheHitRate = clamp01(
|
|
246
|
+
requireFiniteNonNeg(input.workload.cacheHitRate ?? 0, "cacheHitRate")
|
|
247
|
+
);
|
|
248
|
+
const readsPerSec = requireFiniteNonNeg(input.workload.readsPerSec ?? 0, "readsPerSec");
|
|
249
|
+
const intersectsPerSec = requireFiniteNonNeg(
|
|
250
|
+
input.workload.intersectsPerSec ?? 0,
|
|
251
|
+
"intersectsPerSec"
|
|
252
|
+
);
|
|
253
|
+
const chunksPerIntersect = requireFiniteNonNeg(
|
|
254
|
+
input.workload.chunksPerIntersect ?? 1,
|
|
255
|
+
"chunksPerIntersect"
|
|
256
|
+
);
|
|
257
|
+
const loadsPerMonth = requireFiniteNonNeg(input.workload.loadsPerMonth ?? 0, "loadsPerMonth");
|
|
258
|
+
const requestsPerLoad = requireFiniteNonNeg(
|
|
259
|
+
input.workload.requestsPerLoad ?? 1,
|
|
260
|
+
"requestsPerLoad"
|
|
261
|
+
);
|
|
262
|
+
const operandsPerIntersect = requireFiniteNonNeg(
|
|
263
|
+
input.workload.operandsPerIntersect ?? 2,
|
|
264
|
+
"operandsPerIntersect"
|
|
265
|
+
);
|
|
266
|
+
if (operandsPerIntersect < 1) {
|
|
267
|
+
throw new ValidationError(
|
|
268
|
+
`operandsPerIntersect must be at least 1, since an intersection reads its operands; got ${operandsPerIntersect}`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
const hotSegments = requireFiniteNonNeg(input.workload.hotSegments ?? 0, "hotSegments");
|
|
272
|
+
const readerProcesses = requireFiniteNonNeg(
|
|
273
|
+
input.workload.readerProcesses ?? 1,
|
|
274
|
+
"readerProcesses"
|
|
275
|
+
);
|
|
276
|
+
if (readerProcesses < 1) {
|
|
277
|
+
throw new ValidationError(`readerProcesses must be at least 1; got ${readerProcesses}`);
|
|
278
|
+
}
|
|
279
|
+
const genTtlMs = requireFiniteNonNeg(
|
|
280
|
+
input.workload.genTtlMs ?? DEFAULT_CURRENT_GEN_TTL_MS,
|
|
281
|
+
"genTtlMs"
|
|
282
|
+
);
|
|
283
|
+
const retirementsPerMonth = requireFiniteNonNeg(
|
|
284
|
+
input.workload.retirementsPerMonth ?? 0,
|
|
285
|
+
"retirementsPerMonth"
|
|
286
|
+
);
|
|
287
|
+
const purgesPerMonth = requireFiniteNonNeg(input.workload.purgesPerMonth ?? 0, "purgesPerMonth");
|
|
288
|
+
const conditionalDelete = input.workload.conditionalDelete ?? true;
|
|
289
|
+
if (typeof conditionalDelete !== "boolean") {
|
|
290
|
+
throw new ValidationError(`conditionalDelete must be a boolean; got ${conditionalDelete}`);
|
|
291
|
+
}
|
|
292
|
+
const storageGetUSD = storage.getPerMillion / 1e6;
|
|
293
|
+
const putUSD = storage.putPerMillion / 1e6;
|
|
294
|
+
const missFraction = 1 - cacheHitRate;
|
|
295
|
+
const readMisses = readsPerSec * S * missFraction;
|
|
296
|
+
const intersects = intersectsPerSec * S;
|
|
297
|
+
const refreshes = genTtlMs > 0 ? Math.min(readerProcesses * hotSegments * S * 1e3 / genTtlMs, readsPerSec * S) : 0;
|
|
298
|
+
const storageUSD = storedGiB * storage.storagePerGiBMonth;
|
|
299
|
+
const readsUSD = readMisses * storageGetUSD;
|
|
300
|
+
const getsPerColdOperand = pointerRead + sizedRead;
|
|
301
|
+
const intersectGets = chunksPerIntersect + getsPerColdOperand * operandsPerIntersect;
|
|
302
|
+
const intersectsUSD = intersects * intersectGets * storageGetUSD;
|
|
303
|
+
const loadGets = STORE_LOAD_POINTER_READS * pointerRead + STORE_LOAD_EXISTENCE_CHECKS;
|
|
304
|
+
const loadsUSD = loadsPerMonth * ((requestsPerLoad + STORE_LOAD_PUT_CLASS) * putUSD + loadGets * storageGetUSD);
|
|
305
|
+
const refreshUSD = refreshes * pointerRead * storageGetUSD;
|
|
306
|
+
const sweep = RETENTION_SWEEP_REQUESTS[conditionalDelete ? "conditionalDelete" : "tombstoning"];
|
|
307
|
+
const retentionUSD = retirementsPerMonth * (sweep.retirement.reads * storageGetUSD + sweep.retirement.writes * putUSD) + purgesPerMonth * (sweep.purge.reads * storageGetUSD + sweep.purge.writes * putUSD);
|
|
308
|
+
const total = readsUSD + intersectsUSD + storageUSD + loadsUSD + refreshUSD + retentionUSD;
|
|
309
|
+
const headroom = Math.max(0, baselineUSD - storageUSD - refreshUSD);
|
|
310
|
+
const readsCross = storageGetUSD > 0 && missFraction > 0 ? headroom / (S * storageGetUSD * missFraction) : Infinity;
|
|
311
|
+
const verdict = total <= baselineUSD * 0.1 ? "win-big" : total < baselineUSD ? "win" : "lose-zone";
|
|
312
|
+
const dominant = Math.max(
|
|
313
|
+
readsUSD,
|
|
314
|
+
intersectsUSD,
|
|
315
|
+
storageUSD,
|
|
316
|
+
loadsUSD,
|
|
317
|
+
refreshUSD,
|
|
318
|
+
retentionUSD
|
|
319
|
+
);
|
|
320
|
+
let driver = "storage";
|
|
321
|
+
if (dominant === readsUSD && readsUSD > 0) driver = "point reads (object GETs)";
|
|
322
|
+
else if (dominant === intersectsUSD && intersectsUSD > 0) {
|
|
323
|
+
driver = "intersections (pointer, index and chunk GETs)";
|
|
324
|
+
} else if (dominant === loadsUSD && loadsUSD > 0)
|
|
325
|
+
driver = "loads (object, listing and pointer requests)";
|
|
326
|
+
else if (dominant === refreshUSD && refreshUSD > 0) driver = "the pointer refresh (object GETs)";
|
|
327
|
+
else if (dominant === retentionUSD && retentionUSD > 0)
|
|
328
|
+
driver = "the retention sweep (registry reads and writes)";
|
|
329
|
+
const sized = baseline.sized;
|
|
330
|
+
const noBytes = Math.round(storageBytes) === 0;
|
|
331
|
+
const redisWords = sized === void 0 ? void 0 : noBytes ? `$${baselineUSD.toFixed(2)}/mo cheapest Redis cluster in the catalogue, as no bytes are counted (${clusterWords(sized.cluster)})` : `$${baselineUSD.toFixed(2)}/mo Redis that would hold ${bytesWords(storageBytes)} (${clusterWords(sized.cluster)})`;
|
|
332
|
+
const rationale = verdict === "lose-zone" ? `pay-per-use total $${total.toFixed(2)}/mo exceeds the ${redisWords ?? `$${baselineUSD}/mo flat baseline`}; dominated by ${driver}` : verdict === "win-big" ? `$${total.toFixed(2)}/mo \u2014 \u226410% of the ${redisWords ?? `$${baselineUSD}/mo baseline`}; dominated by ${driver}` : `$${total.toFixed(2)}/mo, under the ${redisWords ?? `$${baselineUSD}/mo baseline`}; dominated by ${driver}`;
|
|
333
|
+
const dataTiering = sized !== void 0 && (sized.cluster.nodeType.ssdGiB ?? 0) > 0;
|
|
334
|
+
const redisNote = sized === void 0 ? `Redis priced at the fixed $${baselineUSD}/mo given, whatever the data size.` : (noBytes ? "Redis priced at the cheapest cluster in the catalogue, as no bytes are counted, because nothing is stored or nothing was measured" : `Redis priced at the cheapest cluster in the catalogue that holds the ${bytesWords(storageBytes)} stored`) + `: ${clusterWords(sized.cluster)}, each shard a primary and ${sized.sizing.replicasPerShard} replica(s), ${Number((sized.sizing.reservedMemoryFraction * 100).toFixed(1))}% of memory reserved (${sized.sizing.source}).` + (noBytes ? " Pass pricing.redis: { monthlyUSD } to compare against a Redis of your own." : " Compressed bytes are a floor on the memory Redis needs: a native Redis bitmap is sized by its highest id, so sparse ids need more.") + " Redis bought another way \u2014 fewer replicas, another engine, reserved nodes \u2014 can cost less than the catalogue prices it." + (dataTiering ? ` ${sized.cluster.nodeType.name} is a data-tiering node, which keeps the values read least recently on its SSD. It is priced as if the data read often fits in its memory: AWS recommends data tiering for workloads that regularly read up to 20% of their data.` : "") + (input.sizesFromCardinality ? " Some segment sizes came from cardinality, at 2 bytes an id, which is more than a dense bitmap needs, so this Redis may be larger than the data calls for: pass sizeBytes to size it to the compressed bytes." : "");
|
|
335
|
+
const notes = [
|
|
336
|
+
"Same-region egress treated as free; internet egress is not modeled.",
|
|
337
|
+
"Request cost is from the supplied workload rates (live-metrics-derived request cost is a later phase).",
|
|
338
|
+
loadsPerMonth > 0 ? `Loads modeled: ${loadsPerMonth}/mo, each ${requestsPerLoad} PUT-class request(s) for the object plus ${shown(STORE_LOAD_PUT_CLASS)} PUT-class and ${shown(loadGets)} GETs that store.load() adds (the pointer, a check that the next generation number is free, a check that the current generation is there, and a listing every ${LIST_COLLECTION_CADENCE}th load).` : "Loads are NOT modeled \u2014 set workload.loadsPerMonth (+ requestsPerLoad for multipart) to include them.",
|
|
339
|
+
...intersects > 0 ? [
|
|
340
|
+
`Intersections priced cold: ${intersectGets} GETs each, ${getsPerColdOperand} for each of ${operandsPerIntersect} operand(s) plus ${chunksPerIntersect} chunk range request(s); cacheHitRate does not apply.`
|
|
341
|
+
] : [],
|
|
342
|
+
hotSegments > 0 ? genTtlMs > 0 ? `Pointer refresh modeled: ${hotSegments} hot segment(s) in each of ${readerProcesses} reader process(es), each re-reading its pointer at most every ${genTtlMs} ms, and at most once a point read.` : "Pointer refresh: none \u2014 genTtlMs 0 turns the timed refresh off. A reader still re-reads a pointer on an eviction, a swept generation or an invalidation, which this does not price." : readsPerSec > 0 ? "The pointer refresh is NOT modeled \u2014 set workload.hotSegments to the segments each long-lived reader keeps reading, and workload.readerProcesses to how many readers there are." : null,
|
|
343
|
+
hotSegments > DEFAULT_MAX_OPEN_SEGMENTS ? `More hot segments in a reader than a store keeps open by default (${DEFAULT_MAX_OPEN_SEGMENTS}): a read of a segment ` + (genTtlMs > 0 ? "the reader evicted opens it again when the read needs its index or an uncached chunk, a tail read, and reads its pointer too once the store has let its resolution go as well, which this does not price. " : "the reader evicted opens it again, a pointer and a tail read, which this does not price. ") + "Raise cache.readerMax, and cache.readerMaxBytes, to keep them open." : null,
|
|
344
|
+
...retirementsPerMonth > 0 || purgesPerMonth > 0 ? [
|
|
345
|
+
`Retention sweep modeled: ${retirementsPerMonth}/mo retirements at ${sweep.retirement.reads} reads, ${sweep.retirement.writes} writes and ${sweep.retirement.deletes} delete(s) each, ${purgesPerMonth}/mo purges at ${sweep.purge.reads} reads, ${sweep.purge.writes} writes and ${sweep.purge.deletes} delete(s) each; reads are priced at the GET rate and writes at the PUT rate, deletes at nothing, as S3 bills none` + (conditionalDelete ? "." : "; a registry that only tombstones is read again, two reads per purged segment, by every later full sweep, which is not priced.")
|
|
346
|
+
] : [],
|
|
347
|
+
...input.extraNotes ?? [],
|
|
348
|
+
// Last, so the notes a report already carried keep their places.
|
|
349
|
+
redisNote
|
|
350
|
+
].filter((n) => n !== null);
|
|
351
|
+
return {
|
|
352
|
+
monthlyUSD: {
|
|
353
|
+
byOp: {
|
|
354
|
+
reads: readsUSD,
|
|
355
|
+
intersects: intersectsUSD,
|
|
356
|
+
storage: storageUSD,
|
|
357
|
+
loads: loadsUSD,
|
|
358
|
+
pointerRefresh: refreshUSD,
|
|
359
|
+
retention: retentionUSD
|
|
360
|
+
},
|
|
361
|
+
total
|
|
362
|
+
},
|
|
363
|
+
redisBaseline: sized === void 0 ? { basis: "fixed", monthlyUSD: baselineUSD } : {
|
|
364
|
+
basis: "sized-to-data",
|
|
365
|
+
monthlyUSD: baselineUSD,
|
|
366
|
+
cluster: {
|
|
367
|
+
nodeType: sized.cluster.nodeType.name,
|
|
368
|
+
shards: sized.cluster.shards,
|
|
369
|
+
nodes: sized.cluster.nodes,
|
|
370
|
+
dataTiering
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
redisCrossover: { readsPerSec: readsCross },
|
|
374
|
+
verdict,
|
|
375
|
+
rationale,
|
|
376
|
+
assumptions: { cacheHitRate, pricingName: pricing.name, grounded, notes }
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function checkModelInputs(op, pricing, workload) {
|
|
380
|
+
const storage = pricing !== null && typeof pricing === "object" ? pricing.storage : void 0;
|
|
381
|
+
if (pricing !== void 0 && (storage === null || typeof storage !== "object")) {
|
|
382
|
+
throw new ValidationError(`${op}: pricing must be a profile with a storage price list`);
|
|
383
|
+
}
|
|
384
|
+
if (workload !== void 0 && workload !== null && (typeof workload !== "object" || Array.isArray(workload))) {
|
|
385
|
+
throw new ValidationError(`${op}: workload must be an object`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function estimateCost(input) {
|
|
389
|
+
if (input === null || typeof input !== "object") {
|
|
390
|
+
throw new ValidationError("estimateCost: input must be an object such as { segments }");
|
|
391
|
+
}
|
|
392
|
+
if (!Array.isArray(input.segments)) {
|
|
393
|
+
throw new ValidationError(
|
|
394
|
+
"estimateCost: segments must be an array of { sizeBytes } or { cardinality }"
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
for (let i = 0; i < input.segments.length; i++) {
|
|
398
|
+
const spec = input.segments[i];
|
|
399
|
+
if (spec === null || typeof spec !== "object") {
|
|
400
|
+
throw new ValidationError(`estimateCost: segments[${i}] must be an object`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
checkModelInputs("estimateCost", input.pricing, input.workload);
|
|
404
|
+
const pricing = input.pricing ?? AWS_US_EAST_1_ONDEMAND;
|
|
405
|
+
const workload = input.workload ?? {};
|
|
406
|
+
let storageBytes = 0;
|
|
407
|
+
let sizesFromCardinality = false;
|
|
408
|
+
for (const spec of input.segments) {
|
|
409
|
+
const count = Math.floor(requireFiniteNonNeg(spec.count ?? 1, "segment.count"));
|
|
410
|
+
storageBytes += sizingBytes(spec) * count;
|
|
411
|
+
if (spec.sizeBytes === void 0 && spec.cardinality !== void 0 && sizingBytes(spec) * count > 0) {
|
|
412
|
+
sizesFromCardinality = true;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return buildReport({ storageBytes, workload, pricing, grounded: false, sizesFromCardinality });
|
|
416
|
+
}
|
|
417
|
+
var GROUNDED_KEYS = /* @__PURE__ */ new Set(["storageBytes", "workload", "pricing"]);
|
|
418
|
+
function groundedReport(input) {
|
|
419
|
+
if (input === null || typeof input !== "object") {
|
|
420
|
+
throw new ValidationError("groundedReport: input must be an object such as { storageBytes }");
|
|
421
|
+
}
|
|
422
|
+
for (const key of Object.keys(input)) {
|
|
423
|
+
if (!GROUNDED_KEYS.has(key)) {
|
|
424
|
+
throw new ValidationError(
|
|
425
|
+
`groundedReport: unknown option "${key}"; it takes { storageBytes, workload?, pricing? } (pass a stat()'s \`size\` as storageBytes)`
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const bytes = input.storageBytes;
|
|
430
|
+
if (bytes !== null && (typeof bytes !== "number" || !Number.isFinite(bytes) || bytes < 0)) {
|
|
431
|
+
throw new ValidationError(
|
|
432
|
+
`groundedReport: storageBytes must be a byte count (a finite number >= 0) or null when nothing was measured; got ${typeof bytes === "string" ? `${JSON.stringify(bytes)} (a string)` : String(bytes)}`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
checkModelInputs("groundedReport", input.pricing, input.workload);
|
|
436
|
+
const measured = input.storageBytes !== null;
|
|
437
|
+
return buildReport({
|
|
438
|
+
storageBytes: measured ? input.storageBytes : 0,
|
|
439
|
+
workload: input.workload ?? {},
|
|
440
|
+
pricing: input.pricing ?? AWS_US_EAST_1_ONDEMAND,
|
|
441
|
+
grounded: measured,
|
|
442
|
+
extraNotes: measured ? void 0 : [
|
|
443
|
+
"storageBytes is null \u2014 no size was measured (a segment with no generation, or a store whose source cannot report one), so storage is reported as $0."
|
|
444
|
+
]
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
export {
|
|
448
|
+
AWS_US_EAST_1_ONDEMAND,
|
|
449
|
+
ELASTICACHE_REDIS_US_EAST_1_ONDEMAND,
|
|
450
|
+
ONE_REDIS_HA_CLUSTER,
|
|
451
|
+
estimateCost,
|
|
452
|
+
groundedReport
|
|
453
|
+
};
|
|
454
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/cost.ts", "../src/store-defaults.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Cost model & estimator \u2014 pure arithmetic: no I/O, no time, no SDK, and nothing of a store's but the figures it\n * is handed. It prices the economics of the loaded store:\n *\n * - {@link estimateCost} \u2014 **planning** mode: pure what-if from segment sizes + a workload (sizing/sales).\n * - {@link groundedReport} \u2014 **grounded** mode: a segment's real size, which its handle's `stat()` reports as\n * `size` from the `.crbm` footer and index (no payload reads), + a supplied workload for request rates.\n *\n * Guiding split: **formulas are the spec; rates are a pluggable {@link PricingProfile}.** The report always emits\n * a {@link CostReport.verdict} that includes the lose-zone \u2014 it never hides where an always-on cache (Redis)\n * wins.\n *\n * What the model covers, and states in `assumptions.notes`: object-store GETs for point reads, for\n * intersections (each operand's pointer and index as well as its chunks) and for the pointer refresh a long-lived\n * reader pays; the requests of the retention sweep (a retirement and a purge, each priced from the registry requests\n * it makes); the requests of a load (the object's write, and the pointer reads and write, the checks of the next\n * generation number and of the current generation's object, and the listing of every 16th generation that\n * `store.load()` makes around it); and storage. Same-region egress is\n * treated as free and internet egress is not modeled; request cost is derived from the supplied workload rates\n * (deriving it from live metrics counters is a later refinement). There is no per-write term because the loaded\n * store has no per-id write: data arrives as generations, and a generation is a load.\n *\n * Every request count below is one the engine makes, and `tests/tools/cost.test.ts` holds each to the engine by\n * counting what it sends: the model is only as honest as those counts, and they move when the engine does. They\n * are for a single-bucket store, where the pointer is an object beside the data, which is the topology that\n * ships. A load is more than the object's PUT, and an intersect more than its chunk reads: pricing either as the\n * one alone under-quotes a load by more than half and leaves out a pointer read and an index read for every\n * operand.\n *\n * The counts are S3's (and GCS's), and one reader process's. A pointer read is one request on every backend, and\n * {@link PricingProfile} carries it as `requestsPerPointerRead`. A segment's tail read needs the object's size, and\n * on Azure Blob, which takes no suffix range, it is two requests, the properties and then the bytes, which\n * {@link PricingProfile} carries as `requestsPerSizedRead`. A fleet of reader processes pays the pointer refresh\n * once per process, which {@link Workload.readerProcesses} carries. Where the model still quotes low is listed on\n * {@link Workload.hotSegments} and {@link Workload.chunksPerIntersect}.\n */\nimport { ValidationError } from '@cloudbitmaps/core';\nimport {\n DEFAULT_CURRENT_GEN_TTL_MS,\n DEFAULT_MAX_OPEN_SEGMENTS,\n LIST_COLLECTION_CADENCE,\n} from './store-defaults';\n\n/**\n * Freeze a constant and everything in it, so a caller that changes one \u2014 a `push` onto a catalogue's rows \u2014 cannot\n * change the default for every other caller in the process: the change throws in strict mode, as in every ES module,\n * and is ignored in a sloppy-mode script.\n */\nfunction deepFreeze<T>(value: T): Readonly<T> {\n if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {\n Object.freeze(value);\n for (const inner of Object.values(value)) deepFreeze(inner);\n }\n return value;\n}\n\n/** Pluggable rate card. Rates differ by cloud/region/term and drift over time; the formulas don't. */\nexport interface PricingProfile {\n readonly name: string;\n readonly storage: {\n /** Object GET (per **million** requests). A ranged GET bills as a full GET. */\n readonly getPerMillion: number;\n /** Object PUT (per million) \u2014 what a load pays, per request. */\n readonly putPerMillion: number;\n readonly storagePerGiBMonth: number;\n /**\n * Requests one pointer read costs: a read of a segment's registry row, whose version comes back with its bytes.\n * Default **1**, and 1 on S3, GCS and Azure Blob, each of which answers it with one GET. Charged for each operand\n * of an intersection, for the pointer reads a load makes, and for each pointer refresh. Set it when a\n * registry of your own takes more than one request to read a row.\n */\n readonly requestsPerPointerRead?: number;\n /**\n * Requests one tail read costs: the read of a segment's index from the end of its generation, which needs the\n * object's size. Default **1**, S3's, whose suffix-range GET returns the size with the bytes, and GCS's, the same.\n * **2** on Azure Blob, which takes no suffix range and reads the properties and then the bytes. Charged for each\n * operand of an intersection. A load reads no index of its own: a row that carries a summary of its current\n * generation gives the load the size it needs, and the checks it makes are one metadata request on every backend.\n * A chunk read knows its range, and is one request everywhere.\n */\n readonly requestsPerSizedRead?: number;\n };\n /**\n * The always-on Redis the verdict compares against: exactly one of the two. `sizedToData`, the default's, prices\n * the cheapest cluster that holds the report's stored bytes; `monthlyUSD` is one cluster you name, whatever the\n * data size. A profile carrying both is refused rather than read one way.\n */\n readonly redis:\n | { readonly monthlyUSD: number; readonly sizedToData?: never }\n | { readonly sizedToData: RedisSizing; readonly monthlyUSD?: never };\n}\n\n/** One node type a Redis cluster can be built from, as its cloud prices it. */\nexport interface RedisNodeType {\n readonly name: string;\n /** Memory, in GiB, as the cloud publishes it for the node type. */\n readonly memoryGiB: number;\n /**\n * SSD, in GiB, on a data-tiering node (ElastiCache's `r6gd`): keys stay in memory, and the values read least\n * recently move to the SSD. Counted in full beside the memory, as AWS counts a data-tiering node's capacity.\n */\n readonly ssdGiB?: number;\n /** Price per node-hour, in USD. */\n readonly hourlyUSD: number;\n /**\n * The most shards a cluster of this type is priced at. The default catalogue prices its burstable nodes as one\n * shard: a cluster sharded across them is not how data anyone queries is held. Default: no limit.\n */\n readonly maxShards?: number;\n}\n\n/**\n * Redis sized to hold the data: enough shards for the bytes, each a primary and its replicas, on whichever node\n * type in `nodeTypes` makes that cheapest. Prices within a millionth of a dollar of the cheapest are a tie, which\n * fewer nodes win, then the lower price, then the name, so the answer never depends on the order of the rows. The data is held at its stored,\n * compressed size, which is a floor on the memory Redis needs for it: a native Redis bitmap is sized by its highest\n * id, not by how many ids it holds, so sparse ids need more.\n */\nexport interface RedisSizing {\n /** Where the prices come from, and when; the report's notes quote it. */\n readonly source: string;\n readonly nodeTypes: readonly RedisNodeType[];\n /** Replicas per shard. AWS's best practice is 2; Multi-AZ needs at least 1. */\n readonly replicasPerShard: number;\n /** The share of each node's memory kept back from data: ElastiCache's `reserved-memory-percent`, 25% by default. */\n readonly reservedMemoryFraction: number;\n}\n\n/**\n * **ElastiCache for Redis OSS in us-east-1, on-demand**: node prices from AWS's public price list, version\n * 20260914063714 (published 2026-09-14, effective 2026-09-01,\n * https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonElastiCache/20260914063714/us-east-1/index.json), and the\n * memory AWS publishes for each node type. Every shard is a primary and two replicas, AWS's best practice, with\n * ElastiCache's default 25% of each node's memory reserved, and the burstable `t4g` nodes are priced as one shard.\n *\n * It lists only rows that are the cheapest fit for some size of data: the data-tiering `r6gd` rows above about\n * 30 GiB, and the larger in-memory `r6g` rows, `2xlarge` and up, when those are left out. The current `m7g` and `r7g` nodes are not here,\n * because an `m6g` or `r6g` with the same memory costs less. It is the cheapest cluster of THIS kind; Redis bought\n * another way costs less \u2014 one replica a shard a third less, ElastiCache for Valkey 20% less a node, reserved nodes\n * less again \u2014 so against those the saving the verdict reports is smaller. Pass their prices to compare with them.\n */\nexport const ELASTICACHE_REDIS_US_EAST_1_ONDEMAND: RedisSizing = deepFreeze({\n source: 'ElastiCache for Redis OSS, us-east-1 on-demand, AWS price list 20260914063714',\n replicasPerShard: 2,\n reservedMemoryFraction: 0.25,\n nodeTypes: [\n { name: 'cache.t4g.micro', memoryGiB: 0.5, hourlyUSD: 0.016, maxShards: 1 },\n { name: 'cache.t4g.small', memoryGiB: 1.37, hourlyUSD: 0.032, maxShards: 1 },\n { name: 'cache.t4g.medium', memoryGiB: 3.09, hourlyUSD: 0.065, maxShards: 1 },\n { name: 'cache.m6g.large', memoryGiB: 6.38, hourlyUSD: 0.149 },\n { name: 'cache.r6g.large', memoryGiB: 13.07, hourlyUSD: 0.206 },\n { name: 'cache.r6g.xlarge', memoryGiB: 26.32, hourlyUSD: 0.411 },\n { name: 'cache.r6g.2xlarge', memoryGiB: 52.82, hourlyUSD: 0.821 },\n { name: 'cache.r6g.4xlarge', memoryGiB: 105.81, hourlyUSD: 1.642 },\n { name: 'cache.r6g.8xlarge', memoryGiB: 209.55, hourlyUSD: 3.284 },\n { name: 'cache.r6g.12xlarge', memoryGiB: 317.77, hourlyUSD: 4.925 },\n { name: 'cache.r6g.16xlarge', memoryGiB: 419.09, hourlyUSD: 6.567 },\n { name: 'cache.r6gd.xlarge', memoryGiB: 26.32, ssdGiB: 99.33, hourlyUSD: 0.781 },\n { name: 'cache.r6gd.2xlarge', memoryGiB: 52.82, ssdGiB: 199.07, hourlyUSD: 1.56 },\n { name: 'cache.r6gd.4xlarge', memoryGiB: 105.81, ssdGiB: 398.14, hourlyUSD: 3.12 },\n { name: 'cache.r6gd.8xlarge', memoryGiB: 209.55, ssdGiB: 796.28, hourlyUSD: 6.24 },\n { name: 'cache.r6gd.12xlarge', memoryGiB: 317.77, ssdGiB: 1194.42, hourlyUSD: 9.358 },\n { name: 'cache.r6gd.16xlarge', memoryGiB: 419.09, ssdGiB: 1592.56, hourlyUSD: 12.477 },\n ],\n});\n\n/**\n * One ElastiCache HA cluster, a primary and two replicas of cache.m7g.large \u2014 the m7g family's smallest node \u2014\n * at $0.158 an hour each: 3 \u00D7 730 h \u00D7 $0.158 = $346.02, published to the dollar. The per-request crossovers on the\n * benchmarks page are drawn against this one cluster, whatever the data size; pass it as `pricing.redis` to compare\n * with it. It is not the cheapest cluster for any size of data: three cache.m6g.large hold the same memory for less.\n */\n// The trailing comment below is parsed by scripts/site-figures.cjs, and the literal by bench/lib/calibration-figures.cjs.\nexport const ONE_REDIS_HA_CLUSTER = deepFreeze({ monthlyUSD: 346 }); // ElastiCache HA: 1 primary + 2 replicas (cache.m7g.large); ~$115 single-node\n\n/**\n * Default profile \u2014 **AWS us-east-1, on-demand**, mid-2026, from the fact-checked published pricing rather than\n * copied from a blog post, with Redis sized to the data. Override it for your region, cloud, or committed term.\n * Its prices are as old as the release of this package that ships them: a planning figure, not a quote.\n */\nexport const AWS_US_EAST_1_ONDEMAND: PricingProfile = deepFreeze({\n name: 'aws-us-east-1-ondemand',\n storage: { getPerMillion: 0.4, putPerMillion: 5.0, storagePerGiBMonth: 0.023 },\n redis: { sizedToData: ELASTICACHE_REDIS_US_EAST_1_ONDEMAND },\n});\n\n/** Sustained access pattern. All rates default to 0; unspecified \u21D2 that op contributes nothing. */\nexport interface Workload {\n /**\n * Point reads (`has`) per second. Each cache miss is one object GET, once the segment's pointer and index are\n * read.\n */\n readonly readsPerSec?: number;\n readonly intersectsPerSec?: number;\n /** Cache hit rate in `[0, 1]` \u2014 hits are free; only misses cost. Default 0. */\n readonly cacheHitRate?: number;\n /**\n * Range requests one intersection makes for its chunks, summed over its operands. Default 1. The engine reads each\n * operand's needed chunks (the chunk-skipping survivors) as coalesced ranges: chunks within 256 KiB of each other\n * are read in one request, up to 1 MiB, so this is the requests the chunks need, not the chunks. It follows the\n * bytes the needed chunks span in each object: two segments sharing 100 chunks that sit side by side make one request\n * of each, `2`; the same 100 spread over each object make as many as it takes to cover most of the object, a\n * request per MiB, and read most of its bytes. Count it by running the engine over a layout of your own, as\n * `bench/range-counts.cjs` does, or take it as one request per MiB the shared chunks span, per operand. The model adds each operand's pointer and index reads itself (see\n * {@link Workload.operandsPerIntersect}), so count chunk requests only. One more GET per operand whose index\n * outgrows the reader's tail read (256 KiB by default), which then reads the index whole, belongs here too, and so\n * does a pointer re-read by an intersect slow enough to outlive {@link Workload.genTtlMs}.\n */\n readonly chunksPerIntersect?: number;\n /**\n * Segments each intersection reads, `exclude` operands included; at least 1. Default 2. An intersection is priced\n * **cold**: before its chunks, each operand's pointer is read, then its index, in one read of the object's tail \u2014\n * 2 GETs an operand on S3 and GCS, so a cold intersect that makes `r` range requests of each of two segments is `4 + 2r` GETs there,\n * and 3 on Azure Blob, whose tail read is two requests (see {@link PricingProfile}). `cacheHitRate`\n * does not apply to intersections, so a long-lived reader that answers a repeat from its cache pays less. Other\n * combines read their operands the same way and can be priced here too, with the chunk requests they make.\n */\n readonly operandsPerIntersect?: number;\n /**\n * Generations published per month across the modeled data \u2014 the write side of a loaded store. Default **0**\n * \u21D2 loads are not modeled and the report *discloses* the omission rather than silently under-reporting.\n */\n readonly loadsPerMonth?: number;\n /**\n * PUT-class requests one load's object write issues, each priced at the PUT rate. Default **1** (a single-object\n * PUT). A multipart write of `P` parts bills `P + 2` (initiate, the parts, complete) \u2014 set it when you know your\n * object sizes. The model adds what `store.load()` does around the write, at any `keep` from 1 to 64: the pointer's\n * write, PUT-class on S3, and four GETs: the pointer read twice, one check that the next generation number is\n * free, and one that the current generation's object is there, which tells the collection that deletes by name it\n * may. The current generation's size comes from the row's summary of it, so the load reads no index. Collection\n * deletes the generation the window pushed out by name, so it lists only on every 16th generation, which adds a\n * PUT-class request and two pointer reads there and makes no check that the current generation is there, a\n * sixteenth of each on average. That is a segment with two\n * generations behind it, whose row carries a summary; its first two loads make fewer requests and collect nothing,\n * and the first load onto a row with no summary of its current generation (one a rollback left when it could not\n * open the key, one a registry of your own stores without one, or one this store cannot use) reads that generation's index, one tail read, in\n * place of the check that its object is there. A publish that loses a race to another writer\n * reads the pointer again, and a load whose check finds the number taken (a crashed load's object, or the\n * generations a rollback left above the pointer) lists the segment's objects to number past them and to collect,\n * two PUT-class requests on S3 and two more pointer reads. So does every load whose `keep` is above 64, which lists\n * to collect: one more PUT-class request and two more pointer reads than the model counts. The counts\n * are a cleartext segment's: an encrypted segment's load reads its row once more, after its ids and before it\n * unwraps the key, one more GET ($0.40 per million at the default prices) that the model leaves out, beside the\n * key-management calls it does not price either.\n */\n readonly requestsPerLoad?: number;\n /**\n * Segments each long-lived reader process keeps reading. A reader re-reads a segment's pointer when it reads the\n * segment after {@link Workload.genTtlMs} has passed, so each costs at most one GET per `genTtlMs` in each process\n * that reads it \u2014 1,314,000 GETs a month at the default 2 s \u2014 and the whole term at most one GET per point read\n * (`readsPerSec`, across the fleet). Intersections are priced cold and pay their own pointer reads. Default **0**\n * \u21D2 the refresh is not modeled, and the report *discloses* the omission when there are reads to refresh for.\n *\n * It assumes each hot segment stays open in the reader's cache (`cache.readerMax`, 1,024 segments by default,\n * and `cache.readerMaxBytes`, 64 MiB of parsed index). A read of a segment the cache evicted that needs its index or\n * a chunk the chunk cache does not hold opens it again, a tail read, which is not priced here; with a timed refresh the store keeps the segment's resolution apart from its\n * reader, so the eviction reads its pointer again only for a segment whose resolution was let go too, and with none\n * every reopen reads it. Neither is the re-open every reader makes after each load priced, for the new generation's\n * index. Size those caches to keep the hot set open; the report says so when `hotSegments` is more than a store\n * keeps open by default.\n */\n readonly hotSegments?: number;\n /**\n * Long-lived reader processes, each keeping its own {@link Workload.hotSegments} open and refreshing their\n * pointers on its own: ten processes reading the same hundred segments pay ten times one process's refresh.\n * At least 1. Default 1.\n */\n readonly readerProcesses?: number;\n /**\n * How long the reader trusts a pointer, in ms: the store's `cache.genTtlMs`. Default 2000, the store's own\n * default; pass the store's when it sets one. `0` turns the timed refresh off, and the model bills none: a\n * reader then re-reads a pointer only when its cache evicts the segment, a read finds its generation swept, or a\n * write invalidates it, none of which this term prices.\n */\n readonly genTtlMs?: number;\n /**\n * Segments the retention sweep (`retireExpired`) retires a month. Each costs 7 registry reads, 2 writes and a\n * delete with {@link Workload.conditionalDelete} on, and 6 reads and 2 writes with it off, priced at the GET and PUT\n * rates, a delete unbilled, as S3 leaves it. Default\n * **0**.\n */\n readonly retirementsPerMonth?: number;\n /**\n * Tombstones the sweep purges a month: at a steady state, as many as it retires a month, a `tombstoneGraceMs` later.\n * A purge costs 4 reads and 2 deletes with {@link Workload.conditionalDelete} on, and 3 reads and a write with it\n * off. Default **0**.\n */\n readonly purgesPerMonth?: number;\n /**\n * Whether the registry's deletes remove a row for good (`RegCaps.conditionalDelete`), which the shipped registries\n * do where the backend applies a delete precondition. Default **true**. `false` prices the sweep of a registry that\n * only tombstones: a purge rewrites the row instead of deleting it, and every later full sweep reads each tombstone\n * left (two reads per purged segment), which this term does not price.\n */\n readonly conditionalDelete?: boolean;\n}\n\n/** One (group of) segment(s) for planning. `count` = how many like this (default 1). */\nexport interface SegmentSizing {\n readonly sizeBytes?: number;\n readonly cardinality?: number;\n /** Number of segments with these characteristics. Default 1. */\n readonly count?: number;\n}\n\nexport interface EstimateInput {\n readonly segments: readonly SegmentSizing[];\n readonly workload?: Workload;\n readonly pricing?: PricingProfile;\n}\n\nexport interface CostReport {\n readonly monthlyUSD: {\n readonly byOp: {\n readonly reads: number;\n readonly intersects: number;\n readonly storage: number;\n /**\n * Loads: each its object's `requestsPerLoad` PUT-class requests plus what `store.load()` adds around them.\n * 0 unless `loadsPerMonth` is set.\n */\n readonly loads: number;\n /** The pointer refresh of {@link Workload.hotSegments}. 0 unless `hotSegments` is set. */\n readonly pointerRefresh: number;\n /**\n * The retention sweep's retirements and purges: each's registry reads at the GET rate and its writes at the PUT\n * rate. 0 unless `retirementsPerMonth` or `purgesPerMonth` is set.\n */\n readonly retention: number;\n };\n readonly total: number;\n };\n /**\n * The Redis the verdict compares against. Sized, the default, it is the cheapest cluster in\n * `pricing.redis.sizedToData` that holds this report's stored bytes; fixed, it is `pricing.redis.monthlyUSD` as\n * given.\n *\n * **It is not additive across reports.** Each report sizes Redis to its own bytes, so a per-segment report\n * (`groundedReport` of one segment's size) compares with a cluster holding that one segment alone, and the baselines of a store's\n * segments do not sum to the store's. To judge a store, price all its segments in one `estimateCost`. To alarm on\n * one, sum `monthlyUSD.total` over its segments and compare the sum with the Redis you would run for it: a\n * per-segment verdict against that whole price fires only when one segment alone costs more than all of it.\n */\n readonly redisBaseline:\n | { readonly basis: 'fixed'; readonly monthlyUSD: number }\n | {\n readonly basis: 'sized-to-data';\n readonly monthlyUSD: number;\n /** The node type, the shards, the nodes (every shard's primary and replicas), and whether they tier to SSD. */\n readonly cluster: {\n readonly nodeType: string;\n readonly shards: number;\n readonly nodes: number;\n readonly dataTiering: boolean;\n };\n };\n /**\n * Sustained read rate at which the pay-per-use model's cost passes {@link CostReport.redisBaseline}, **evaluated\n * at this report's `cacheHitRate`** \u2014 so a higher cache-hit rate raises it (cache hits are free) \u2014 with the other\n * request axes at 0, and with what keeping the data readable costs taken out of the baseline first: storage, and\n * this report's pointer refresh. Loads, the write side, are left out of it. `Infinity` means it never crosses (a\n * 100% cache-hit rate). The published anchor (~329 reads/s) is against {@link ONE_REDIS_HA_CLUSTER}, at\n * `cacheHitRate: 0` with no refresh modeled.\n */\n readonly redisCrossover: { readonly readsPerSec: number };\n readonly verdict: 'win-big' | 'win' | 'lose-zone';\n readonly rationale: string;\n readonly assumptions: {\n readonly cacheHitRate: number;\n readonly pricingName: string;\n /**\n * True when the size was measured (a `groundedReport` given a byte count), false for a pure `estimateCost` and\n * for a `groundedReport` given `storageBytes: null`.\n */\n readonly grounded: boolean;\n /** What the model assumed and what it left out, in words. The last says how the Redis was priced. */\n readonly notes: readonly string[];\n };\n}\n\nconst HOURS_PER_MONTH = 730; // AWS's own convention: 365 days \u00D7 24 hours \u00F7 12 months\nconst SECONDS_PER_MONTH = HOURS_PER_MONTH * 3600; // 2,628,000\nconst GIB = 1024 ** 3;\n\n/**\n * What `store.load()` adds to its object's write, as the engine makes the requests on a segment with a window of\n * generations behind it, with nothing above the pointer, at any `keep` from 1 to 64. The row records the generations\n * the load keeps, so collection deletes by name the generation or generations the window pushed out, and the pointer is\n * read twice (the load's one read before its publish, which the publish's compare-and-swap writes against without\n * reading the row again, and the re-read before the delete) and nothing is listed, except on every\n * {@link LIST_COLLECTION_CADENCE}th generation, where collection lists instead: one more PUT-class request and two more\n * pointer reads (before and after the listing). Those two are averaged over the cadence, so a count of `n` loads is\n * exact for `n` consecutive generations of the cadence. PUT-class: the pointer's write and the averaged listing.\n * Pointer reads each cost `requestsPerPointerRead` requests. The load reads no index: the row's summary of the current\n * generation gives it the size the guard needs. It makes two checks, each a single metadata request on every backend\n * (S3's `HeadObject`, GCS's object metadata, Azure Blob's properties), so `requestsPerSizedRead` applies to neither:\n * that the next generation number is free, on every load, and that the current generation's object is there, before\n * the collection deletes by name, so on every load but the listing one. `tests/tools/cost.test.ts` holds these to the\n * engine over sixteen consecutive loads at a `keep` of 1, 2, 12 and 64. A load whose `keep` is above 64 records no list\n * and lists on every load, and so does the first load of a row that records none, and one whose check meets an object:\n * one more PUT-class request and two more pointer reads than these, and one check fewer. This model prices the first\n * kind and not the others.\n */\nconst STORE_LOAD_PUT_CLASS = 1 + 1 / LIST_COLLECTION_CADENCE;\nconst STORE_LOAD_POINTER_READS = 2 + 2 / LIST_COLLECTION_CADENCE;\nconst STORE_LOAD_EXISTENCE_CHECKS = 2 - 1 / LIST_COLLECTION_CADENCE;\n\n/** The requests one segment's retirement or purge makes: reads (GET-class), writes (PUT-class) and deletes. */\ninterface SweepRequests {\n readonly reads: number;\n readonly writes: number;\n readonly deletes: number;\n}\n\n/**\n * What the retention sweep makes per segment, counted with a store that counts its requests\n * (`tests/core/retention-hard-purge.test.ts` holds each figure to the engine, and `tests/tools/cost.test.ts` holds the\n * estimator to these). With the registry's `conditionalDelete` on, a retirement files a due-index pointer to the\n * tombstone and a purge removes the row and every pointer: a later sweep reads nothing of a purged segment. With it\n * off, a retirement files no pointer, a purge rewrites the row as a tombstone, and every later full sweep reads what\n * is left, two objects per purged segment.\n */\nexport const RETENTION_SWEEP_REQUESTS = deepFreeze({\n conditionalDelete: {\n retirement: { reads: 7, writes: 2, deletes: 1 },\n purge: { reads: 4, writes: 0, deletes: 2 },\n },\n tombstoning: {\n retirement: { reads: 6, writes: 2, deletes: 0 },\n purge: { reads: 3, writes: 1, deletes: 0 },\n },\n}) satisfies Readonly<Record<string, Readonly<Record<string, SweepRequests>>>>;\n\n/** A count of requests for a note: at most two decimals, none when it is whole. */\nconst shown = (n: number): string => String(Number(n.toFixed(2)));\n\n/** Fail-fast at the boundary: reject non-finite / negative inputs rather than leak NaN into the report. */\nfunction requireFiniteNonNeg(n: number | undefined, field: string): number {\n if (n === undefined || !Number.isFinite(n) || n < 0) {\n throw new ValidationError(`${field} must be a finite number >= 0; got ${n}`);\n }\n return n;\n}\n\nfunction clamp01(n: number): number {\n return n < 0 ? 0 : n > 1 ? 1 : n;\n}\n\n/** Two cluster prices closer than this are the same price. */\nconst TIE_USD = 1e-6;\n\ninterface SizedCluster {\n readonly nodeType: RedisNodeType;\n readonly shards: number;\n readonly nodes: number;\n readonly monthlyUSD: number;\n}\n\n/** The node's share of the data: its memory less the reserve, and all of a data-tiering node's SSD. */\nfunction usableGiB(sizing: RedisSizing, nodeType: RedisNodeType): number {\n return nodeType.memoryGiB * (1 - sizing.reservedMemoryFraction) + (nodeType.ssdGiB ?? 0);\n}\n\n/** Fail-fast on a caller-supplied catalogue: a bad row would otherwise win on NaN, or never fit anything. */\nfunction validateSizing(sizing: RedisSizing | undefined): asserts sizing is RedisSizing {\n const at = 'pricing.redis.sizedToData';\n if (sizing === null || typeof sizing !== 'object') {\n throw new ValidationError(`${at} must be a RedisSizing`);\n }\n if (typeof sizing.source !== 'string' || sizing.source === '') {\n throw new ValidationError(`${at}.source must say where the prices come from`);\n }\n if (!Array.isArray(sizing.nodeTypes) || sizing.nodeTypes.length === 0) {\n throw new ValidationError(`${at}.nodeTypes must list at least one node type`);\n }\n if (!Number.isInteger(sizing.replicasPerShard) || sizing.replicasPerShard < 0) {\n throw new ValidationError(\n `${at}.replicasPerShard must be an integer >= 0; got ${sizing.replicasPerShard}`,\n );\n }\n const reserved = requireFiniteNonNeg(\n sizing.reservedMemoryFraction,\n `${at}.reservedMemoryFraction`,\n );\n if (reserved >= 1) {\n throw new ValidationError(`${at}.reservedMemoryFraction must be below 1; got ${reserved}`);\n }\n const names = new Set<string>();\n for (const n of sizing.nodeTypes) {\n if (n === null || typeof n !== 'object' || typeof n.name !== 'string' || n.name === '') {\n throw new ValidationError(`${at}.nodeTypes: every node type needs a name`);\n }\n const row = `${at}.nodeTypes[${JSON.stringify(n.name)}]`;\n if (names.has(n.name)) throw new ValidationError(`${row} is listed twice`);\n names.add(n.name);\n if (!(requireFiniteNonNeg(n.memoryGiB, `${row}.memoryGiB`) > 0)) {\n throw new ValidationError(`${row}.memoryGiB must be above 0`);\n }\n requireFiniteNonNeg(n.ssdGiB ?? 0, `${row}.ssdGiB`);\n requireFiniteNonNeg(n.hourlyUSD, `${row}.hourlyUSD`);\n if (n.maxShards !== undefined && (!Number.isInteger(n.maxShards) || n.maxShards < 1)) {\n throw new ValidationError(`${row}.maxShards must be an integer >= 1; got ${n.maxShards}`);\n }\n // A memory so small that the reserve leaves nothing would size any data at infinite shards.\n if (!(usableGiB(sizing, n) > 0)) {\n throw new ValidationError(\n `${row} leaves no memory for data once ${reserved} of it is reserved`,\n );\n }\n }\n}\n\n/**\n * The cheapest cluster in `sizing` that holds `dataGiB`: for each node type, enough shards for the data in what each\n * node leaves free, every shard a primary and its replicas. See {@link RedisSizing} for how a tie is broken.\n */\nfunction cheapestCluster(sizing: RedisSizing, dataGiB: number): SizedCluster {\n const fits: SizedCluster[] = [];\n for (const nodeType of sizing.nodeTypes) {\n const usable = usableGiB(sizing, nodeType);\n let shards = Math.max(1, Math.ceil(dataGiB / usable));\n // The division can round an exact multiple up by one: a shard fewer that still holds the data is the answer.\n if (shards > 1 && (shards - 1) * usable >= dataGiB) shards -= 1;\n if (nodeType.maxShards !== undefined && shards > nodeType.maxShards) continue;\n const nodes = shards * (1 + sizing.replicasPerShard);\n fits.push({\n nodeType,\n shards,\n nodes,\n monthlyUSD: nodes * nodeType.hourlyUSD * HOURS_PER_MONTH,\n });\n }\n const size = bytesWords(dataGiB * GIB);\n if (fits.length === 0) {\n throw new ValidationError(\n `no node type in pricing.redis.sizedToData.nodeTypes holds ${size} within its maxShards`,\n );\n }\n // Prices within TIE_USD of the cheapest are one price reached by different roundings. Chosen among them by a rule\n // that never looks at where a row sits in the list: fewer nodes, then the lower price, then the name.\n const cheapest = fits.reduce((m, c) => Math.min(m, c.monthlyUSD), Infinity);\n const [best] = fits\n .filter((c) => c.monthlyUSD <= cheapest + TIE_USD)\n .sort(\n (a, b) =>\n a.nodes - b.nodes ||\n a.monthlyUSD - b.monthlyUSD ||\n (a.nodeType.name < b.nodeType.name ? -1 : a.nodeType.name > b.nodeType.name ? 1 : 0),\n );\n if (best === undefined || !Number.isFinite(best.monthlyUSD)) {\n throw new ValidationError(\n `pricing.redis.sizedToData prices ${size} at no finite cost; check its node types`,\n );\n }\n return best;\n}\n\n/**\n * A plain copy of a caller's sizing, each value read once, so what is validated is what is priced: a getter or a\n * Proxy that answered differently the second time could otherwise price a row it never validated. Anything not\n * shaped like a sizing is passed through for {@link validateSizing} to refuse.\n */\nfunction snapshotSizing(sizing: RedisSizing | undefined): RedisSizing | undefined {\n if (sizing === null || typeof sizing !== 'object') return sizing;\n const { source, nodeTypes, replicasPerShard, reservedMemoryFraction } = sizing;\n const row = (n: RedisNodeType): RedisNodeType => {\n if (n === null || typeof n !== 'object') return n;\n const { name, memoryGiB, ssdGiB, hourlyUSD, maxShards } = n;\n return {\n name,\n memoryGiB,\n hourlyUSD,\n ...(ssdGiB === undefined ? {} : { ssdGiB }),\n ...(maxShards === undefined ? {} : { maxShards }),\n };\n };\n return {\n source,\n replicasPerShard,\n reservedMemoryFraction,\n nodeTypes: Array.isArray(nodeTypes) ? Array.from(nodeTypes, row) : nodeTypes,\n };\n}\n\n/** What a report compares against: a fixed price, or the cluster sized to its bytes, and the sizing that chose it. */\ninterface Baseline {\n readonly monthlyUSD: number;\n readonly sized?: { readonly sizing: RedisSizing; readonly cluster: SizedCluster };\n}\n\n/** Read `pricing.redis`, which must be exactly one of the two shapes, and price it for `storedGiB`. */\nfunction resolveBaseline(redis: PricingProfile['redis'], storedGiB: number): Baseline {\n if (redis === null || typeof redis !== 'object') {\n throw new ValidationError('pricing.redis must be { monthlyUSD } or { sizedToData }');\n }\n // A shape is given when its key has a value, however the value is reached: an inherited price, or one behind a\n // getter, is as given as an own property, and a key set to undefined gives nothing. Each is read once.\n const { monthlyUSD, sizedToData } = redis;\n const fixed = monthlyUSD !== undefined;\n const sized = sizedToData !== undefined;\n if (fixed === sized) {\n throw new ValidationError(\n 'pricing.redis must be exactly one of { monthlyUSD } and { sizedToData }' +\n (fixed\n ? ', not both: spreading AWS_US_EAST_1_ONDEMAND.redis keeps its sizedToData, so pass redis: { monthlyUSD } alone'\n : ''),\n );\n }\n if (sized) {\n const sizing = snapshotSizing(sizedToData);\n validateSizing(sizing);\n const cluster = cheapestCluster(sizing, storedGiB);\n return { monthlyUSD: cluster.monthlyUSD, sized: { sizing, cluster } };\n }\n return { monthlyUSD: requireFiniteNonNeg(monthlyUSD, 'pricing.redis.monthlyUSD') };\n}\n\n/**\n * A byte count in the largest binary unit it fills \u2014 \"18.63 GiB\", \"190.73 MiB\", \"20 bytes\" \u2014 since a report on one\n * segment is usually far under a GiB, and \"0.00 GiB\" would read as nothing at all.\n */\nfunction bytesWords(bytes: number): string {\n const units = ['bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'];\n let value = bytes;\n let unit = 0;\n // Rounded before the unit is chosen, so 1,048,575 bytes reads \"1.00 MiB\", not \"1024.00 KiB\".\n const shown = (v: number, u: number): number => (u === 0 ? Math.round(v) : Number(v.toFixed(2)));\n while (unit < units.length - 1 && shown(value, unit) >= 1024) {\n value /= 1024;\n unit += 1;\n }\n if (unit > 0) return `${value.toFixed(2)} ${units[unit]}`;\n const n = Math.round(value);\n return `${n} byte${n === 1 ? '' : 's'}`;\n}\n\n/** \"1 shard of 3 cache.r6g.xlarge nodes\", or \"95 shards, 285 cache.r6g.xlarge nodes\". */\nfunction clusterWords(c: SizedCluster): string {\n const count = (n: number): string => n.toLocaleString('en-US');\n const nodes = `${count(c.nodes)} ${c.nodeType.name} node${c.nodes === 1 ? '' : 's'}`;\n return c.shards === 1 ? `1 shard of ${nodes}` : `${count(c.shards)} shards, ${nodes}`;\n}\n\n/** Rough bytes for a segment when only cardinality is known: 2 B/value (array-container upper bound). */\nfunction sizingBytes(spec: SegmentSizing): number {\n if (spec.sizeBytes !== undefined) return requireFiniteNonNeg(spec.sizeBytes, 'segment.sizeBytes');\n if (spec.cardinality !== undefined) {\n return requireFiniteNonNeg(spec.cardinality, 'segment.cardinality') * 2;\n }\n return 0;\n}\n\n/** Core report builder shared by planning + grounded modes. `storageBytes` is total across all segments. */\nfunction buildReport(input: {\n readonly storageBytes: number;\n readonly workload: Workload;\n readonly pricing: PricingProfile;\n readonly grounded: boolean;\n readonly extraNotes?: readonly string[];\n /** Some sizes were derived from cardinality, an upper bound: a Redis sized to them may be larger than needed. */\n readonly sizesFromCardinality?: boolean;\n}): CostReport {\n const { pricing, grounded } = input;\n const { storage, redis } = pricing;\n const S = SECONDS_PER_MONTH;\n\n // The pricing profile is a public, caller-supplied boundary input too \u2014 validate every rate that feeds\n // the report so a malformed profile fails fast rather than leaking NaN/Infinity dollars + a bogus verdict.\n requireFiniteNonNeg(storage.getPerMillion, 'pricing.storage.getPerMillion');\n requireFiniteNonNeg(storage.putPerMillion, 'pricing.storage.putPerMillion');\n requireFiniteNonNeg(storage.storagePerGiBMonth, 'pricing.storage.storagePerGiBMonth');\n const pointerRead = requireFiniteNonNeg(\n storage.requestsPerPointerRead ?? 1,\n 'pricing.storage.requestsPerPointerRead',\n );\n const sizedRead = requireFiniteNonNeg(\n storage.requestsPerSizedRead ?? 1,\n 'pricing.storage.requestsPerSizedRead',\n );\n\n const storageBytes = requireFiniteNonNeg(input.storageBytes, 'storageBytes');\n const storedGiB = storageBytes / GIB;\n\n // The Redis to compare against: the cheapest cluster that holds the stored bytes, or the one cluster named.\n const baseline = resolveBaseline(redis, storedGiB);\n const baselineUSD = baseline.monthlyUSD;\n\n // A fraction: 95 meant as 95% would otherwise read as 1, every read a hit.\n if ((input.workload.cacheHitRate ?? 0) > 1) {\n throw new ValidationError(\n `cacheHitRate must be a fraction from 0 to 1; got ${String(input.workload.cacheHitRate)}`,\n );\n }\n const cacheHitRate = clamp01(\n requireFiniteNonNeg(input.workload.cacheHitRate ?? 0, 'cacheHitRate'),\n );\n const readsPerSec = requireFiniteNonNeg(input.workload.readsPerSec ?? 0, 'readsPerSec');\n const intersectsPerSec = requireFiniteNonNeg(\n input.workload.intersectsPerSec ?? 0,\n 'intersectsPerSec',\n );\n const chunksPerIntersect = requireFiniteNonNeg(\n input.workload.chunksPerIntersect ?? 1,\n 'chunksPerIntersect',\n );\n const loadsPerMonth = requireFiniteNonNeg(input.workload.loadsPerMonth ?? 0, 'loadsPerMonth');\n const requestsPerLoad = requireFiniteNonNeg(\n input.workload.requestsPerLoad ?? 1,\n 'requestsPerLoad',\n );\n const operandsPerIntersect = requireFiniteNonNeg(\n input.workload.operandsPerIntersect ?? 2,\n 'operandsPerIntersect',\n );\n if (operandsPerIntersect < 1) {\n throw new ValidationError(\n `operandsPerIntersect must be at least 1, since an intersection reads its operands; got ${operandsPerIntersect}`,\n );\n }\n const hotSegments = requireFiniteNonNeg(input.workload.hotSegments ?? 0, 'hotSegments');\n const readerProcesses = requireFiniteNonNeg(\n input.workload.readerProcesses ?? 1,\n 'readerProcesses',\n );\n if (readerProcesses < 1) {\n throw new ValidationError(`readerProcesses must be at least 1; got ${readerProcesses}`);\n }\n const genTtlMs = requireFiniteNonNeg(\n input.workload.genTtlMs ?? DEFAULT_CURRENT_GEN_TTL_MS,\n 'genTtlMs',\n );\n const retirementsPerMonth = requireFiniteNonNeg(\n input.workload.retirementsPerMonth ?? 0,\n 'retirementsPerMonth',\n );\n const purgesPerMonth = requireFiniteNonNeg(input.workload.purgesPerMonth ?? 0, 'purgesPerMonth');\n const conditionalDelete = input.workload.conditionalDelete ?? true;\n if (typeof conditionalDelete !== 'boolean') {\n throw new ValidationError(`conditionalDelete must be a boolean; got ${conditionalDelete}`);\n }\n\n // Per-request unit costs (USD). Same-region egress is free; internet egress not modeled.\n const storageGetUSD = storage.getPerMillion / 1e6;\n const putUSD = storage.putPerMillion / 1e6;\n\n // Monthly volumes.\n const missFraction = 1 - cacheHitRate;\n const readMisses = readsPerSec * S * missFraction;\n const intersects = intersectsPerSec * S;\n\n // A reader re-reads a pointer only when it reads the segment after the TTL has lapsed, so the refresh is at most\n // one per hot segment per TTL, and at most one per point read. With no timed refresh (`genTtlMs: 0`) it is none.\n const refreshes =\n genTtlMs > 0\n ? Math.min((readerProcesses * hotSegments * S * 1000) / genTtlMs, readsPerSec * S)\n : 0;\n\n const storageUSD = storedGiB * storage.storagePerGiBMonth;\n const readsUSD = readMisses * storageGetUSD;\n // A cold intersection reads each operand's pointer, then its index in one tail read, before its chunks.\n const getsPerColdOperand = pointerRead + sizedRead;\n const intersectGets = chunksPerIntersect + getsPerColdOperand * operandsPerIntersect;\n const intersectsUSD = intersects * intersectGets * storageGetUSD;\n const loadGets = STORE_LOAD_POINTER_READS * pointerRead + STORE_LOAD_EXISTENCE_CHECKS;\n const loadsUSD =\n loadsPerMonth * ((requestsPerLoad + STORE_LOAD_PUT_CLASS) * putUSD + loadGets * storageGetUSD);\n const refreshUSD = refreshes * pointerRead * storageGetUSD;\n const sweep = RETENTION_SWEEP_REQUESTS[conditionalDelete ? 'conditionalDelete' : 'tombstoning'];\n const retentionUSD =\n retirementsPerMonth *\n (sweep.retirement.reads * storageGetUSD + sweep.retirement.writes * putUSD) +\n purgesPerMonth * (sweep.purge.reads * storageGetUSD + sweep.purge.writes * putUSD);\n const total = readsUSD + intersectsUSD + storageUSD + loadsUSD + refreshUSD + retentionUSD;\n\n // Crossover: the sustained read rate (other axes 0) where request cost alone passes the baseline less the fixed\n // monthly costs (storage and the pointer refresh), evaluated at this report's cache posture (misses).\n const headroom = Math.max(0, baselineUSD - storageUSD - refreshUSD);\n const readsCross =\n storageGetUSD > 0 && missFraction > 0\n ? headroom / (S * storageGetUSD * missFraction)\n : Infinity;\n\n const verdict: CostReport['verdict'] =\n total <= baselineUSD * 0.1 ? 'win-big' : total < baselineUSD ? 'win' : 'lose-zone';\n\n const dominant = Math.max(\n readsUSD,\n intersectsUSD,\n storageUSD,\n loadsUSD,\n refreshUSD,\n retentionUSD,\n );\n let driver = 'storage';\n if (dominant === readsUSD && readsUSD > 0) driver = 'point reads (object GETs)';\n else if (dominant === intersectsUSD && intersectsUSD > 0) {\n driver = 'intersections (pointer, index and chunk GETs)';\n } else if (dominant === loadsUSD && loadsUSD > 0)\n driver = 'loads (object, listing and pointer requests)';\n else if (dominant === refreshUSD && refreshUSD > 0) driver = 'the pointer refresh (object GETs)';\n else if (dominant === retentionUSD && retentionUSD > 0)\n driver = 'the retention sweep (registry reads and writes)';\n // A fixed baseline keeps the words it always had \u2014 \"flat baseline\" in a lose-zone, \"baseline\" otherwise \u2014 and a\n // sized one says what it priced.\n const sized = baseline.sized;\n // No bytes counted \u2014 nothing stored, or a source that cannot measure \u2014 sizes Redis to nothing: the rationale says\n // so, rather than naming a cluster that \"would hold 0 bytes\" as if that were a measurement.\n const noBytes = Math.round(storageBytes) === 0;\n const redisWords =\n sized === undefined\n ? undefined\n : noBytes\n ? `$${baselineUSD.toFixed(2)}/mo cheapest Redis cluster in the catalogue, as no bytes are counted ` +\n `(${clusterWords(sized.cluster)})`\n : `$${baselineUSD.toFixed(2)}/mo Redis that would hold ${bytesWords(storageBytes)} ` +\n `(${clusterWords(sized.cluster)})`;\n const rationale =\n verdict === 'lose-zone'\n ? `pay-per-use total $${total.toFixed(2)}/mo exceeds the ${redisWords ?? `$${baselineUSD}/mo flat baseline`}; dominated by ${driver}`\n : verdict === 'win-big'\n ? `$${total.toFixed(2)}/mo \u2014 \u226410% of the ${redisWords ?? `$${baselineUSD}/mo baseline`}; dominated by ${driver}`\n : `$${total.toFixed(2)}/mo, under the ${redisWords ?? `$${baselineUSD}/mo baseline`}; dominated by ${driver}`;\n\n const dataTiering = sized !== undefined && (sized.cluster.nodeType.ssdGiB ?? 0) > 0;\n const redisNote =\n sized === undefined\n ? `Redis priced at the fixed $${baselineUSD}/mo given, whatever the data size.`\n : (noBytes\n ? 'Redis priced at the cheapest cluster in the catalogue, as no bytes are counted, because nothing ' +\n 'is stored or nothing was measured'\n : `Redis priced at the cheapest cluster in the catalogue that holds the ${bytesWords(storageBytes)} stored`) +\n `: ${clusterWords(sized.cluster)}, each shard a primary and ${sized.sizing.replicasPerShard} ` +\n `replica(s), ${Number((sized.sizing.reservedMemoryFraction * 100).toFixed(1))}% of memory reserved ` +\n `(${sized.sizing.source}).` +\n (noBytes\n ? ' Pass pricing.redis: { monthlyUSD } to compare against a Redis of your own.'\n : ' Compressed bytes are a floor on the memory Redis needs: a native Redis bitmap is sized ' +\n 'by its highest id, so sparse ids need more.') +\n ' Redis bought another way \u2014 fewer replicas, another engine, reserved nodes \u2014 can cost less than the ' +\n 'catalogue prices it.' +\n (dataTiering\n ? ` ${sized.cluster.nodeType.name} is a data-tiering node, which keeps the values read least recently ` +\n 'on its SSD. It is priced as if the data read often fits in its memory: AWS recommends data tiering ' +\n 'for workloads that regularly read up to 20% of their data.'\n : '') +\n (input.sizesFromCardinality\n ? ' Some segment sizes came from cardinality, at 2 bytes an id, which is more than a dense bitmap ' +\n 'needs, so this Redis may be larger than the data calls for: pass sizeBytes to size it to the ' +\n 'compressed bytes.'\n : '');\n\n const notes = [\n 'Same-region egress treated as free; internet egress is not modeled.',\n 'Request cost is from the supplied workload rates (live-metrics-derived request cost is a later phase).',\n loadsPerMonth > 0\n ? `Loads modeled: ${loadsPerMonth}/mo, each ${requestsPerLoad} PUT-class request(s) for the object plus ` +\n `${shown(STORE_LOAD_PUT_CLASS)} PUT-class and ${shown(loadGets)} GETs that store.load() adds (the pointer, ` +\n 'a check that the next generation number is free, a check that the current generation is there, and a listing every ' +\n `${LIST_COLLECTION_CADENCE}th load).`\n : 'Loads are NOT modeled \u2014 set workload.loadsPerMonth (+ requestsPerLoad for multipart) to include them.',\n ...(intersects > 0\n ? [\n `Intersections priced cold: ${intersectGets} GETs each, ` +\n `${getsPerColdOperand} for each of ${operandsPerIntersect} operand(s) plus ` +\n `${chunksPerIntersect} chunk range request(s); cacheHitRate does not apply.`,\n ]\n : []),\n hotSegments > 0\n ? genTtlMs > 0\n ? `Pointer refresh modeled: ${hotSegments} hot segment(s) in each of ${readerProcesses} reader ` +\n `process(es), each re-reading its pointer at most every ${genTtlMs} ms, and at most once a point read.`\n : 'Pointer refresh: none \u2014 genTtlMs 0 turns the timed refresh off. A reader still re-reads a pointer on an ' +\n 'eviction, a swept generation or an invalidation, which this does not price.'\n : readsPerSec > 0\n ? 'The pointer refresh is NOT modeled \u2014 set workload.hotSegments to the segments each long-lived reader ' +\n 'keeps reading, and workload.readerProcesses to how many readers there are.'\n : null,\n hotSegments > DEFAULT_MAX_OPEN_SEGMENTS\n ? `More hot segments in a reader than a store keeps open by default (${DEFAULT_MAX_OPEN_SEGMENTS}): a read of a segment ` +\n (genTtlMs > 0\n ? 'the reader evicted opens it again when the read needs its index or an uncached chunk, a tail read, and ' +\n 'reads its pointer too once the store has let its resolution go as well, which this does not price. '\n : 'the reader evicted opens it again, a pointer and a tail read, which this does not price. ') +\n 'Raise cache.readerMax, and cache.readerMaxBytes, to keep them open.'\n : null,\n ...(retirementsPerMonth > 0 || purgesPerMonth > 0\n ? [\n `Retention sweep modeled: ${retirementsPerMonth}/mo retirements at ${sweep.retirement.reads} reads, ` +\n `${sweep.retirement.writes} writes and ${sweep.retirement.deletes} delete(s) each, ${purgesPerMonth}/mo ` +\n `purges at ${sweep.purge.reads} reads, ${sweep.purge.writes} writes and ${sweep.purge.deletes} ` +\n 'delete(s) each; reads are priced at the GET rate and writes at the PUT rate, deletes at nothing, as S3 ' +\n 'bills none' +\n (conditionalDelete\n ? '.'\n : '; a registry that only tombstones is read again, two reads per purged segment, by every later ' +\n 'full sweep, which is not priced.'),\n ]\n : []),\n ...(input.extraNotes ?? []),\n // Last, so the notes a report already carried keep their places.\n redisNote,\n ].filter((n): n is string => n !== null);\n\n return {\n monthlyUSD: {\n byOp: {\n reads: readsUSD,\n intersects: intersectsUSD,\n storage: storageUSD,\n loads: loadsUSD,\n pointerRefresh: refreshUSD,\n retention: retentionUSD,\n },\n total,\n },\n redisBaseline:\n sized === undefined\n ? { basis: 'fixed', monthlyUSD: baselineUSD }\n : {\n basis: 'sized-to-data',\n monthlyUSD: baselineUSD,\n cluster: {\n nodeType: sized.cluster.nodeType.name,\n shards: sized.cluster.shards,\n nodes: sized.cluster.nodes,\n dataTiering,\n },\n },\n redisCrossover: { readsPerSec: readsCross },\n verdict,\n rationale,\n assumptions: { cacheHitRate, pricingName: pricing.name, grounded, notes },\n };\n}\n\n/**\n * Refuses a `pricing` that is not a profile with a storage price list, and a `workload` that is not an object (`null`\n * reads as none), as a `ValidationError` naming the call rather than a raw `TypeError` from inside the model.\n */\nfunction checkModelInputs(op: string, pricing: unknown, workload: unknown): void {\n const storage: unknown =\n pricing !== null && typeof pricing === 'object'\n ? (pricing as PricingProfile).storage\n : undefined;\n if (pricing !== undefined && (storage === null || typeof storage !== 'object')) {\n throw new ValidationError(`${op}: pricing must be a profile with a storage price list`);\n }\n if (\n workload !== undefined &&\n workload !== null &&\n (typeof workload !== 'object' || Array.isArray(workload))\n ) {\n throw new ValidationError(`${op}: workload must be an object`);\n }\n}\n\n/**\n * **Planning** cost estimate \u2014 pure, no instance or live data needed (sizing, sales, what-if). Segment sizes\n * are taken as given (or roughly derived from cardinality); use {@link groundedReport} with a segment's measured\n * size for exact, real sizes. See {@link CostReport}.\n */\nexport function estimateCost(input: EstimateInput): CostReport {\n // Shapes a plain-JavaScript caller can get wrong, refused as such rather than as a raw TypeError from deep inside.\n if (input === null || typeof input !== 'object') {\n throw new ValidationError('estimateCost: input must be an object such as { segments }');\n }\n if (!Array.isArray(input.segments)) {\n throw new ValidationError(\n 'estimateCost: segments must be an array of { sizeBytes } or { cardinality }',\n );\n }\n // An index loop, so a hole in a sparse array is refused as an entry too.\n for (let i = 0; i < input.segments.length; i++) {\n const spec: unknown = input.segments[i];\n if (spec === null || typeof spec !== 'object') {\n throw new ValidationError(`estimateCost: segments[${i}] must be an object`);\n }\n }\n checkModelInputs('estimateCost', input.pricing, input.workload);\n const pricing = input.pricing ?? AWS_US_EAST_1_ONDEMAND;\n const workload = input.workload ?? {};\n let storageBytes = 0;\n let sizesFromCardinality = false;\n for (const spec of input.segments) {\n const count = Math.floor(requireFiniteNonNeg(spec.count ?? 1, 'segment.count'));\n storageBytes += sizingBytes(spec) * count;\n if (\n spec.sizeBytes === undefined &&\n spec.cardinality !== undefined &&\n sizingBytes(spec) * count > 0\n ) {\n sizesFromCardinality = true;\n }\n }\n return buildReport({ storageBytes, workload, pricing, grounded: false, sizesFromCardinality });\n}\n\n/** The keys {@link groundedReport} takes. */\nconst GROUNDED_KEYS: ReadonlySet<string> = new Set(['storageBytes', 'workload', 'pricing']);\n\n/**\n * **Grounded** report from a measured byte total + a supplied workload. A segment handle's `stat()` reports the\n * size of its current generation as `size`, read from the `.crbm` footer and index with no payload reads:\n *\n * ```ts\n * const report = groundedReport({ storageBytes: (await seg.stat()).sizeBytes, workload: { readsPerSec: 50 } });\n * ```\n *\n * Sum the sizes of several segments to price them together; see {@link CostReport.redisBaseline} for why one report\n * of the sum is not the sum of the reports. `storageBytes: null`, which `stat()` answers for a segment with no\n * generation and on a store whose source cannot report a size, prices storage at $0 and says so in the notes, with\n * `assumptions.grounded` false: nothing was measured, so the report does not claim a confident $0.\n */\nexport function groundedReport(input: {\n readonly storageBytes: number | null;\n readonly workload?: Workload;\n readonly pricing?: PricingProfile;\n}): CostReport {\n if (input === null || typeof input !== 'object') {\n throw new ValidationError('groundedReport: input must be an object such as { storageBytes }');\n }\n // A misspelt key would drop what it names and still return a confident report, so every key is checked.\n for (const key of Object.keys(input)) {\n if (!GROUNDED_KEYS.has(key)) {\n throw new ValidationError(\n `groundedReport: unknown option \"${key}\"; it takes { storageBytes, workload?, pricing? } (pass a stat()'s ` +\n '`size` as storageBytes)',\n );\n }\n }\n const bytes: unknown = input.storageBytes;\n if (bytes !== null && (typeof bytes !== 'number' || !Number.isFinite(bytes) || bytes < 0)) {\n throw new ValidationError(\n 'groundedReport: storageBytes must be a byte count (a finite number >= 0) or null when nothing was measured; ' +\n `got ${typeof bytes === 'string' ? `${JSON.stringify(bytes)} (a string)` : String(bytes)}`,\n );\n }\n checkModelInputs('groundedReport', input.pricing, input.workload);\n const measured = input.storageBytes !== null;\n return buildReport({\n storageBytes: measured ? input.storageBytes : 0,\n workload: input.workload ?? {},\n pricing: input.pricing ?? AWS_US_EAST_1_ONDEMAND,\n grounded: measured,\n extraNotes: measured\n ? undefined\n : [\n 'storageBytes is null \u2014 no size was measured (a segment with no generation, or a store whose source ' +\n 'cannot report one), so storage is reported as $0.',\n ],\n });\n}\n", "/**\n * The figures of a store the cost model prices what they cause. Core keeps them internal, so they are copied here\n * rather than imported: this package reaches `@cloudbitmaps/core` through its public entry alone.\n * `tests/tools/store-defaults.test.ts` holds each equal to core's own, so a change there fails until this copy moves\n * with it.\n */\n\n/** A load that could collect by name lists the segment instead on every generation divisible by this. */\nexport const LIST_COLLECTION_CADENCE = 16;\n\n/** How long, in ms, a reader trusts a segment's pointer before it reads it again: the store's `cache.genTtlMs`. */\nexport const DEFAULT_CURRENT_GEN_TTL_MS = 2000;\n\n/** How many segments a store keeps open by default: the store's `cache.readerMax`. */\nexport const DEFAULT_MAX_OPEN_SEGMENTS = 1024;\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["shown"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The figures of a store the cost model prices what they cause. Core keeps them internal, so they are copied here
|
|
3
|
+
* rather than imported: this package reaches `@cloudbitmaps/core` through its public entry alone.
|
|
4
|
+
* `tests/tools/store-defaults.test.ts` holds each equal to core's own, so a change there fails until this copy moves
|
|
5
|
+
* with it.
|
|
6
|
+
*/
|
|
7
|
+
/** A load that could collect by name lists the segment instead on every generation divisible by this. */
|
|
8
|
+
export declare const LIST_COLLECTION_CADENCE = 16;
|
|
9
|
+
/** How long, in ms, a reader trusts a segment's pointer before it reads it again: the store's `cache.genTtlMs`. */
|
|
10
|
+
export declare const DEFAULT_CURRENT_GEN_TTL_MS = 2000;
|
|
11
|
+
/** How many segments a store keeps open by default: the store's `cache.readerMax`. */
|
|
12
|
+
export declare const DEFAULT_MAX_OPEN_SEGMENTS = 1024;
|