@soulcraft/cor 3.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +16 -0
- package/README.md +328 -0
- package/dist/aggregation/NativeAggregationEngine.d.ts +118 -0
- package/dist/aggregation/NativeAggregationEngine.js +186 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.js +236 -0
- package/dist/graph/GraphAccelerationAdapter.d.ts +153 -0
- package/dist/graph/GraphAccelerationAdapter.js +326 -0
- package/dist/graph/NativeGraphAdjacencyIndex.d.ts +286 -0
- package/dist/graph/NativeGraphAdjacencyIndex.js +944 -0
- package/dist/hnsw/AdaptiveDiskAnnModeSelector.d.ts +187 -0
- package/dist/hnsw/AdaptiveDiskAnnModeSelector.js +313 -0
- package/dist/hnsw/NativeDiskAnnWrapper.d.ts +489 -0
- package/dist/hnsw/NativeDiskAnnWrapper.js +1456 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +29 -0
- package/dist/legacyLayoutGuard.d.ts +93 -0
- package/dist/legacyLayoutGuard.js +237 -0
- package/dist/license/constants.d.ts +30 -0
- package/dist/license/constants.js +30 -0
- package/dist/license/onlineValidator.d.ts +43 -0
- package/dist/license/onlineValidator.js +225 -0
- package/dist/license/types.d.ts +39 -0
- package/dist/license/types.js +5 -0
- package/dist/license.d.ts +64 -0
- package/dist/license.js +165 -0
- package/dist/migration/MigrationCoordinator.d.ts +171 -0
- package/dist/migration/MigrationCoordinator.js +249 -0
- package/dist/migration/indexEpochGuard.d.ts +163 -0
- package/dist/migration/indexEpochGuard.js +202 -0
- package/dist/native/NativeEmbeddingEngine.d.ts +79 -0
- package/dist/native/NativeEmbeddingEngine.js +318 -0
- package/dist/native/NativeRoaringBitmap32.d.ts +114 -0
- package/dist/native/NativeRoaringBitmap32.js +221 -0
- package/dist/native/ffi.d.ts +20 -0
- package/dist/native/ffi.js +48 -0
- package/dist/native/idMapperTestSupport.d.ts +67 -0
- package/dist/native/idMapperTestSupport.js +112 -0
- package/dist/native/index.d.ts +49 -0
- package/dist/native/index.js +87 -0
- package/dist/native/napi.d.ts +21 -0
- package/dist/native/napi.js +88 -0
- package/dist/native/types.d.ts +1298 -0
- package/dist/native/types.js +16 -0
- package/dist/plugin.d.ts +50 -0
- package/dist/plugin.js +388 -0
- package/dist/providerContracts.d.ts +277 -0
- package/dist/providerContracts.js +38 -0
- package/dist/resource/OsMemoryProbe.d.ts +175 -0
- package/dist/resource/OsMemoryProbe.js +206 -0
- package/dist/resource/ResourceManager.d.ts +491 -0
- package/dist/resource/ResourceManager.js +960 -0
- package/dist/utils/ColumnManifest.d.ts +97 -0
- package/dist/utils/ColumnManifest.js +129 -0
- package/dist/utils/NativeColumnStore.d.ts +284 -0
- package/dist/utils/NativeColumnStore.js +685 -0
- package/dist/utils/NativeMetadataIndex.d.ts +882 -0
- package/dist/utils/NativeMetadataIndex.js +2631 -0
- package/dist/utils/NativeUnifiedCache.d.ts +87 -0
- package/dist/utils/NativeUnifiedCache.js +273 -0
- package/dist/utils/binaryIdMapperFactory.d.ts +59 -0
- package/dist/utils/binaryIdMapperFactory.js +94 -0
- package/dist/utils/collation.d.ts +30 -0
- package/dist/utils/collation.js +40 -0
- package/dist/utils/columnStoreTypes.d.ts +161 -0
- package/dist/utils/columnStoreTypes.js +29 -0
- package/dist/utils/nativeBinaryEntityIdMapper.d.ts +211 -0
- package/dist/utils/nativeBinaryEntityIdMapper.js +381 -0
- package/dist/utils/nativeEntityIdMapper.d.ts +111 -0
- package/dist/utils/nativeEntityIdMapper.js +170 -0
- package/docs/ADR-002-diskann-100-percent-rust.md +294 -0
- package/docs/ADR-003-semantic-time-travel.md +100 -0
- package/docs/aggregation.md +251 -0
- package/docs/comparison.md +162 -0
- package/docs/deployment-limits.md +87 -0
- package/docs/diskann.md +184 -0
- package/docs/migration-3.0.md +396 -0
- package/docs/performance-budget.md +274 -0
- package/docs/performance.md +117 -0
- package/docs/scaling.md +439 -0
- package/docs/snapshot-safety.md +246 -0
- package/docs/u64-id-space.md +214 -0
- package/docs/verification-report.md +670 -0
- package/native/brainy-native.node +0 -0
- package/native/index.d.ts +3916 -0
- package/package.json +92 -0
- package/scripts/migrate-cortex-2x-to-3x.mjs +970 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module resource/OsMemoryProbe
|
|
3
|
+
* @description OS-level memory observer for the cor 3.0 **Adaptive
|
|
4
|
+
* DiskANN** mode selector (Piece J of the cor 3.0 plan).
|
|
5
|
+
*
|
|
6
|
+
* The {@link ResourceManager} already observes the V8 heap and a
|
|
7
|
+
* coarse system-RSS pressure ratio (Piece 8). What it didn't have
|
|
8
|
+
* was a read on the kernel's `MemAvailable` figure — the metric the
|
|
9
|
+
* Linux kernel computes as "how much physical RAM can a new
|
|
10
|
+
* allocation claim without swapping," factoring in reclaimable page
|
|
11
|
+
* cache and slab. That's the right signal for the adaptive selector
|
|
12
|
+
* to pick a DiskANN open mode in multi-tenant scenarios where a
|
|
13
|
+
* shared box runs many brainy+cor instances: each instance's
|
|
14
|
+
* `os.freemem()` undercounts available memory (it reports MemFree,
|
|
15
|
+
* not MemAvailable), so a naïve `freemem`-based check would push
|
|
16
|
+
* the selector toward OnDisk hints even when there's plenty of
|
|
17
|
+
* reclaimable cache available.
|
|
18
|
+
*
|
|
19
|
+
* ## Surface
|
|
20
|
+
*
|
|
21
|
+
* - {@link parseMeminfo} — pure parser; fixture-friendly.
|
|
22
|
+
* - {@link OsMemoryProbe} — readers + caching wrapper. Dependency-
|
|
23
|
+
* injected so tests can simulate Linux on macOS and exercise the
|
|
24
|
+
* `MemAvailable` missing branch without touching the real
|
|
25
|
+
* `/proc/meminfo`.
|
|
26
|
+
*
|
|
27
|
+
* ## Why a cache
|
|
28
|
+
*
|
|
29
|
+
* `/proc/meminfo` reads cost ~10 μs of syscall time — not free, but
|
|
30
|
+
* also not worth re-reading on every single call. The 250 ms cache
|
|
31
|
+
* is comfortably below the 5 s pressure-tick cadence and the 30 s
|
|
32
|
+
* rebalance cadence the ResourceManager already runs, so any
|
|
33
|
+
* meaningful pressure event lands on a fresh read.
|
|
34
|
+
*
|
|
35
|
+
* ## Non-Linux fallback
|
|
36
|
+
*
|
|
37
|
+
* On macOS and Windows there is no `/proc/meminfo`. The probe falls
|
|
38
|
+
* back to `os.totalmem()` + `os.freemem()` and marks the snapshot
|
|
39
|
+
* `source: 'os-fallback'` so callers can degrade gracefully (the
|
|
40
|
+
* adaptive selector currently treats fallback snapshots as
|
|
41
|
+
* "available bytes is conservative, prefer Hybrid over OnDisk").
|
|
42
|
+
*/
|
|
43
|
+
import os from 'node:os';
|
|
44
|
+
import { readFileSync } from 'node:fs';
|
|
45
|
+
const KIB = 1024;
|
|
46
|
+
/**
|
|
47
|
+
* Match a `Field: value [kB]` line in `/proc/meminfo`.
|
|
48
|
+
*
|
|
49
|
+
* Allowed key chars cover the punctuation that real kernel-side
|
|
50
|
+
* names use: `Active(anon)`, `Inactive(file)`, etc. We don't read
|
|
51
|
+
* those fields ourselves, but the regex tolerates them so a typo in
|
|
52
|
+
* a future key extension doesn't silently fall through.
|
|
53
|
+
*/
|
|
54
|
+
const FIELD_PATTERN = /^([A-Za-z()_]+):\s+(\d+)(?:\s+kB)?\s*$/;
|
|
55
|
+
/**
|
|
56
|
+
* Parse a `/proc/meminfo` text blob and extract the three fields the
|
|
57
|
+
* probe needs. Pure function — no I/O, fully deterministic, the
|
|
58
|
+
* only thing fixture tests need to cover.
|
|
59
|
+
*
|
|
60
|
+
* Returns `null` only when `MemTotal` is missing, which would mean
|
|
61
|
+
* the input isn't `/proc/meminfo` at all (every Linux kernel since
|
|
62
|
+
* the file existed has emitted it). Missing `MemAvailable` or
|
|
63
|
+
* `MemFree` is normal on old or specialised kernels and is left for
|
|
64
|
+
* the snapshot composer to handle.
|
|
65
|
+
*/
|
|
66
|
+
export function parseMeminfo(text) {
|
|
67
|
+
const fields = {};
|
|
68
|
+
for (const rawLine of text.split('\n')) {
|
|
69
|
+
const line = rawLine.trimEnd();
|
|
70
|
+
if (line.length === 0)
|
|
71
|
+
continue;
|
|
72
|
+
const match = line.match(FIELD_PATTERN);
|
|
73
|
+
if (!match)
|
|
74
|
+
continue;
|
|
75
|
+
const key = match[1];
|
|
76
|
+
const value = parseInt(match[2], 10);
|
|
77
|
+
if (!Number.isFinite(value) || value < 0)
|
|
78
|
+
continue;
|
|
79
|
+
if (key === 'MemTotal')
|
|
80
|
+
fields.totalKib = value;
|
|
81
|
+
else if (key === 'MemAvailable')
|
|
82
|
+
fields.availableKib = value;
|
|
83
|
+
else if (key === 'MemFree')
|
|
84
|
+
fields.freeKib = value;
|
|
85
|
+
}
|
|
86
|
+
if (fields.totalKib === undefined)
|
|
87
|
+
return null;
|
|
88
|
+
return fields;
|
|
89
|
+
}
|
|
90
|
+
function clamp01(x) {
|
|
91
|
+
if (!Number.isFinite(x))
|
|
92
|
+
return 0;
|
|
93
|
+
if (x < 0)
|
|
94
|
+
return 0;
|
|
95
|
+
if (x > 1)
|
|
96
|
+
return 1;
|
|
97
|
+
return x;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Build an {@link OsMemorySnapshot} from the parsed `/proc/meminfo`
|
|
101
|
+
* fields. Visible for tests; production callers use
|
|
102
|
+
* {@link OsMemoryProbe.snapshot}.
|
|
103
|
+
*/
|
|
104
|
+
export function snapshotFromMeminfo(fields) {
|
|
105
|
+
const totalBytes = (fields.totalKib ?? 0) * KIB;
|
|
106
|
+
const freeBytes = (fields.freeKib ?? 0) * KIB;
|
|
107
|
+
// MemAvailable landed in Linux 3.14. Older kernels need the
|
|
108
|
+
// MemFree fallback — strictly worse signal but the best we have.
|
|
109
|
+
const availableBytes = fields.availableKib !== undefined ? fields.availableKib * KIB : freeBytes;
|
|
110
|
+
const pressureScore = totalBytes > 0 ? clamp01(1 - availableBytes / totalBytes) : 0;
|
|
111
|
+
return {
|
|
112
|
+
totalBytes,
|
|
113
|
+
availableBytes,
|
|
114
|
+
freeBytes,
|
|
115
|
+
pressureScore,
|
|
116
|
+
source: 'proc-meminfo',
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Build the os-fallback snapshot from Node's `os` module. Visible
|
|
121
|
+
* for tests; production callers use {@link OsMemoryProbe.snapshot}.
|
|
122
|
+
*/
|
|
123
|
+
export function snapshotFromOs() {
|
|
124
|
+
const totalBytes = os.totalmem();
|
|
125
|
+
const freeBytes = os.freemem();
|
|
126
|
+
// No MemAvailable equivalent without a platform-specific syscall;
|
|
127
|
+
// freemem is the best approximation available cross-platform.
|
|
128
|
+
const pressureScore = totalBytes > 0 ? clamp01(1 - freeBytes / totalBytes) : 0;
|
|
129
|
+
return {
|
|
130
|
+
totalBytes,
|
|
131
|
+
availableBytes: freeBytes,
|
|
132
|
+
freeBytes,
|
|
133
|
+
pressureScore,
|
|
134
|
+
source: 'os-fallback',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Default `/proc/meminfo` reader. Returns the file's text or `null`
|
|
139
|
+
* when the file doesn't exist / is unreadable (the expected case on
|
|
140
|
+
* macOS and Windows). Synchronous because the file is virtual and
|
|
141
|
+
* reads cost microseconds.
|
|
142
|
+
*/
|
|
143
|
+
function defaultProcReader() {
|
|
144
|
+
try {
|
|
145
|
+
return readFileSync('/proc/meminfo', 'utf8');
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Cached `/proc/meminfo` observer. Wire one instance into the
|
|
153
|
+
* {@link ResourceManager}; the adaptive DiskANN selector (Piece I)
|
|
154
|
+
* will read `snapshot()` at brain-open time.
|
|
155
|
+
*/
|
|
156
|
+
export class OsMemoryProbe {
|
|
157
|
+
cacheMs;
|
|
158
|
+
procReader;
|
|
159
|
+
osFallback;
|
|
160
|
+
now;
|
|
161
|
+
cached = null;
|
|
162
|
+
constructor(options = {}) {
|
|
163
|
+
this.cacheMs = options.cacheMs ?? 250;
|
|
164
|
+
this.procReader = options.procReader ?? defaultProcReader;
|
|
165
|
+
this.osFallback = options.osFallback ?? snapshotFromOs;
|
|
166
|
+
this.now = options.now ?? Date.now;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Latest memory snapshot. Cached for {@link OsMemoryProbeOptions.cacheMs}
|
|
170
|
+
* milliseconds; calls inside the window reuse the cached value.
|
|
171
|
+
*/
|
|
172
|
+
snapshot() {
|
|
173
|
+
const now = this.now();
|
|
174
|
+
if (this.cached && this.cached.expires > now) {
|
|
175
|
+
return this.cached.snapshot;
|
|
176
|
+
}
|
|
177
|
+
const text = this.procReader();
|
|
178
|
+
let fresh;
|
|
179
|
+
if (text !== null) {
|
|
180
|
+
const fields = parseMeminfo(text);
|
|
181
|
+
if (fields !== null) {
|
|
182
|
+
fresh = snapshotFromMeminfo(fields);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
// The proc reader succeeded but the parse failed — malformed
|
|
186
|
+
// file. Degrade rather than throw so a flaky kernel doesn't
|
|
187
|
+
// crash the whole observation surface.
|
|
188
|
+
fresh = this.osFallback();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
fresh = this.osFallback();
|
|
193
|
+
}
|
|
194
|
+
this.cached = { snapshot: fresh, expires: now + this.cacheMs };
|
|
195
|
+
return fresh;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Invalidate the cache so the next {@link snapshot} call performs
|
|
199
|
+
* a fresh read. Used by tests; production callers shouldn't need
|
|
200
|
+
* this because the cache TTL is short.
|
|
201
|
+
*/
|
|
202
|
+
invalidate() {
|
|
203
|
+
this.cached = null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=OsMemoryProbe.js.map
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module resource/ResourceManager
|
|
3
|
+
* @description Adaptive resource manager for multi-instance Brainy deployments.
|
|
4
|
+
*
|
|
5
|
+
* Observes system resources (RAM, CPU, cgroups limits, active instance count)
|
|
6
|
+
* and dynamically adjusts cache sizing and memory budgets. All products
|
|
7
|
+
* (Workshop, Venue, Academy) benefit automatically — zero configuration needed.
|
|
8
|
+
*
|
|
9
|
+
* Singleton — one ResourceManager per Node.js process, shared across all
|
|
10
|
+
* Brainy instances. Registered as a Cor provider so SDK and brainy can
|
|
11
|
+
* query resource profiles without importing cor directly.
|
|
12
|
+
*
|
|
13
|
+
* ## Per-category memory budget (Piece 8 extension, cor 3.0)
|
|
14
|
+
*
|
|
15
|
+
* The cache budget is split across four named categories — `vectors`,
|
|
16
|
+
* `metadata`, `embedding`, `other` — each with a hardcoded base share of
|
|
17
|
+
* the total cache pool (50% / 25% / 15% / 10% respectively). Subsystems
|
|
18
|
+
* (cor's native LSM metadata index, DiskANN graph, embedding cache, etc.)
|
|
19
|
+
* register as `MemoryBudgetSubscriber`s with their category and a callback
|
|
20
|
+
* that fires whenever their effective allocation changes.
|
|
21
|
+
*
|
|
22
|
+
* Per the zero-config principle: the base shares are the **starting point**,
|
|
23
|
+
* not a hard cap. The ResourceManager observes actual utilization
|
|
24
|
+
* (`reportNativeMemory(name, bytes)` calls) on a 5-second pressure-tick
|
|
25
|
+
* cadence and **dynamically rebalances** allocations at runtime —
|
|
26
|
+
* under-utilized categories donate capacity to over-utilized ones,
|
|
27
|
+
* bounded so no category can shrink below 25% of its base share or grow
|
|
28
|
+
* above 200% of its base share. Operators never tune the split; the
|
|
29
|
+
* system adapts to its workload.
|
|
30
|
+
*
|
|
31
|
+
* The pre-existing `setCacheResizeCallback` API is preserved as a
|
|
32
|
+
* back-compat shim that auto-subscribes to the `other` category with a
|
|
33
|
+
* zero-bytes report — no behavior change for existing wiring.
|
|
34
|
+
*
|
|
35
|
+
* ## OS-memory observation (Piece J, cor 3.0)
|
|
36
|
+
*
|
|
37
|
+
* A built-in {@link OsMemoryProbe} reads `/proc/meminfo` on Linux
|
|
38
|
+
* (with an `os.totalmem`/`os.freemem` fallback elsewhere) and
|
|
39
|
+
* exposes the result via {@link getResourceProfile}'s `osMemory`
|
|
40
|
+
* field and the convenience accessor
|
|
41
|
+
* {@link ResourceManager.getOsMemorySnapshot}. The **Adaptive
|
|
42
|
+
* DiskANN** mode selector (Piece I) reads this snapshot at
|
|
43
|
+
* brain-open time to pick between in-memory / hybrid / on-disk
|
|
44
|
+
* operating modes — the key signal is `availableBytes`
|
|
45
|
+
* (`MemAvailable` on Linux), which counts reclaimable page cache
|
|
46
|
+
* as "available for new allocations." Naïve `os.freemem()`
|
|
47
|
+
* undercounts in multi-tenant scenarios where many brainy+cor
|
|
48
|
+
* instances share a box.
|
|
49
|
+
*/
|
|
50
|
+
import { OsMemoryProbe, type OsMemorySnapshot } from './OsMemoryProbe.js';
|
|
51
|
+
/**
|
|
52
|
+
* Snapshot of system resources and computed recommendations.
|
|
53
|
+
*/
|
|
54
|
+
export interface ResourceProfile {
|
|
55
|
+
/** Total system memory in bytes */
|
|
56
|
+
totalMemory: number;
|
|
57
|
+
/** Free system memory in bytes (at observation time) */
|
|
58
|
+
freeMemory: number;
|
|
59
|
+
/** Effective memory limit (cgroups-aware: min of system and cgroup limit) */
|
|
60
|
+
effectiveLimit: number;
|
|
61
|
+
/** Whether running in a container (cgroups detected) */
|
|
62
|
+
isContainer: boolean;
|
|
63
|
+
/** Process RSS in bytes */
|
|
64
|
+
processRss: number;
|
|
65
|
+
/** Process heap used in bytes */
|
|
66
|
+
heapUsed: number;
|
|
67
|
+
/** Number of CPU cores */
|
|
68
|
+
cpuCount: number;
|
|
69
|
+
/** Number of active brainy instances tracked */
|
|
70
|
+
activeInstances: number;
|
|
71
|
+
/** Total entities across all active instances */
|
|
72
|
+
totalEntities: number;
|
|
73
|
+
/** Recommended cache budget in bytes */
|
|
74
|
+
recommendedCacheSize: number;
|
|
75
|
+
/** Memory pressure level */
|
|
76
|
+
pressure: 'normal' | 'elevated' | 'critical';
|
|
77
|
+
/** Per-instance average memory estimate in bytes */
|
|
78
|
+
avgInstanceMemory: number;
|
|
79
|
+
/** Estimated headroom for new instances in bytes */
|
|
80
|
+
headroom: number;
|
|
81
|
+
/**
|
|
82
|
+
* **Piece 8 — V8 heap tracking.** Brainy's TS-side memory pressure
|
|
83
|
+
* is a separate signal from system RSS pressure: the V8 heap has its
|
|
84
|
+
* own hard ceiling (controlled by `--max-old-space-size`, default
|
|
85
|
+
* ~4 GB on Node) and GC starts thrashing well before the system is
|
|
86
|
+
* out of RAM. Tracked so subsystems (filter lowering, iter-spill,
|
|
87
|
+
* VFS pool) can shed JS allocations when the heap fills even if
|
|
88
|
+
* system memory is plentiful.
|
|
89
|
+
*/
|
|
90
|
+
v8Heap: V8HeapSnapshot;
|
|
91
|
+
/**
|
|
92
|
+
* **Piece J — OS-memory observation.** Snapshot of the
|
|
93
|
+
* kernel-reported `MemTotal` / `MemAvailable` / `MemFree` (Linux)
|
|
94
|
+
* or `os.totalmem` / `os.freemem` (fallback). Distinct from
|
|
95
|
+
* {@link processRss} (which is the THIS process's footprint) and
|
|
96
|
+
* from {@link freeMemory} (which is the legacy `os.freemem()`
|
|
97
|
+
* reading). The Adaptive DiskANN mode selector (Piece I) reads
|
|
98
|
+
* `osMemory.availableBytes` to decide which open mode to apply.
|
|
99
|
+
*/
|
|
100
|
+
osMemory: OsMemorySnapshot;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Snapshot of V8 heap state at observation time. Mirrors the subset
|
|
104
|
+
* of `v8.getHeapStatistics()` that matters for budget decisions, plus
|
|
105
|
+
* the derived pressure classification.
|
|
106
|
+
*/
|
|
107
|
+
export interface V8HeapSnapshot {
|
|
108
|
+
/** `used_heap_size` — live JS object footprint */
|
|
109
|
+
usedHeapSize: number;
|
|
110
|
+
/** `heap_size_limit` — V8's hard ceiling before OOM */
|
|
111
|
+
heapSizeLimit: number;
|
|
112
|
+
/** `external_memory` — buffers + mmap'd regions outside the JS heap */
|
|
113
|
+
externalMemory: number;
|
|
114
|
+
/** `used_heap_size / heap_size_limit` (0..1) */
|
|
115
|
+
ratio: number;
|
|
116
|
+
/**
|
|
117
|
+
* Pressure classification — same three-level shape as the
|
|
118
|
+
* system-RSS `pressure` field for consistency. Thresholds chosen
|
|
119
|
+
* to fire BEFORE V8's GC starts thrashing:
|
|
120
|
+
* - `elevated` ≥ 0.75 ratio (matches system PRESSURE_ELEVATED)
|
|
121
|
+
* - `critical` ≥ 0.90 ratio (matches system PRESSURE_CRITICAL)
|
|
122
|
+
*
|
|
123
|
+
* Subsystems should treat `elevated` as "stop growing
|
|
124
|
+
* allocations, prefer iter-spill paths" and `critical` as "shed
|
|
125
|
+
* allocations now, before the next minor GC".
|
|
126
|
+
*/
|
|
127
|
+
pressure: 'normal' | 'elevated' | 'critical';
|
|
128
|
+
}
|
|
129
|
+
export type { OsMemorySnapshot } from './OsMemoryProbe.js';
|
|
130
|
+
/**
|
|
131
|
+
* The named memory categories that share the cache budget. Subsystems
|
|
132
|
+
* register against one of these so the ResourceManager can rebalance
|
|
133
|
+
* allocations between them based on observed pressure.
|
|
134
|
+
*/
|
|
135
|
+
export type MemoryCategory = 'vectors' | 'metadata' | 'embedding' | 'other';
|
|
136
|
+
/**
|
|
137
|
+
* Callback fired when a subscriber's effective allocation changes. The
|
|
138
|
+
* `allowedBytes` value is the **current** ceiling — subsystems should
|
|
139
|
+
* cap their resident bytes at this number; if they're currently over,
|
|
140
|
+
* they should start shedding (evict, flush, etc).
|
|
141
|
+
*/
|
|
142
|
+
export type MemoryBudgetCallback = (allowedBytes: number) => void;
|
|
143
|
+
/**
|
|
144
|
+
* Adaptive resource manager for multi-instance Brainy deployments.
|
|
145
|
+
*
|
|
146
|
+
* Tracks active brainy instances, observes system resources, and computes
|
|
147
|
+
* dynamic memory budgets for cache sizing and pool eviction.
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```typescript
|
|
151
|
+
* const rm = ResourceManager.getInstance()
|
|
152
|
+
* rm.trackInstance('tenant-wicks', 450)
|
|
153
|
+
*
|
|
154
|
+
* const profile = rm.getResourceProfile()
|
|
155
|
+
* console.log(`Cache budget: ${profile.recommendedCacheSize / 1024 / 1024}MB`)
|
|
156
|
+
* console.log(`Headroom for new instances: ${profile.headroom / 1024 / 1024}MB`)
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
export declare class ResourceManager {
|
|
160
|
+
private static instance;
|
|
161
|
+
private readonly instances;
|
|
162
|
+
private readonly effectiveLimit;
|
|
163
|
+
private readonly isContainer;
|
|
164
|
+
private rebalanceTimer;
|
|
165
|
+
private onCacheResize;
|
|
166
|
+
/**
|
|
167
|
+
* **Piece J — OS-memory observation.** `/proc/meminfo`-backed
|
|
168
|
+
* snapshot source. Held as an instance field so tests can swap
|
|
169
|
+
* the probe via {@link _testInjectOsMemoryProbe}.
|
|
170
|
+
*/
|
|
171
|
+
private osMemoryProbe;
|
|
172
|
+
/**
|
|
173
|
+
* Active per-category memory subscribers, keyed by name. Each
|
|
174
|
+
* subscriber receives notifications when its effective allocation
|
|
175
|
+
* changes (whether from a whole-pool rebalance or a category-level
|
|
176
|
+
* dynamic adaptation).
|
|
177
|
+
*/
|
|
178
|
+
private readonly subscribers;
|
|
179
|
+
/**
|
|
180
|
+
* Current dynamic share for each category. Starts at `BASE_CATEGORY_SHARES`
|
|
181
|
+
* and adapts at runtime to reported pressure. Sum across categories is
|
|
182
|
+
* preserved at exactly 1.0 by the rebalance algorithm.
|
|
183
|
+
*/
|
|
184
|
+
private categoryShares;
|
|
185
|
+
/**
|
|
186
|
+
* 5-second pressure-tick timer. Only running while at least one
|
|
187
|
+
* category is at or above the high-water threshold; auto-stops once
|
|
188
|
+
* every category falls back below the low-water threshold.
|
|
189
|
+
*/
|
|
190
|
+
private pressureTickTimer;
|
|
191
|
+
/**
|
|
192
|
+
* Tracks how many consecutive ticks have shown no pressure — used to
|
|
193
|
+
* decide when to stop the pressure-tick.
|
|
194
|
+
*/
|
|
195
|
+
private quietTicks;
|
|
196
|
+
/** One-shot guard for the multi-tenant OS-limits deployment warning. */
|
|
197
|
+
private osLimitsWarned;
|
|
198
|
+
private constructor();
|
|
199
|
+
/**
|
|
200
|
+
* Get the singleton ResourceManager instance.
|
|
201
|
+
*/
|
|
202
|
+
static getInstance(): ResourceManager;
|
|
203
|
+
/**
|
|
204
|
+
* **Back-compat callback** for the cortex 2.x cache resize wiring.
|
|
205
|
+
* The cor plugin (`src/plugin.ts`) registers brainy's
|
|
206
|
+
* `UnifiedCache.setMaxSize` via this method; the callback fires on
|
|
207
|
+
* every whole-pool rebalance with the TOTAL cache budget (not a
|
|
208
|
+
* per-category slice).
|
|
209
|
+
*
|
|
210
|
+
* Behavior is unchanged from cortex 2.x: the callback receives the
|
|
211
|
+
* full `recommendedCacheSize` from `getResourceProfile()` whenever
|
|
212
|
+
* the pool resizes. The new per-category subscriber API
|
|
213
|
+
* ([`registerMemoryBudgetSubscriber`](#registerMemoryBudgetSubscriber))
|
|
214
|
+
* runs in PARALLEL — they don't interfere. Legacy callers can keep
|
|
215
|
+
* their existing wiring; new code that wants a category-specific
|
|
216
|
+
* allocation registers as a subscriber.
|
|
217
|
+
*
|
|
218
|
+
* @param callback - Called with the new recommended cache size in bytes
|
|
219
|
+
*/
|
|
220
|
+
setCacheResizeCallback(callback: (newSize: number) => void): void;
|
|
221
|
+
/**
|
|
222
|
+
* Register a subscriber for per-category memory budget notifications.
|
|
223
|
+
*
|
|
224
|
+
* Subscribers receive an `allowedBytes` value whenever their effective
|
|
225
|
+
* allocation changes — either because the overall pool resized (system
|
|
226
|
+
* memory shift, instance count change) or because the dynamic
|
|
227
|
+
* rebalance loop shifted capacity between categories under observed
|
|
228
|
+
* pressure.
|
|
229
|
+
*
|
|
230
|
+
* The returned function deregisters the subscriber. Each `name` must
|
|
231
|
+
* be unique; registering twice with the same name replaces the prior
|
|
232
|
+
* registration.
|
|
233
|
+
*
|
|
234
|
+
* @param name - Unique subscriber name (used with `reportNativeMemory`)
|
|
235
|
+
* @param category - The category this subscriber draws from
|
|
236
|
+
* @param callback - Invoked with the current `allowedBytes` ceiling
|
|
237
|
+
* @returns A deregistration function (call to detach the subscriber)
|
|
238
|
+
*
|
|
239
|
+
* @example
|
|
240
|
+
* ```typescript
|
|
241
|
+
* const rm = ResourceManager.getInstance()
|
|
242
|
+
* const unsubscribe = rm.registerMemoryBudgetSubscriber(
|
|
243
|
+
* 'metadata-lsm',
|
|
244
|
+
* 'metadata',
|
|
245
|
+
* (allowed) => { lsm.setMaxBytes(allowed) }
|
|
246
|
+
* )
|
|
247
|
+
* // Later, when the LSM measures its footprint:
|
|
248
|
+
* rm.reportNativeMemory('metadata-lsm', lsm.currentBytes())
|
|
249
|
+
* // To detach:
|
|
250
|
+
* unsubscribe()
|
|
251
|
+
* ```
|
|
252
|
+
*/
|
|
253
|
+
registerMemoryBudgetSubscriber(name: string, category: MemoryCategory, callback: MemoryBudgetCallback): () => void;
|
|
254
|
+
/**
|
|
255
|
+
* Report the current resident-memory footprint of a registered
|
|
256
|
+
* subscriber, in bytes. The rebalance loop uses these values to
|
|
257
|
+
* detect category-level pressure and shift capacity dynamically.
|
|
258
|
+
*
|
|
259
|
+
* Call this whenever the subscriber's footprint changes meaningfully
|
|
260
|
+
* (after a flush, after a major allocation, etc). The pressure-tick
|
|
261
|
+
* fires within 5 seconds of any single category exceeding 90% of its
|
|
262
|
+
* current allocation; calling more often is harmless.
|
|
263
|
+
*
|
|
264
|
+
* Unknown names are silently dropped — a subscriber that has
|
|
265
|
+
* deregistered may still have pending reports in flight.
|
|
266
|
+
*/
|
|
267
|
+
reportNativeMemory(name: string, bytes: number): void;
|
|
268
|
+
/**
|
|
269
|
+
* Report the current dynamic share for each category. Mostly used by
|
|
270
|
+
* tests + the diagnostics surface; production callers should subscribe
|
|
271
|
+
* via [`registerMemoryBudgetSubscriber`] instead of polling.
|
|
272
|
+
*
|
|
273
|
+
* The sum across categories is always exactly 1.0 (modulo float
|
|
274
|
+
* rounding); allocations sum to the cache budget.
|
|
275
|
+
*/
|
|
276
|
+
getCategoryShares(): Readonly<Record<MemoryCategory, number>>;
|
|
277
|
+
/**
|
|
278
|
+
* The current per-category allowed-bytes ceiling. Same as what an
|
|
279
|
+
* active subscriber received in its last callback. Provided for
|
|
280
|
+
* diagnostics; production callers subscribe instead of polling.
|
|
281
|
+
*/
|
|
282
|
+
getCategoryAllowedBytes(category: MemoryCategory): number;
|
|
283
|
+
/**
|
|
284
|
+
* Start periodic rebalancing (every 30s). Aligns with brainy's existing
|
|
285
|
+
* memory pressure monitoring interval.
|
|
286
|
+
*/
|
|
287
|
+
startRebalancing(): void;
|
|
288
|
+
/**
|
|
289
|
+
* Stop periodic rebalancing.
|
|
290
|
+
*/
|
|
291
|
+
stopRebalancing(): void;
|
|
292
|
+
/**
|
|
293
|
+
* Start the 5-second pressure-tick if it isn't already running.
|
|
294
|
+
* Idempotent. Called from `reportNativeMemory` when a category crosses
|
|
295
|
+
* the high-water threshold; stops on its own when every category
|
|
296
|
+
* falls back below the low-water threshold for 3 consecutive ticks.
|
|
297
|
+
*/
|
|
298
|
+
private ensurePressureTickRunning;
|
|
299
|
+
private stopPressureTick;
|
|
300
|
+
/**
|
|
301
|
+
* Test-only hook to drive a pressure tick synchronously without
|
|
302
|
+
* waiting for the timer. Not exposed to production callers; tests
|
|
303
|
+
* use this to assert the rebalance behaviour deterministically.
|
|
304
|
+
* @internal
|
|
305
|
+
*/
|
|
306
|
+
_testTickPressure(): void;
|
|
307
|
+
/**
|
|
308
|
+
* Track a new or updated brainy instance. Call when a brainy instance is
|
|
309
|
+
* created or when its entity count changes significantly.
|
|
310
|
+
*
|
|
311
|
+
* @param key - Unique scope key for this instance (e.g. tenant slug, user:workspace)
|
|
312
|
+
* @param entityCount - Current entity count in this instance
|
|
313
|
+
*/
|
|
314
|
+
trackInstance(key: string, entityCount: number): void;
|
|
315
|
+
/**
|
|
316
|
+
* One-shot deployment warning: when enough brains are pooled in ONE
|
|
317
|
+
* process that OS file/map limits could bottleneck before RAM — and the
|
|
318
|
+
* limits are below the projected need — log the exact fix. Cor
|
|
319
|
+
* auto-raises the SOFT `nofile` limit itself; the hard cap +
|
|
320
|
+
* `vm.max_map_count` must be set by the deployment (a process cannot
|
|
321
|
+
* raise those). Context-aware: silent for a single big brain (Venue);
|
|
322
|
+
* fires only at real pool scale. See `docs/ADR-004-multi-tenant-storage.md`.
|
|
323
|
+
*/
|
|
324
|
+
private maybeWarnOsLimits;
|
|
325
|
+
/**
|
|
326
|
+
* Stop tracking a brainy instance. Call when the instance is evicted or closed.
|
|
327
|
+
*
|
|
328
|
+
* @param key - The scope key that was passed to trackInstance()
|
|
329
|
+
*/
|
|
330
|
+
untrackInstance(key: string): void;
|
|
331
|
+
/**
|
|
332
|
+
* Record an access to an instance (updates lastAccess for weighted eviction).
|
|
333
|
+
*
|
|
334
|
+
* @param key - The scope key
|
|
335
|
+
*/
|
|
336
|
+
touchInstance(key: string): void;
|
|
337
|
+
/**
|
|
338
|
+
* **Piece 8 — V8 heap tracking.** Snapshot the current V8 heap
|
|
339
|
+
* state and classify pressure. Cheap call (a single
|
|
340
|
+
* `v8.getHeapStatistics()` syscall + a few divisions); fine to
|
|
341
|
+
* invoke per-query on the hot path. Subsystems like brainy's filter
|
|
342
|
+
* lowering use this to early-exit before allocating big
|
|
343
|
+
* intermediate bitmaps that would push the heap over the ceiling.
|
|
344
|
+
*
|
|
345
|
+
* The pressure classification uses the same three-level shape as
|
|
346
|
+
* system-RSS pressure (`normal` / `elevated` / `critical`) so
|
|
347
|
+
* subscribers can branch uniformly.
|
|
348
|
+
*/
|
|
349
|
+
getHeapStats(): V8HeapSnapshot;
|
|
350
|
+
/**
|
|
351
|
+
* **Piece 8.** Convenience accessor returning just the heap
|
|
352
|
+
* pressure classification — cheaper than constructing a full
|
|
353
|
+
* {@link V8HeapSnapshot} when the caller only needs the branch.
|
|
354
|
+
*/
|
|
355
|
+
getHeapPressure(): 'normal' | 'elevated' | 'critical';
|
|
356
|
+
/**
|
|
357
|
+
* **Piece J.** Latest OS-memory snapshot from `/proc/meminfo`
|
|
358
|
+
* (Linux) or the os-fallback path. Cached for 250 ms by the
|
|
359
|
+
* underlying {@link OsMemoryProbe}; consecutive calls inside the
|
|
360
|
+
* cache window reuse the value. Cheaper than
|
|
361
|
+
* {@link getResourceProfile} when the caller only needs the
|
|
362
|
+
* memory snapshot (the Adaptive DiskANN selector — Piece I — is
|
|
363
|
+
* the canonical caller).
|
|
364
|
+
*/
|
|
365
|
+
getOsMemorySnapshot(): OsMemorySnapshot;
|
|
366
|
+
/**
|
|
367
|
+
* Test-only hook to swap the OS-memory probe with a deterministic
|
|
368
|
+
* stand-in. Production code must not call this — the singleton's
|
|
369
|
+
* probe is part of the resource-observation contract.
|
|
370
|
+
* @internal
|
|
371
|
+
*/
|
|
372
|
+
_testInjectOsMemoryProbe(probe: OsMemoryProbe): void;
|
|
373
|
+
/**
|
|
374
|
+
* Compute a full resource profile snapshot.
|
|
375
|
+
*
|
|
376
|
+
* @returns Current system resources, instance stats, and recommendations
|
|
377
|
+
*/
|
|
378
|
+
getResourceProfile(): ResourceProfile;
|
|
379
|
+
/**
|
|
380
|
+
* Check if there's enough memory headroom to create a new instance
|
|
381
|
+
* with the given entity count.
|
|
382
|
+
*
|
|
383
|
+
* @param estimatedEntities - Expected entity count for the new instance
|
|
384
|
+
* @returns true if headroom is sufficient
|
|
385
|
+
*/
|
|
386
|
+
canAccommodateInstance(estimatedEntities?: number): boolean;
|
|
387
|
+
/**
|
|
388
|
+
* **Piece 8 commit 2 — observed per-entity RAM estimate.** When
|
|
389
|
+
* any subscriber has reported native memory via
|
|
390
|
+
* {@link reportNativeMemory}, the ratio of total reported bytes
|
|
391
|
+
* to total tracked entities is the actual observed per-entity
|
|
392
|
+
* footprint — accurate for THIS deployment's workload mix instead
|
|
393
|
+
* of a one-size-fits-all guess. Falls back to
|
|
394
|
+
* {@link FALLBACK_BYTES_PER_ENTITY} when subscribers haven't
|
|
395
|
+
* reported (cold boot, no instances tracked, or workloads where
|
|
396
|
+
* the brain hasn't done any reading yet).
|
|
397
|
+
*
|
|
398
|
+
* Floored at {@link MIN_BYTES_PER_ENTITY} so a subscriber that
|
|
399
|
+
* temporarily under-reports during a flush doesn't make
|
|
400
|
+
* `canAccommodateInstance` say "yes" to instances that can't fit.
|
|
401
|
+
*
|
|
402
|
+
* Cheap call: O(subscribers + instances) — both bounded by a few
|
|
403
|
+
* dozen in production. Fine to invoke per trackInstance /
|
|
404
|
+
* canAccommodateInstance call.
|
|
405
|
+
*/
|
|
406
|
+
private observedBytesPerEntity;
|
|
407
|
+
/**
|
|
408
|
+
* Get the scope key of the instance that should be evicted first.
|
|
409
|
+
* Uses weighted scoring: large + idle instances are evicted before
|
|
410
|
+
* small + active ones.
|
|
411
|
+
*
|
|
412
|
+
* @returns The key to evict, or null if no instances are tracked
|
|
413
|
+
*/
|
|
414
|
+
getEvictionCandidate(): string | null;
|
|
415
|
+
/**
|
|
416
|
+
* Rebalance cache sizing based on current instance count and memory state.
|
|
417
|
+
*
|
|
418
|
+
* Three responsibilities:
|
|
419
|
+
* 1. Recompute the total cache budget from instance count + system
|
|
420
|
+
* memory (the pre-Piece-8 behaviour).
|
|
421
|
+
* 2. Fire the back-compat `onCacheResize` if it exists.
|
|
422
|
+
* 3. Notify every per-category subscriber of its current
|
|
423
|
+
* `allowedBytes` ceiling — picks up any whole-pool shift even
|
|
424
|
+
* when the per-category dynamic adaptation isn't pressed.
|
|
425
|
+
*/
|
|
426
|
+
private rebalance;
|
|
427
|
+
/**
|
|
428
|
+
* Drive one tick of the per-category dynamic rebalance. Reads the
|
|
429
|
+
* latest `reportNativeMemory` values, identifies over- and under-
|
|
430
|
+
* utilized categories, shifts the dynamic shares between them
|
|
431
|
+
* (bounded by `CATEGORY_MIN_SHARE` / `CATEGORY_MAX_SHARE`), and
|
|
432
|
+
* notifies any subscriber whose allowance changed meaningfully.
|
|
433
|
+
*
|
|
434
|
+
* Auto-stops the pressure-tick loop after 3 consecutive quiet ticks
|
|
435
|
+
* (no category > low-water).
|
|
436
|
+
*/
|
|
437
|
+
private runPressureTick;
|
|
438
|
+
/**
|
|
439
|
+
* Shift dynamic share from donor categories to recipient categories.
|
|
440
|
+
*
|
|
441
|
+
* Both sides are PRE-CAPPED by the per-category min/max share bounds
|
|
442
|
+
* (25% / 200% of base share, respectively) BEFORE the proportional
|
|
443
|
+
* distribution runs. Pre-capping is the only safe pattern: clamping
|
|
444
|
+
* after a proportional split would either (a) lose capacity (donors
|
|
445
|
+
* give shares that recipients can't accept due to their ceiling) or
|
|
446
|
+
* (b) require renormalization, which silently violates the ceiling.
|
|
447
|
+
*
|
|
448
|
+
* `transfer = min(total bounded contribution, total bounded demand)`
|
|
449
|
+
* — by construction the per-donor `give` and per-recipient `take`
|
|
450
|
+
* sums are equal, so the shares always sum to 1.0 without
|
|
451
|
+
* renormalization. The float drift over many cycles is bounded by
|
|
452
|
+
* the floating-point precision of a single division, well under any
|
|
453
|
+
* test's threshold.
|
|
454
|
+
*/
|
|
455
|
+
private rebalanceShares;
|
|
456
|
+
/**
|
|
457
|
+
* Compute the current allowed-bytes ceiling for a category.
|
|
458
|
+
*/
|
|
459
|
+
private computeAllowedBytes;
|
|
460
|
+
/**
|
|
461
|
+
* Sum the reportedBytes of every subscriber by category.
|
|
462
|
+
*/
|
|
463
|
+
private aggregateReportedBytesByCategory;
|
|
464
|
+
/**
|
|
465
|
+
* Notify every subscriber of its current allowed-bytes ceiling. Fires
|
|
466
|
+
* the callback only when the allowance has shifted by more than the
|
|
467
|
+
* `NOTIFY_DELTA` threshold from the last delivered value — avoids
|
|
468
|
+
* spurious wakeups on small re-rebalances.
|
|
469
|
+
*/
|
|
470
|
+
private notifySubscribers;
|
|
471
|
+
/**
|
|
472
|
+
* Normalize categoryShares so they sum to exactly 1.0. Compensates for
|
|
473
|
+
* float drift over many rebalance cycles. Preserves the relative
|
|
474
|
+
* proportions of the four categories.
|
|
475
|
+
*/
|
|
476
|
+
private renormalizeShares;
|
|
477
|
+
/**
|
|
478
|
+
* Compute the recommended cache budget given current instance memory usage.
|
|
479
|
+
*
|
|
480
|
+
* Formula: min(MAX_CACHE_RATIO × available, available - fixed - instances - headroom)
|
|
481
|
+
* Clamped to MIN_CACHE_BYTES.
|
|
482
|
+
*/
|
|
483
|
+
private computeCacheBudget;
|
|
484
|
+
/**
|
|
485
|
+
* Determine memory pressure level from current RSS.
|
|
486
|
+
*/
|
|
487
|
+
private computePressure;
|
|
488
|
+
private getTotalEntities;
|
|
489
|
+
private getTotalInstanceMemory;
|
|
490
|
+
}
|
|
491
|
+
//# sourceMappingURL=ResourceManager.d.ts.map
|