@camstack/addon-post-analysis 1.2.91 → 1.2.93
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{dist-CquggoRC.mjs → dist-3QRyOemL.mjs} +353 -20
- package/dist/{dist-CTIUuGRB.js → dist-DYoWeRRt.js} +370 -19
- package/dist/embedding-encoder/index.js +171 -4
- package/dist/embedding-encoder/index.mjs +171 -4
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Q3EjHtct.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-C4-ljPpt.mjs} +3 -3
- package/dist/pipeline-analytics/{hostInit-D2EZ3nOf.mjs → hostInit-BoUejl7E.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +389 -160
- package/dist/pipeline-analytics/index.mjs +352 -123
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +3 -2
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-DYoWeRRt.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
let node_fs$1 = require_dist.__toESM(node_fs, 1);
|
|
8
8
|
node_fs = require_dist.__toESM(node_fs);
|
|
@@ -10,9 +10,56 @@ let node_path = require("node:path");
|
|
|
10
10
|
let node_path$1 = require_dist.__toESM(node_path, 1);
|
|
11
11
|
node_path = require_dist.__toESM(node_path);
|
|
12
12
|
require("node:crypto");
|
|
13
|
+
let node_child_process = require("node:child_process");
|
|
13
14
|
let node_util = require("node:util");
|
|
14
15
|
let node_zlib = require("node:zlib");
|
|
15
|
-
|
|
16
|
+
//#region src/embedding-encoder/shared/process-memory.ts
|
|
17
|
+
/**
|
|
18
|
+
* Read a child process's memory footprint for the pool memory watchdog.
|
|
19
|
+
*
|
|
20
|
+
* Deliberate twin of `packages/addon-pipeline/src/shared/process-memory.ts`:
|
|
21
|
+
* addons must not import each other, and `@camstack/system` stays EXTERNAL in
|
|
22
|
+
* addon bundles (a symbol added there would be missing at runtime until the
|
|
23
|
+
* next framework train). The pure parse (`parseProcStatus`) and the whole
|
|
24
|
+
* watchdog policy live once in `@camstack/types` (inlined at build time);
|
|
25
|
+
* only this thin `fs`/`ps` read is duplicated.
|
|
26
|
+
*
|
|
27
|
+
* Linux (the fleet): `/proc/<pid>/status` — RSS, VmSize, VmHWM (peak RSS ≈
|
|
28
|
+
* getrusage maxrss), VmSwap, Threads in one read. macOS (dev): `ps -o rss=`
|
|
29
|
+
* gives RSS alone. Returns null when the pid is gone — the CALLER logs the
|
|
30
|
+
* dropped sample.
|
|
31
|
+
*/
|
|
32
|
+
var IS_LINUX = process.platform === "linux";
|
|
33
|
+
async function readProcessMemory(pid) {
|
|
34
|
+
if (IS_LINUX) try {
|
|
35
|
+
return require_dist.parseProcStatus(await node_fs.promises.readFile(`/proc/${pid}/status`, "utf8"));
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return readViaPs(pid);
|
|
40
|
+
}
|
|
41
|
+
function readViaPs(pid) {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
(0, node_child_process.execFile)("ps", [
|
|
44
|
+
"-o",
|
|
45
|
+
"rss=",
|
|
46
|
+
"-p",
|
|
47
|
+
String(pid)
|
|
48
|
+
], { timeout: 2e3 }, (err, stdout) => {
|
|
49
|
+
if (err) return resolve(null);
|
|
50
|
+
const kb = Number(stdout.trim());
|
|
51
|
+
if (!Number.isFinite(kb) || kb <= 0) return resolve(null);
|
|
52
|
+
resolve({
|
|
53
|
+
rssBytes: kb * 1024,
|
|
54
|
+
vmBytes: 0,
|
|
55
|
+
hwmBytes: 0,
|
|
56
|
+
swapBytes: 0,
|
|
57
|
+
threads: 0
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
16
63
|
//#region ../system/dist/file-data-plane-CuE_hBli.mjs
|
|
17
64
|
function isNonEmptyFile(filePath) {
|
|
18
65
|
return node_fs$1.existsSync(filePath) && node_fs$1.statSync(filePath).size > 0;
|
|
@@ -499,8 +546,20 @@ var PythonRawTensorEngine = class {
|
|
|
499
546
|
if (ready.length !== 1 || ready[0] !== 1) throw new Error("PythonRawTensorEngine: unexpected ready frame");
|
|
500
547
|
this.log.info("ONNX raw-tensor engine ready (embedded Python)", { meta: { modelPath: this.modelPath } });
|
|
501
548
|
}
|
|
549
|
+
/** Native pid of the Python subprocess — sampled by the pool memory
|
|
550
|
+
* watchdog (`/proc/<pid>/status`). */
|
|
551
|
+
getPid() {
|
|
552
|
+
return this.process?.pid ?? null;
|
|
553
|
+
}
|
|
554
|
+
/** Inference requests served since spawn — the denominator that separates
|
|
555
|
+
* "RSS grew per unit of work" (leak) from "RSS grew because it worked". */
|
|
556
|
+
getRequestCount() {
|
|
557
|
+
return this.requestCount;
|
|
558
|
+
}
|
|
559
|
+
requestCount = 0;
|
|
502
560
|
async run(input, inputShape) {
|
|
503
561
|
if (!this.process?.stdin) throw new Error("PythonRawTensorEngine: not initialized — call initialize() first");
|
|
562
|
+
this.requestCount++;
|
|
504
563
|
const ndims = inputShape.length;
|
|
505
564
|
const meta = Buffer.allocUnsafe(1 + ndims * 4);
|
|
506
565
|
meta.writeUInt8(ndims, 0);
|
|
@@ -625,9 +684,21 @@ var PythonTextEncoderEngine = class {
|
|
|
625
684
|
tokenizerPath: this.tokenizerPath
|
|
626
685
|
} });
|
|
627
686
|
}
|
|
687
|
+
/** Native pid of the Python subprocess — sampled by the pool memory
|
|
688
|
+
* watchdog (`/proc/<pid>/status`). */
|
|
689
|
+
getPid() {
|
|
690
|
+
return this.process?.pid ?? null;
|
|
691
|
+
}
|
|
692
|
+
/** Encode requests served since spawn — the denominator that separates
|
|
693
|
+
* "RSS grew per unit of work" (leak) from "RSS grew because it worked". */
|
|
694
|
+
getRequestCount() {
|
|
695
|
+
return this.requestCount;
|
|
696
|
+
}
|
|
697
|
+
requestCount = 0;
|
|
628
698
|
/** Tokenize + encode `text` into the model embedding (float32). */
|
|
629
699
|
async encode(text) {
|
|
630
700
|
if (!this.process?.stdin) throw new Error("PythonTextEncoderEngine: not initialized — call initialize() first");
|
|
701
|
+
this.requestCount++;
|
|
631
702
|
const payload = Buffer.from(text, "utf-8");
|
|
632
703
|
const lenBuf = Buffer.allocUnsafe(4);
|
|
633
704
|
lenBuf.writeUInt32LE(payload.length, 0);
|
|
@@ -753,6 +824,22 @@ var EmbeddingEncoderAddon = class extends require_dist.BaseAddon {
|
|
|
753
824
|
imageRawEngine = null;
|
|
754
825
|
textEngine = null;
|
|
755
826
|
models = null;
|
|
827
|
+
/**
|
|
828
|
+
* RSS bound + periodic memory telemetry for the two Python engines
|
|
829
|
+
* (`raw_tensor_inference.py` — one of the four leaking pools in the
|
|
830
|
+
* 2026-08-18 OOM audit — and `text_encoder_inference.py`). Shared
|
|
831
|
+
* watchdog from `@camstack/types`; a restart here is dispose + null, and
|
|
832
|
+
* the next encode lazily re-resolves the engine (the exact recovery an
|
|
833
|
+
* operator model change already uses). Started in `onInitialize`,
|
|
834
|
+
* stopped in `onShutdown`.
|
|
835
|
+
*/
|
|
836
|
+
memoryGuard = null;
|
|
837
|
+
/** Single-flight guards for the lazy engine builds — a watchdog restart
|
|
838
|
+
* followed by two concurrent encodes must not spawn the engine twice
|
|
839
|
+
* (the same bug detection-pipeline fixed three times; see its
|
|
840
|
+
* `engineFactoryInflight`). */
|
|
841
|
+
imageEngineInflight = null;
|
|
842
|
+
textEngineInflight = null;
|
|
756
843
|
constructor() {
|
|
757
844
|
super({
|
|
758
845
|
modelId: DEFAULT_CLIP_MODEL,
|
|
@@ -763,11 +850,75 @@ var EmbeddingEncoderAddon = class extends require_dist.BaseAddon {
|
|
|
763
850
|
async onInitialize() {
|
|
764
851
|
const modelsDir = await this.resolveModelsDir();
|
|
765
852
|
this.models = new ModelDownloadService(modelsDir, [...CLIP_IMAGE_MODELS, ...CLIP_TEXT_MODELS]);
|
|
853
|
+
this.memoryGuard = new require_dist.PoolMemoryWatchdog({
|
|
854
|
+
policy: require_dist.resolvePoolMemoryPolicy(process.env),
|
|
855
|
+
log: this.ctx.logger.child("pool-memory"),
|
|
856
|
+
sample: () => this.sampleEngineMemory(),
|
|
857
|
+
restart: (key) => this.restartEngineForMemory(key)
|
|
858
|
+
});
|
|
859
|
+
this.memoryGuard.start();
|
|
766
860
|
return [{
|
|
767
861
|
capability: require_dist.embeddingEncoderCapability,
|
|
768
862
|
provider: this
|
|
769
863
|
}];
|
|
770
864
|
}
|
|
865
|
+
/** Watchdog sample: one telemetry row per live Python engine. A pid the
|
|
866
|
+
* read cannot see is logged, never silently skipped. */
|
|
867
|
+
async sampleEngineMemory() {
|
|
868
|
+
const engines = [{
|
|
869
|
+
key: "clip-image",
|
|
870
|
+
pid: this.imageRawEngine?.getPid() ?? null,
|
|
871
|
+
requests: this.imageRawEngine?.getRequestCount() ?? 0
|
|
872
|
+
}, {
|
|
873
|
+
key: "clip-text",
|
|
874
|
+
pid: this.textEngine?.getPid() ?? null,
|
|
875
|
+
requests: this.textEngine?.getRequestCount() ?? 0
|
|
876
|
+
}];
|
|
877
|
+
const out = [];
|
|
878
|
+
for (const engine of engines) {
|
|
879
|
+
if (engine.pid === null) continue;
|
|
880
|
+
const mem = await readProcessMemory(engine.pid);
|
|
881
|
+
if (!mem) {
|
|
882
|
+
this.ctx.logger.warn("embedding engine memory sample dropped", { meta: {
|
|
883
|
+
poolKey: engine.key,
|
|
884
|
+
pid: engine.pid
|
|
885
|
+
} });
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
const mb = (bytes) => Math.round(bytes / (1024 * 1024));
|
|
889
|
+
out.push({
|
|
890
|
+
key: engine.key,
|
|
891
|
+
pids: [engine.pid],
|
|
892
|
+
rssBytes: mem.rssBytes,
|
|
893
|
+
meta: {
|
|
894
|
+
modelId: this.config.modelId,
|
|
895
|
+
vmMb: mb(mem.vmBytes),
|
|
896
|
+
peakRssMb: mb(mem.hwmBytes),
|
|
897
|
+
swapMb: mb(mem.swapBytes),
|
|
898
|
+
threads: mem.threads,
|
|
899
|
+
requestsServed: engine.requests
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
return out;
|
|
904
|
+
}
|
|
905
|
+
/** Watchdog restart: dispose + null; the next encode re-resolves lazily
|
|
906
|
+
* through the single-flight ensure. An in-flight encode fails once — its
|
|
907
|
+
* callers are the retry-until-landed bulk passes (D58) and RPCs that
|
|
908
|
+
* surface errors, never silent loss. */
|
|
909
|
+
async restartEngineForMemory(key) {
|
|
910
|
+
if (key === "clip-image") {
|
|
911
|
+
const engine = this.imageRawEngine;
|
|
912
|
+
if (!engine) return;
|
|
913
|
+
this.imageRawEngine = null;
|
|
914
|
+
await engine.dispose();
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
const engine = this.textEngine;
|
|
918
|
+
if (!engine) return;
|
|
919
|
+
this.textEngine = null;
|
|
920
|
+
await engine.dispose();
|
|
921
|
+
}
|
|
771
922
|
async encode(input) {
|
|
772
923
|
const { crop, width, height } = input;
|
|
773
924
|
await this.ensureImageEngine();
|
|
@@ -811,17 +962,31 @@ var EmbeddingEncoderAddon = class extends require_dist.BaseAddon {
|
|
|
811
962
|
}
|
|
812
963
|
async ensureImageEngine() {
|
|
813
964
|
if (this.imageRawEngine) return;
|
|
965
|
+
if (this.imageEngineInflight) return this.imageEngineInflight;
|
|
814
966
|
const meta = getModelMeta(this.config.modelId);
|
|
815
967
|
const imageEntry = CLIP_IMAGE_MODELS.find((m) => m.id === meta.imageModelId);
|
|
816
968
|
if (!imageEntry) throw new Error(`EmbeddingEncoderAddon: unknown image model "${meta.imageModelId}"`);
|
|
817
|
-
|
|
969
|
+
const inflight = this.resolveForEntry(imageEntry, "image");
|
|
970
|
+
this.imageEngineInflight = inflight;
|
|
971
|
+
try {
|
|
972
|
+
await inflight;
|
|
973
|
+
} finally {
|
|
974
|
+
this.imageEngineInflight = null;
|
|
975
|
+
}
|
|
818
976
|
}
|
|
819
977
|
async ensureTextEngine() {
|
|
820
978
|
if (this.textEngine) return;
|
|
979
|
+
if (this.textEngineInflight) return this.textEngineInflight;
|
|
821
980
|
const meta = getModelMeta(this.config.modelId);
|
|
822
981
|
const textEntry = CLIP_TEXT_MODELS.find((m) => m.id === meta.textModelId);
|
|
823
982
|
if (!textEntry) throw new Error(`EmbeddingEncoderAddon: unknown text model "${meta.textModelId}"`);
|
|
824
|
-
|
|
983
|
+
const inflight = this.resolveForEntry(textEntry, "text");
|
|
984
|
+
this.textEngineInflight = inflight;
|
|
985
|
+
try {
|
|
986
|
+
await inflight;
|
|
987
|
+
} finally {
|
|
988
|
+
this.textEngineInflight = null;
|
|
989
|
+
}
|
|
825
990
|
}
|
|
826
991
|
async resolveForEntry(entry, target) {
|
|
827
992
|
const engineLogger = this.ctx.logger.withTags({
|
|
@@ -847,6 +1012,8 @@ var EmbeddingEncoderAddon = class extends require_dist.BaseAddon {
|
|
|
847
1012
|
this.textEngine = textEngine;
|
|
848
1013
|
}
|
|
849
1014
|
async onShutdown() {
|
|
1015
|
+
this.memoryGuard?.stop();
|
|
1016
|
+
this.memoryGuard = null;
|
|
850
1017
|
await this.imageRawEngine?.dispose();
|
|
851
1018
|
await this.textEngine?.dispose();
|
|
852
1019
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { O as PoolMemoryWatchdog, Q as embeddingEncoderCapability, dt as resolvePoolMemoryPolicy, ot as parseProcStatus, tt as hfModelUrl, yt as BaseAddon } from "../dist-3QRyOemL.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import "node:fs";
|
|
5
5
|
import * as path$1 from "node:path";
|
|
6
6
|
import "node:path";
|
|
7
7
|
import "node:crypto";
|
|
8
|
+
import { execFile, spawn } from "node:child_process";
|
|
8
9
|
import { promisify } from "node:util";
|
|
9
10
|
import { brotliCompress, gzip } from "node:zlib";
|
|
10
|
-
import { spawn } from "node:child_process";
|
|
11
11
|
//#region \0rolldown/runtime.js
|
|
12
12
|
var __defProp = Object.defineProperty;
|
|
13
13
|
var __exportAll = (all, no_symbols) => {
|
|
@@ -21,6 +21,53 @@ var __exportAll = (all, no_symbols) => {
|
|
|
21
21
|
};
|
|
22
22
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
23
23
|
//#endregion
|
|
24
|
+
//#region src/embedding-encoder/shared/process-memory.ts
|
|
25
|
+
/**
|
|
26
|
+
* Read a child process's memory footprint for the pool memory watchdog.
|
|
27
|
+
*
|
|
28
|
+
* Deliberate twin of `packages/addon-pipeline/src/shared/process-memory.ts`:
|
|
29
|
+
* addons must not import each other, and `@camstack/system` stays EXTERNAL in
|
|
30
|
+
* addon bundles (a symbol added there would be missing at runtime until the
|
|
31
|
+
* next framework train). The pure parse (`parseProcStatus`) and the whole
|
|
32
|
+
* watchdog policy live once in `@camstack/types` (inlined at build time);
|
|
33
|
+
* only this thin `fs`/`ps` read is duplicated.
|
|
34
|
+
*
|
|
35
|
+
* Linux (the fleet): `/proc/<pid>/status` — RSS, VmSize, VmHWM (peak RSS ≈
|
|
36
|
+
* getrusage maxrss), VmSwap, Threads in one read. macOS (dev): `ps -o rss=`
|
|
37
|
+
* gives RSS alone. Returns null when the pid is gone — the CALLER logs the
|
|
38
|
+
* dropped sample.
|
|
39
|
+
*/
|
|
40
|
+
var IS_LINUX = process.platform === "linux";
|
|
41
|
+
async function readProcessMemory(pid) {
|
|
42
|
+
if (IS_LINUX) try {
|
|
43
|
+
return parseProcStatus(await fs.promises.readFile(`/proc/${pid}/status`, "utf8"));
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return readViaPs(pid);
|
|
48
|
+
}
|
|
49
|
+
function readViaPs(pid) {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
execFile("ps", [
|
|
52
|
+
"-o",
|
|
53
|
+
"rss=",
|
|
54
|
+
"-p",
|
|
55
|
+
String(pid)
|
|
56
|
+
], { timeout: 2e3 }, (err, stdout) => {
|
|
57
|
+
if (err) return resolve(null);
|
|
58
|
+
const kb = Number(stdout.trim());
|
|
59
|
+
if (!Number.isFinite(kb) || kb <= 0) return resolve(null);
|
|
60
|
+
resolve({
|
|
61
|
+
rssBytes: kb * 1024,
|
|
62
|
+
vmBytes: 0,
|
|
63
|
+
hwmBytes: 0,
|
|
64
|
+
swapBytes: 0,
|
|
65
|
+
threads: 0
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
24
71
|
//#region ../system/dist/file-data-plane-CuE_hBli.mjs
|
|
25
72
|
function isNonEmptyFile(filePath) {
|
|
26
73
|
return fs.existsSync(filePath) && fs.statSync(filePath).size > 0;
|
|
@@ -507,8 +554,20 @@ var PythonRawTensorEngine = class {
|
|
|
507
554
|
if (ready.length !== 1 || ready[0] !== 1) throw new Error("PythonRawTensorEngine: unexpected ready frame");
|
|
508
555
|
this.log.info("ONNX raw-tensor engine ready (embedded Python)", { meta: { modelPath: this.modelPath } });
|
|
509
556
|
}
|
|
557
|
+
/** Native pid of the Python subprocess — sampled by the pool memory
|
|
558
|
+
* watchdog (`/proc/<pid>/status`). */
|
|
559
|
+
getPid() {
|
|
560
|
+
return this.process?.pid ?? null;
|
|
561
|
+
}
|
|
562
|
+
/** Inference requests served since spawn — the denominator that separates
|
|
563
|
+
* "RSS grew per unit of work" (leak) from "RSS grew because it worked". */
|
|
564
|
+
getRequestCount() {
|
|
565
|
+
return this.requestCount;
|
|
566
|
+
}
|
|
567
|
+
requestCount = 0;
|
|
510
568
|
async run(input, inputShape) {
|
|
511
569
|
if (!this.process?.stdin) throw new Error("PythonRawTensorEngine: not initialized — call initialize() first");
|
|
570
|
+
this.requestCount++;
|
|
512
571
|
const ndims = inputShape.length;
|
|
513
572
|
const meta = Buffer.allocUnsafe(1 + ndims * 4);
|
|
514
573
|
meta.writeUInt8(ndims, 0);
|
|
@@ -633,9 +692,21 @@ var PythonTextEncoderEngine = class {
|
|
|
633
692
|
tokenizerPath: this.tokenizerPath
|
|
634
693
|
} });
|
|
635
694
|
}
|
|
695
|
+
/** Native pid of the Python subprocess — sampled by the pool memory
|
|
696
|
+
* watchdog (`/proc/<pid>/status`). */
|
|
697
|
+
getPid() {
|
|
698
|
+
return this.process?.pid ?? null;
|
|
699
|
+
}
|
|
700
|
+
/** Encode requests served since spawn — the denominator that separates
|
|
701
|
+
* "RSS grew per unit of work" (leak) from "RSS grew because it worked". */
|
|
702
|
+
getRequestCount() {
|
|
703
|
+
return this.requestCount;
|
|
704
|
+
}
|
|
705
|
+
requestCount = 0;
|
|
636
706
|
/** Tokenize + encode `text` into the model embedding (float32). */
|
|
637
707
|
async encode(text) {
|
|
638
708
|
if (!this.process?.stdin) throw new Error("PythonTextEncoderEngine: not initialized — call initialize() first");
|
|
709
|
+
this.requestCount++;
|
|
639
710
|
const payload = Buffer.from(text, "utf-8");
|
|
640
711
|
const lenBuf = Buffer.allocUnsafe(4);
|
|
641
712
|
lenBuf.writeUInt32LE(payload.length, 0);
|
|
@@ -761,6 +832,22 @@ var EmbeddingEncoderAddon = class extends BaseAddon {
|
|
|
761
832
|
imageRawEngine = null;
|
|
762
833
|
textEngine = null;
|
|
763
834
|
models = null;
|
|
835
|
+
/**
|
|
836
|
+
* RSS bound + periodic memory telemetry for the two Python engines
|
|
837
|
+
* (`raw_tensor_inference.py` — one of the four leaking pools in the
|
|
838
|
+
* 2026-08-18 OOM audit — and `text_encoder_inference.py`). Shared
|
|
839
|
+
* watchdog from `@camstack/types`; a restart here is dispose + null, and
|
|
840
|
+
* the next encode lazily re-resolves the engine (the exact recovery an
|
|
841
|
+
* operator model change already uses). Started in `onInitialize`,
|
|
842
|
+
* stopped in `onShutdown`.
|
|
843
|
+
*/
|
|
844
|
+
memoryGuard = null;
|
|
845
|
+
/** Single-flight guards for the lazy engine builds — a watchdog restart
|
|
846
|
+
* followed by two concurrent encodes must not spawn the engine twice
|
|
847
|
+
* (the same bug detection-pipeline fixed three times; see its
|
|
848
|
+
* `engineFactoryInflight`). */
|
|
849
|
+
imageEngineInflight = null;
|
|
850
|
+
textEngineInflight = null;
|
|
764
851
|
constructor() {
|
|
765
852
|
super({
|
|
766
853
|
modelId: DEFAULT_CLIP_MODEL,
|
|
@@ -771,11 +858,75 @@ var EmbeddingEncoderAddon = class extends BaseAddon {
|
|
|
771
858
|
async onInitialize() {
|
|
772
859
|
const modelsDir = await this.resolveModelsDir();
|
|
773
860
|
this.models = new ModelDownloadService(modelsDir, [...CLIP_IMAGE_MODELS, ...CLIP_TEXT_MODELS]);
|
|
861
|
+
this.memoryGuard = new PoolMemoryWatchdog({
|
|
862
|
+
policy: resolvePoolMemoryPolicy(process.env),
|
|
863
|
+
log: this.ctx.logger.child("pool-memory"),
|
|
864
|
+
sample: () => this.sampleEngineMemory(),
|
|
865
|
+
restart: (key) => this.restartEngineForMemory(key)
|
|
866
|
+
});
|
|
867
|
+
this.memoryGuard.start();
|
|
774
868
|
return [{
|
|
775
869
|
capability: embeddingEncoderCapability,
|
|
776
870
|
provider: this
|
|
777
871
|
}];
|
|
778
872
|
}
|
|
873
|
+
/** Watchdog sample: one telemetry row per live Python engine. A pid the
|
|
874
|
+
* read cannot see is logged, never silently skipped. */
|
|
875
|
+
async sampleEngineMemory() {
|
|
876
|
+
const engines = [{
|
|
877
|
+
key: "clip-image",
|
|
878
|
+
pid: this.imageRawEngine?.getPid() ?? null,
|
|
879
|
+
requests: this.imageRawEngine?.getRequestCount() ?? 0
|
|
880
|
+
}, {
|
|
881
|
+
key: "clip-text",
|
|
882
|
+
pid: this.textEngine?.getPid() ?? null,
|
|
883
|
+
requests: this.textEngine?.getRequestCount() ?? 0
|
|
884
|
+
}];
|
|
885
|
+
const out = [];
|
|
886
|
+
for (const engine of engines) {
|
|
887
|
+
if (engine.pid === null) continue;
|
|
888
|
+
const mem = await readProcessMemory(engine.pid);
|
|
889
|
+
if (!mem) {
|
|
890
|
+
this.ctx.logger.warn("embedding engine memory sample dropped", { meta: {
|
|
891
|
+
poolKey: engine.key,
|
|
892
|
+
pid: engine.pid
|
|
893
|
+
} });
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
const mb = (bytes) => Math.round(bytes / (1024 * 1024));
|
|
897
|
+
out.push({
|
|
898
|
+
key: engine.key,
|
|
899
|
+
pids: [engine.pid],
|
|
900
|
+
rssBytes: mem.rssBytes,
|
|
901
|
+
meta: {
|
|
902
|
+
modelId: this.config.modelId,
|
|
903
|
+
vmMb: mb(mem.vmBytes),
|
|
904
|
+
peakRssMb: mb(mem.hwmBytes),
|
|
905
|
+
swapMb: mb(mem.swapBytes),
|
|
906
|
+
threads: mem.threads,
|
|
907
|
+
requestsServed: engine.requests
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
return out;
|
|
912
|
+
}
|
|
913
|
+
/** Watchdog restart: dispose + null; the next encode re-resolves lazily
|
|
914
|
+
* through the single-flight ensure. An in-flight encode fails once — its
|
|
915
|
+
* callers are the retry-until-landed bulk passes (D58) and RPCs that
|
|
916
|
+
* surface errors, never silent loss. */
|
|
917
|
+
async restartEngineForMemory(key) {
|
|
918
|
+
if (key === "clip-image") {
|
|
919
|
+
const engine = this.imageRawEngine;
|
|
920
|
+
if (!engine) return;
|
|
921
|
+
this.imageRawEngine = null;
|
|
922
|
+
await engine.dispose();
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
const engine = this.textEngine;
|
|
926
|
+
if (!engine) return;
|
|
927
|
+
this.textEngine = null;
|
|
928
|
+
await engine.dispose();
|
|
929
|
+
}
|
|
779
930
|
async encode(input) {
|
|
780
931
|
const { crop, width, height } = input;
|
|
781
932
|
await this.ensureImageEngine();
|
|
@@ -819,17 +970,31 @@ var EmbeddingEncoderAddon = class extends BaseAddon {
|
|
|
819
970
|
}
|
|
820
971
|
async ensureImageEngine() {
|
|
821
972
|
if (this.imageRawEngine) return;
|
|
973
|
+
if (this.imageEngineInflight) return this.imageEngineInflight;
|
|
822
974
|
const meta = getModelMeta(this.config.modelId);
|
|
823
975
|
const imageEntry = CLIP_IMAGE_MODELS.find((m) => m.id === meta.imageModelId);
|
|
824
976
|
if (!imageEntry) throw new Error(`EmbeddingEncoderAddon: unknown image model "${meta.imageModelId}"`);
|
|
825
|
-
|
|
977
|
+
const inflight = this.resolveForEntry(imageEntry, "image");
|
|
978
|
+
this.imageEngineInflight = inflight;
|
|
979
|
+
try {
|
|
980
|
+
await inflight;
|
|
981
|
+
} finally {
|
|
982
|
+
this.imageEngineInflight = null;
|
|
983
|
+
}
|
|
826
984
|
}
|
|
827
985
|
async ensureTextEngine() {
|
|
828
986
|
if (this.textEngine) return;
|
|
987
|
+
if (this.textEngineInflight) return this.textEngineInflight;
|
|
829
988
|
const meta = getModelMeta(this.config.modelId);
|
|
830
989
|
const textEntry = CLIP_TEXT_MODELS.find((m) => m.id === meta.textModelId);
|
|
831
990
|
if (!textEntry) throw new Error(`EmbeddingEncoderAddon: unknown text model "${meta.textModelId}"`);
|
|
832
|
-
|
|
991
|
+
const inflight = this.resolveForEntry(textEntry, "text");
|
|
992
|
+
this.textEngineInflight = inflight;
|
|
993
|
+
try {
|
|
994
|
+
await inflight;
|
|
995
|
+
} finally {
|
|
996
|
+
this.textEngineInflight = null;
|
|
997
|
+
}
|
|
833
998
|
}
|
|
834
999
|
async resolveForEntry(entry, target) {
|
|
835
1000
|
const engineLogger = this.ctx.logger.withTags({
|
|
@@ -855,6 +1020,8 @@ var EmbeddingEncoderAddon = class extends BaseAddon {
|
|
|
855
1020
|
this.textEngine = textEngine;
|
|
856
1021
|
}
|
|
857
1022
|
async onShutdown() {
|
|
1023
|
+
this.memoryGuard?.stop();
|
|
1024
|
+
this.memoryGuard = null;
|
|
858
1025
|
await this.imageRawEngine?.dispose();
|
|
859
1026
|
await this.textEngine?.dispose();
|
|
860
1027
|
}
|
|
@@ -3,7 +3,7 @@ import "./dist-CYZr2fwk.mjs";
|
|
|
3
3
|
var e = {
|
|
4
4
|
"@camstack/sdk": {
|
|
5
5
|
name: "@camstack/sdk",
|
|
6
|
-
version: "1.2.
|
|
6
|
+
version: "1.2.23",
|
|
7
7
|
scope: ["default"],
|
|
8
8
|
loaded: !1,
|
|
9
9
|
from: "addon_pipeline_analytics_widgets",
|
|
@@ -18,7 +18,7 @@ var e = {
|
|
|
18
18
|
},
|
|
19
19
|
"@camstack/types": {
|
|
20
20
|
name: "@camstack/types",
|
|
21
|
-
version: "1.2.
|
|
21
|
+
version: "1.2.87",
|
|
22
22
|
scope: ["default"],
|
|
23
23
|
loaded: !1,
|
|
24
24
|
from: "addon_pipeline_analytics_widgets",
|
|
@@ -33,7 +33,7 @@ var e = {
|
|
|
33
33
|
},
|
|
34
34
|
"@camstack/ui-library": {
|
|
35
35
|
name: "@camstack/ui-library",
|
|
36
|
-
version: "1.2.
|
|
36
|
+
version: "1.2.59",
|
|
37
37
|
scope: ["default"],
|
|
38
38
|
loaded: !1,
|
|
39
39
|
from: "addon_pipeline_analytics_widgets",
|
|
@@ -36,7 +36,7 @@ async function r() {
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"@camstack/types": {
|
|
39
|
-
version: "1.2.
|
|
39
|
+
version: "1.2.87",
|
|
40
40
|
scope: "default",
|
|
41
41
|
shareConfig: {
|
|
42
42
|
singleton: !0,
|
|
@@ -45,7 +45,7 @@ async function r() {
|
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
47
|
"@camstack/sdk": {
|
|
48
|
-
version: "1.2.
|
|
48
|
+
version: "1.2.23",
|
|
49
49
|
scope: "default",
|
|
50
50
|
shareConfig: {
|
|
51
51
|
singleton: !0,
|
|
@@ -81,7 +81,7 @@ async function r() {
|
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
83
|
"@camstack/ui-library": {
|
|
84
|
-
version: "1.2.
|
|
84
|
+
version: "1.2.59",
|
|
85
85
|
scope: "default",
|
|
86
86
|
shareConfig: {
|
|
87
87
|
singleton: !0,
|