@camstack/system 1.2.137 → 1.2.138
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.
|
@@ -3949,6 +3949,112 @@ var DeviceMetaStore = class {
|
|
|
3949
3949
|
};
|
|
3950
3950
|
};
|
|
3951
3951
|
//#endregion
|
|
3952
|
+
//#region src/builtins/device-manager/fleet-read-census.ts
|
|
3953
|
+
/** How many stack frames identify a call site. One frame is a method; three are
|
|
3954
|
+
* a cause — `listFleet < listAll < onDeviceRegistered` is the answer, `listAll`
|
|
3955
|
+
* alone is not. Same choice, for the same reason, as `read-census.ts`. */
|
|
3956
|
+
var CENSUS_STACK_FRAMES = 3;
|
|
3957
|
+
function siteFromStack(stack) {
|
|
3958
|
+
if (stack === void 0) return "unknown";
|
|
3959
|
+
const frames = stack.split("\n").slice(1).map((line) => line.trim().replace(/^at\s+/, "")).filter((line) => line.length > 0).slice(0, CENSUS_STACK_FRAMES).map((line) => {
|
|
3960
|
+
const open = line.indexOf(" (");
|
|
3961
|
+
return (open > 0 ? line.slice(0, open) : line).replace(/^async\s+/, "");
|
|
3962
|
+
});
|
|
3963
|
+
return frames.length === 0 ? "unknown" : frames.join(" < ");
|
|
3964
|
+
}
|
|
3965
|
+
var FleetReadCensus = class FleetReadCensus {
|
|
3966
|
+
/**
|
|
3967
|
+
* Longest window the census may be armed for.
|
|
3968
|
+
*
|
|
3969
|
+
* Ten minutes covers a boot several times over and bounds what an operator
|
|
3970
|
+
* pays if they set the variable and forget it. A diagnostic that can be left
|
|
3971
|
+
* on becomes a tax nobody remembers enabling.
|
|
3972
|
+
*/
|
|
3973
|
+
static MAX_WINDOW_MS = 10 * 6e4;
|
|
3974
|
+
windowMs;
|
|
3975
|
+
closesAt;
|
|
3976
|
+
counts = /* @__PURE__ */ new Map();
|
|
3977
|
+
reported = false;
|
|
3978
|
+
constructor(rawEnv, nowMs) {
|
|
3979
|
+
const parsed = rawEnv === void 0 ? NaN : Number(rawEnv);
|
|
3980
|
+
const valid = Number.isFinite(parsed) && parsed > 0;
|
|
3981
|
+
this.windowMs = valid ? Math.min(parsed, FleetReadCensus.MAX_WINDOW_MS) : 0;
|
|
3982
|
+
this.closesAt = valid ? nowMs + this.windowMs : 0;
|
|
3983
|
+
}
|
|
3984
|
+
get armed() {
|
|
3985
|
+
return this.windowMs > 0;
|
|
3986
|
+
}
|
|
3987
|
+
/** Test seam: pretend the window closed. Production reads the clock. */
|
|
3988
|
+
closeAt(nowMs) {
|
|
3989
|
+
if (nowMs >= this.closesAt) this.forceClosed = true;
|
|
3990
|
+
}
|
|
3991
|
+
forceClosed = false;
|
|
3992
|
+
isOpen(nowMs) {
|
|
3993
|
+
if (!this.armed || this.forceClosed) return false;
|
|
3994
|
+
return nowMs < this.closesAt;
|
|
3995
|
+
}
|
|
3996
|
+
/**
|
|
3997
|
+
* Capture the caller, or `null` when the window is shut.
|
|
3998
|
+
*
|
|
3999
|
+
* Called at the START of a read. A read that began inside the window belongs
|
|
4000
|
+
* to it even if it lands after the close — otherwise the slow reads at the
|
|
4001
|
+
* edge would be attributed to nobody, and the slow ones are the interesting
|
|
4002
|
+
* ones.
|
|
4003
|
+
*/
|
|
4004
|
+
siteOf(nowMs = Date.now()) {
|
|
4005
|
+
if (!this.isOpen(nowMs)) return null;
|
|
4006
|
+
const holder = {};
|
|
4007
|
+
Error.captureStackTrace(holder, this.siteOf);
|
|
4008
|
+
return siteFromStack(holder.stack);
|
|
4009
|
+
}
|
|
4010
|
+
/** Count one read at an already-captured site. A `null` site is not counted. */
|
|
4011
|
+
record(site, rowCount) {
|
|
4012
|
+
if (site === null) return;
|
|
4013
|
+
const existing = this.counts.get(site);
|
|
4014
|
+
if (existing === void 0) {
|
|
4015
|
+
this.counts.set(site, {
|
|
4016
|
+
site,
|
|
4017
|
+
calls: 1,
|
|
4018
|
+
rows: rowCount
|
|
4019
|
+
});
|
|
4020
|
+
return;
|
|
4021
|
+
}
|
|
4022
|
+
existing.calls += 1;
|
|
4023
|
+
existing.rows += rowCount;
|
|
4024
|
+
}
|
|
4025
|
+
/** Attributed sites, busiest first. */
|
|
4026
|
+
rows() {
|
|
4027
|
+
return [...this.counts.values()].map((c) => ({
|
|
4028
|
+
site: c.site,
|
|
4029
|
+
calls: c.calls,
|
|
4030
|
+
rows: c.rows
|
|
4031
|
+
})).sort((a, b) => b.calls - a.calls);
|
|
4032
|
+
}
|
|
4033
|
+
/**
|
|
4034
|
+
* Emit the report ONCE.
|
|
4035
|
+
*
|
|
4036
|
+
* `perMin` is included because the raw count is unreadable without the window
|
|
4037
|
+
* it was taken over — "4 149" means nothing until you know it was 66 seconds.
|
|
4038
|
+
*/
|
|
4039
|
+
report(logger, elapsedMs) {
|
|
4040
|
+
if (this.reported || !this.armed) return;
|
|
4041
|
+
this.reported = true;
|
|
4042
|
+
const rows = this.rows();
|
|
4043
|
+
const totalCalls = rows.reduce((acc, r) => acc + r.calls, 0);
|
|
4044
|
+
logger.info("fleet read census", { meta: {
|
|
4045
|
+
elapsedMs,
|
|
4046
|
+
sites: rows.length,
|
|
4047
|
+
totalCalls,
|
|
4048
|
+
rows: rows.map((r) => ({
|
|
4049
|
+
site: r.site,
|
|
4050
|
+
calls: r.calls,
|
|
4051
|
+
rows: r.rows,
|
|
4052
|
+
perMin: elapsedMs === 0 ? 0 : Math.round(r.calls * 6e4 / elapsedMs)
|
|
4053
|
+
}))
|
|
4054
|
+
} });
|
|
4055
|
+
}
|
|
4056
|
+
};
|
|
4057
|
+
//#endregion
|
|
3952
4058
|
//#region src/builtins/device-manager/device-row-store.ts
|
|
3953
4059
|
/**
|
|
3954
4060
|
* @durable class=ledger owner=device-manager
|
|
@@ -4278,9 +4384,17 @@ var DeviceRowStore = class {
|
|
|
4278
4384
|
backend;
|
|
4279
4385
|
logger;
|
|
4280
4386
|
declared = null;
|
|
4281
|
-
|
|
4387
|
+
/** Call-site attribution for fleet reads. Armed only by
|
|
4388
|
+
* `CAMSTACK_FLEET_READ_CENSUS_MS`; see `fleet-read-census.ts`. */
|
|
4389
|
+
census;
|
|
4390
|
+
constructor(backend, logger, census) {
|
|
4282
4391
|
this.backend = backend;
|
|
4283
4392
|
this.logger = logger;
|
|
4393
|
+
this.census = census ?? new FleetReadCensus(process.env["CAMSTACK_FLEET_READ_CENSUS_MS"], Date.now());
|
|
4394
|
+
if (this.census.armed) {
|
|
4395
|
+
const startedAt = Date.now();
|
|
4396
|
+
setTimeout(() => this.census.report(this.logger, Date.now() - startedAt), this.census.windowMs).unref?.();
|
|
4397
|
+
}
|
|
4284
4398
|
}
|
|
4285
4399
|
/**
|
|
4286
4400
|
* Lazy, idempotent `declareCollection`, memoised on the PROMISE so N
|
|
@@ -4449,11 +4563,13 @@ var DeviceRowStore = class {
|
|
|
4449
4563
|
}
|
|
4450
4564
|
async list(filter, columns) {
|
|
4451
4565
|
await this.declare();
|
|
4566
|
+
const site = this.census.siteOf();
|
|
4452
4567
|
const records = await this.backend.query({
|
|
4453
4568
|
collection: DEVICE_ROWS_COLLECTION,
|
|
4454
4569
|
filter,
|
|
4455
4570
|
...columns === void 0 ? {} : { columns }
|
|
4456
4571
|
});
|
|
4572
|
+
this.census.record(site, records.length);
|
|
4457
4573
|
const out = [];
|
|
4458
4574
|
for (const record of records) {
|
|
4459
4575
|
const decoded = decodeDeviceRow(record.data);
|
|
@@ -3944,6 +3944,112 @@ var DeviceMetaStore = class {
|
|
|
3944
3944
|
};
|
|
3945
3945
|
};
|
|
3946
3946
|
//#endregion
|
|
3947
|
+
//#region src/builtins/device-manager/fleet-read-census.ts
|
|
3948
|
+
/** How many stack frames identify a call site. One frame is a method; three are
|
|
3949
|
+
* a cause — `listFleet < listAll < onDeviceRegistered` is the answer, `listAll`
|
|
3950
|
+
* alone is not. Same choice, for the same reason, as `read-census.ts`. */
|
|
3951
|
+
var CENSUS_STACK_FRAMES = 3;
|
|
3952
|
+
function siteFromStack(stack) {
|
|
3953
|
+
if (stack === void 0) return "unknown";
|
|
3954
|
+
const frames = stack.split("\n").slice(1).map((line) => line.trim().replace(/^at\s+/, "")).filter((line) => line.length > 0).slice(0, CENSUS_STACK_FRAMES).map((line) => {
|
|
3955
|
+
const open = line.indexOf(" (");
|
|
3956
|
+
return (open > 0 ? line.slice(0, open) : line).replace(/^async\s+/, "");
|
|
3957
|
+
});
|
|
3958
|
+
return frames.length === 0 ? "unknown" : frames.join(" < ");
|
|
3959
|
+
}
|
|
3960
|
+
var FleetReadCensus = class FleetReadCensus {
|
|
3961
|
+
/**
|
|
3962
|
+
* Longest window the census may be armed for.
|
|
3963
|
+
*
|
|
3964
|
+
* Ten minutes covers a boot several times over and bounds what an operator
|
|
3965
|
+
* pays if they set the variable and forget it. A diagnostic that can be left
|
|
3966
|
+
* on becomes a tax nobody remembers enabling.
|
|
3967
|
+
*/
|
|
3968
|
+
static MAX_WINDOW_MS = 10 * 6e4;
|
|
3969
|
+
windowMs;
|
|
3970
|
+
closesAt;
|
|
3971
|
+
counts = /* @__PURE__ */ new Map();
|
|
3972
|
+
reported = false;
|
|
3973
|
+
constructor(rawEnv, nowMs) {
|
|
3974
|
+
const parsed = rawEnv === void 0 ? NaN : Number(rawEnv);
|
|
3975
|
+
const valid = Number.isFinite(parsed) && parsed > 0;
|
|
3976
|
+
this.windowMs = valid ? Math.min(parsed, FleetReadCensus.MAX_WINDOW_MS) : 0;
|
|
3977
|
+
this.closesAt = valid ? nowMs + this.windowMs : 0;
|
|
3978
|
+
}
|
|
3979
|
+
get armed() {
|
|
3980
|
+
return this.windowMs > 0;
|
|
3981
|
+
}
|
|
3982
|
+
/** Test seam: pretend the window closed. Production reads the clock. */
|
|
3983
|
+
closeAt(nowMs) {
|
|
3984
|
+
if (nowMs >= this.closesAt) this.forceClosed = true;
|
|
3985
|
+
}
|
|
3986
|
+
forceClosed = false;
|
|
3987
|
+
isOpen(nowMs) {
|
|
3988
|
+
if (!this.armed || this.forceClosed) return false;
|
|
3989
|
+
return nowMs < this.closesAt;
|
|
3990
|
+
}
|
|
3991
|
+
/**
|
|
3992
|
+
* Capture the caller, or `null` when the window is shut.
|
|
3993
|
+
*
|
|
3994
|
+
* Called at the START of a read. A read that began inside the window belongs
|
|
3995
|
+
* to it even if it lands after the close — otherwise the slow reads at the
|
|
3996
|
+
* edge would be attributed to nobody, and the slow ones are the interesting
|
|
3997
|
+
* ones.
|
|
3998
|
+
*/
|
|
3999
|
+
siteOf(nowMs = Date.now()) {
|
|
4000
|
+
if (!this.isOpen(nowMs)) return null;
|
|
4001
|
+
const holder = {};
|
|
4002
|
+
Error.captureStackTrace(holder, this.siteOf);
|
|
4003
|
+
return siteFromStack(holder.stack);
|
|
4004
|
+
}
|
|
4005
|
+
/** Count one read at an already-captured site. A `null` site is not counted. */
|
|
4006
|
+
record(site, rowCount) {
|
|
4007
|
+
if (site === null) return;
|
|
4008
|
+
const existing = this.counts.get(site);
|
|
4009
|
+
if (existing === void 0) {
|
|
4010
|
+
this.counts.set(site, {
|
|
4011
|
+
site,
|
|
4012
|
+
calls: 1,
|
|
4013
|
+
rows: rowCount
|
|
4014
|
+
});
|
|
4015
|
+
return;
|
|
4016
|
+
}
|
|
4017
|
+
existing.calls += 1;
|
|
4018
|
+
existing.rows += rowCount;
|
|
4019
|
+
}
|
|
4020
|
+
/** Attributed sites, busiest first. */
|
|
4021
|
+
rows() {
|
|
4022
|
+
return [...this.counts.values()].map((c) => ({
|
|
4023
|
+
site: c.site,
|
|
4024
|
+
calls: c.calls,
|
|
4025
|
+
rows: c.rows
|
|
4026
|
+
})).sort((a, b) => b.calls - a.calls);
|
|
4027
|
+
}
|
|
4028
|
+
/**
|
|
4029
|
+
* Emit the report ONCE.
|
|
4030
|
+
*
|
|
4031
|
+
* `perMin` is included because the raw count is unreadable without the window
|
|
4032
|
+
* it was taken over — "4 149" means nothing until you know it was 66 seconds.
|
|
4033
|
+
*/
|
|
4034
|
+
report(logger, elapsedMs) {
|
|
4035
|
+
if (this.reported || !this.armed) return;
|
|
4036
|
+
this.reported = true;
|
|
4037
|
+
const rows = this.rows();
|
|
4038
|
+
const totalCalls = rows.reduce((acc, r) => acc + r.calls, 0);
|
|
4039
|
+
logger.info("fleet read census", { meta: {
|
|
4040
|
+
elapsedMs,
|
|
4041
|
+
sites: rows.length,
|
|
4042
|
+
totalCalls,
|
|
4043
|
+
rows: rows.map((r) => ({
|
|
4044
|
+
site: r.site,
|
|
4045
|
+
calls: r.calls,
|
|
4046
|
+
rows: r.rows,
|
|
4047
|
+
perMin: elapsedMs === 0 ? 0 : Math.round(r.calls * 6e4 / elapsedMs)
|
|
4048
|
+
}))
|
|
4049
|
+
} });
|
|
4050
|
+
}
|
|
4051
|
+
};
|
|
4052
|
+
//#endregion
|
|
3947
4053
|
//#region src/builtins/device-manager/device-row-store.ts
|
|
3948
4054
|
/**
|
|
3949
4055
|
* @durable class=ledger owner=device-manager
|
|
@@ -4273,9 +4379,17 @@ var DeviceRowStore = class {
|
|
|
4273
4379
|
backend;
|
|
4274
4380
|
logger;
|
|
4275
4381
|
declared = null;
|
|
4276
|
-
|
|
4382
|
+
/** Call-site attribution for fleet reads. Armed only by
|
|
4383
|
+
* `CAMSTACK_FLEET_READ_CENSUS_MS`; see `fleet-read-census.ts`. */
|
|
4384
|
+
census;
|
|
4385
|
+
constructor(backend, logger, census) {
|
|
4277
4386
|
this.backend = backend;
|
|
4278
4387
|
this.logger = logger;
|
|
4388
|
+
this.census = census ?? new FleetReadCensus(process.env["CAMSTACK_FLEET_READ_CENSUS_MS"], Date.now());
|
|
4389
|
+
if (this.census.armed) {
|
|
4390
|
+
const startedAt = Date.now();
|
|
4391
|
+
setTimeout(() => this.census.report(this.logger, Date.now() - startedAt), this.census.windowMs).unref?.();
|
|
4392
|
+
}
|
|
4279
4393
|
}
|
|
4280
4394
|
/**
|
|
4281
4395
|
* Lazy, idempotent `declareCollection`, memoised on the PROMISE so N
|
|
@@ -4444,11 +4558,13 @@ var DeviceRowStore = class {
|
|
|
4444
4558
|
}
|
|
4445
4559
|
async list(filter, columns) {
|
|
4446
4560
|
await this.declare();
|
|
4561
|
+
const site = this.census.siteOf();
|
|
4447
4562
|
const records = await this.backend.query({
|
|
4448
4563
|
collection: DEVICE_ROWS_COLLECTION,
|
|
4449
4564
|
filter,
|
|
4450
4565
|
...columns === void 0 ? {} : { columns }
|
|
4451
4566
|
});
|
|
4567
|
+
this.census.record(site, records.length);
|
|
4452
4568
|
const out = [];
|
|
4453
4569
|
for (const record of records) {
|
|
4454
4570
|
const decoded = decodeDeviceRow(record.data);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ChildLayout, CollectionColumn, CollectionIndex, DeviceDisplayOverride, IScopedLogger, MutationFilter, QueryFilter, SettingsRecord, SettingsStoreClient } from '@camstack/types';
|
|
2
2
|
import { RetiredRowStore } from '../sqlite-storage/retired-settings-keys.js';
|
|
3
3
|
import { PersistedDeviceMeta } from './device-meta-types.js';
|
|
4
|
+
import { FleetReadCensus } from './fleet-read-census.js';
|
|
4
5
|
/**
|
|
5
6
|
* @durable class=ledger owner=device-manager
|
|
6
7
|
* write="one row per device, written by `allocateDeviceId` (identity placeholder),
|
|
@@ -211,7 +212,10 @@ export declare class DeviceRowStore {
|
|
|
211
212
|
private readonly backend;
|
|
212
213
|
private readonly logger;
|
|
213
214
|
private declared;
|
|
214
|
-
|
|
215
|
+
/** Call-site attribution for fleet reads. Armed only by
|
|
216
|
+
* `CAMSTACK_FLEET_READ_CENSUS_MS`; see `fleet-read-census.ts`. */
|
|
217
|
+
private readonly census;
|
|
218
|
+
constructor(backend: DeviceRowBackend, logger: IScopedLogger, census?: FleetReadCensus);
|
|
215
219
|
/**
|
|
216
220
|
* Lazy, idempotent `declareCollection`, memoised on the PROMISE so N
|
|
217
221
|
* concurrent first-callers issue one declaration rather than N. A rejection
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { IScopedLogger } from '@camstack/types';
|
|
2
|
+
/** One attributed call site. */
|
|
3
|
+
export interface FleetCensusRow {
|
|
4
|
+
readonly site: string;
|
|
5
|
+
readonly calls: number;
|
|
6
|
+
readonly rows: number;
|
|
7
|
+
}
|
|
8
|
+
export declare class FleetReadCensus {
|
|
9
|
+
/**
|
|
10
|
+
* Longest window the census may be armed for.
|
|
11
|
+
*
|
|
12
|
+
* Ten minutes covers a boot several times over and bounds what an operator
|
|
13
|
+
* pays if they set the variable and forget it. A diagnostic that can be left
|
|
14
|
+
* on becomes a tax nobody remembers enabling.
|
|
15
|
+
*/
|
|
16
|
+
static readonly MAX_WINDOW_MS: number;
|
|
17
|
+
readonly windowMs: number;
|
|
18
|
+
private readonly closesAt;
|
|
19
|
+
private readonly counts;
|
|
20
|
+
private reported;
|
|
21
|
+
constructor(rawEnv: string | undefined, nowMs: number);
|
|
22
|
+
get armed(): boolean;
|
|
23
|
+
/** Test seam: pretend the window closed. Production reads the clock. */
|
|
24
|
+
closeAt(nowMs: number): void;
|
|
25
|
+
private forceClosed;
|
|
26
|
+
private isOpen;
|
|
27
|
+
/**
|
|
28
|
+
* Capture the caller, or `null` when the window is shut.
|
|
29
|
+
*
|
|
30
|
+
* Called at the START of a read. A read that began inside the window belongs
|
|
31
|
+
* to it even if it lands after the close — otherwise the slow reads at the
|
|
32
|
+
* edge would be attributed to nobody, and the slow ones are the interesting
|
|
33
|
+
* ones.
|
|
34
|
+
*/
|
|
35
|
+
siteOf(nowMs?: number): string | null;
|
|
36
|
+
/** Count one read at an already-captured site. A `null` site is not counted. */
|
|
37
|
+
record(site: string | null, rowCount: number): void;
|
|
38
|
+
/** Attributed sites, busiest first. */
|
|
39
|
+
rows(): readonly FleetCensusRow[];
|
|
40
|
+
/**
|
|
41
|
+
* Emit the report ONCE.
|
|
42
|
+
*
|
|
43
|
+
* `perMin` is included because the raw count is unreadable without the window
|
|
44
|
+
* it was taken over — "4 149" means nothing until you know it was 66 seconds.
|
|
45
|
+
*/
|
|
46
|
+
report(logger: IScopedLogger, elapsedMs: number): void;
|
|
47
|
+
}
|