@rocicorp/zero 1.11.0-canary.22 → 1.11.0-canary.23
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/out/zero/package.js +1 -1
- package/out/zero/package.js.map +1 -1
- package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
- package/out/zero-cache/src/server/change-streamer.js +87 -73
- package/out/zero-cache/src/server/change-streamer.js.map +1 -1
- package/out/zero-cache/src/services/change-source/common/init-cleanup.d.ts +24 -0
- package/out/zero-cache/src/services/change-source/common/init-cleanup.d.ts.map +1 -0
- package/out/zero-cache/src/services/change-source/common/init-cleanup.js +44 -0
- package/out/zero-cache/src/services/change-source/common/init-cleanup.js.map +1 -0
- package/out/zero-cache/src/services/change-source/common/replica-restore.d.ts +18 -0
- package/out/zero-cache/src/services/change-source/common/replica-restore.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/common/replica-restore.js.map +1 -1
- package/out/zero-cache/src/services/change-source/custom/change-source.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/custom/change-source.js +2 -1
- package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source-init.d.ts +6 -0
- package/out/zero-cache/src/services/change-source/pg/change-source-init.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/change-source-init.js +26 -11
- package/out/zero-cache/src/services/change-source/pg/change-source-init.js.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts +2 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts.map +1 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.js +3 -1
- package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.d.ts +5 -0
- package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
- package/out/zero-cache/src/services/litestream/commands.js +27 -6
- package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
- package/out/zero-cache/src/services/profz.d.ts.map +1 -1
- package/out/zero-cache/src/services/profz.js +19 -1
- package/out/zero-cache/src/services/profz.js.map +1 -1
- package/out/zero-cache/src/services/replicator/replication-status.js +1 -1
- package/out/zero-cache/src/services/replicator/replication-status.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/advancement-metrics.d.ts +26 -0
- package/out/zero-cache/src/services/view-syncer/advancement-metrics.d.ts.map +1 -0
- package/out/zero-cache/src/services/view-syncer/advancement-metrics.js +26 -0
- package/out/zero-cache/src/services/view-syncer/advancement-metrics.js.map +1 -0
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts +10 -0
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js +57 -15
- package/out/zero-cache/src/services/view-syncer/pipeline-driver.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts +8 -9
- package/out/zero-cache/src/services/view-syncer/snapshotter.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/snapshotter.js +15 -3
- package/out/zero-cache/src/services/view-syncer/snapshotter.js.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
- package/out/zero-cache/src/services/view-syncer/view-syncer.js +50 -28
- package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
- package/out/zero-client/src/client/version.js +1 -1
- package/out/zero-protocol/src/down.d.ts +27 -27
- package/out/zqlite/src/db.d.ts.map +1 -1
- package/out/zqlite/src/db.js +34 -6
- package/out/zqlite/src/db.js.map +1 -1
- package/package.json +1 -1
|
@@ -48,7 +48,25 @@ async function handleProfzRequest(lc, config, req, res, getWorker, forcedWorker,
|
|
|
48
48
|
worker: targetWorker === localProcessName ? "__none__" : targetWorker,
|
|
49
49
|
workerIndex: targetWorkerIndex
|
|
50
50
|
}]);
|
|
51
|
-
|
|
51
|
+
const graceMs = params.getInteger("graceMs", false) ?? params.getInteger("timeout", false) ?? 5e3;
|
|
52
|
+
await sleep(durationMs);
|
|
53
|
+
const pollMs = 50;
|
|
54
|
+
let waited = 0;
|
|
55
|
+
let lastCount = responses.size;
|
|
56
|
+
let quietMs = 0;
|
|
57
|
+
while (waited < graceMs) {
|
|
58
|
+
await sleep(pollMs);
|
|
59
|
+
waited += pollMs;
|
|
60
|
+
if (targetWorker && targetWorker !== "all") {
|
|
61
|
+
if ([...responses.keys()].some((name) => name === targetWorker || name.startsWith(`${targetWorker}-`))) break;
|
|
62
|
+
} else if (responses.size !== lastCount) {
|
|
63
|
+
lastCount = responses.size;
|
|
64
|
+
quietMs = 0;
|
|
65
|
+
} else if (responses.size > 0) {
|
|
66
|
+
quietMs += pollMs;
|
|
67
|
+
if (quietMs >= 400) break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
52
70
|
} finally {
|
|
53
71
|
unsubscribe();
|
|
54
72
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profz.js","names":[],"sources":["../../../../../zero-cache/src/services/profz.ts"],"sourcesContent":["import {randomUUID} from 'node:crypto';\nimport type {Profiler} from 'node:inspector';\nimport type {LogContext} from '@rocicorp/logger';\nimport auth from 'basic-auth';\nimport type {FastifyReply, FastifyRequest} from 'fastify';\nimport {sleep} from '../../../shared/src/sleep.ts';\nimport type {NormalizedZeroConfig} from '../config/normalize.ts';\nimport {getOperatorAccess} from '../config/zero-config.ts';\nimport {\n singleProcessMode,\n subscribeToMessageType,\n type ProfileResponse,\n type ProfileResponseMessage,\n type Worker,\n} from '../types/processes.ts';\nimport {CpuProfiler} from '../types/profiler.ts';\nimport {URLParams} from '../types/url-params.ts';\n\n/**\n * Serves CPU profiles. This is available to the operator password as well as\n * the admin password (see `--operator-password`), so it must never serve\n * application data to `operator` access.\n *\n * A V8 CPU profile describes code, not data: function names, script URLs and\n * positions, and sample timings. The exception is a regular expression, which\n * V8 names after its source (`RegExp: <source>`). zero-cache compiles `LIKE`\n * and `ILIKE` patterns from client queries into regular expressions, so their\n * sources are redacted for `operator` access. See {@link redactProfile}.\n */\nexport async function handleProfzRequest(\n lc: LogContext,\n config: Pick<NormalizedZeroConfig, 'adminPassword' | 'operatorPassword'>,\n req: FastifyRequest,\n res: FastifyReply,\n getWorker?: (() => Promise<Worker>) | undefined,\n forcedWorker?: string | undefined,\n localProcessName: string = 'dispatcher',\n): Promise<void> {\n const credentials = auth(req);\n const access = getOperatorAccess(lc, config, credentials?.pass);\n if (access === undefined) {\n void res\n .code(401)\n .header('WWW-Authenticate', 'Basic realm=\"Profz Protected Area\"')\n .send('Unauthorized');\n return;\n }\n\n const params = new URLParams(new URL(req.url, 'http://localhost'));\n const rawDuration =\n params.getInteger('duration', false) ??\n params.getInteger('seconds', false) ??\n 5;\n const durationSec = Math.max(1, Math.min(60, rawDuration));\n const durationMs = durationSec * 1000;\n const targetWorker = forcedWorker ?? params.get('worker', false) ?? undefined;\n const targetWorkerIndex = params.getInteger('index', false) ?? undefined;\n const id = randomUUID();\n\n const responses = new Map<string, unknown>();\n\n // Profile dispatcher / local process if requested, when profiling all processes,\n // or when no worker dispatcher is available (single process / standalone mode)\n const shouldProfileLocal =\n singleProcessMode() ||\n getWorker === undefined ||\n targetWorker === undefined ||\n targetWorker === localProcessName;\n\n const localProfilePromise = shouldProfileLocal\n ? CpuProfiler.profile(durationMs).catch(err => {\n lc.warn?.('Failed to capture local CPU profile:', err);\n return null;\n })\n : Promise.resolve(null);\n\n if (!singleProcessMode() && getWorker !== undefined) {\n try {\n const worker = await getWorker();\n const onMessage = (msg: ProfileResponse) => {\n if (msg.id === id && msg.profile) {\n responses.set(msg.name, msg.profile);\n } else if (msg.id === id && msg.error) {\n lc.warn?.(`Worker ${msg.name} profile error: ${msg.error}`);\n }\n };\n\n // The worker is long-lived (and shared across requests), so the\n // per-request handler must be removed once this request is done.\n const unsubscribe = subscribeToMessageType<ProfileResponseMessage>(\n worker,\n 'profileResponse',\n onMessage,\n );\n try {\n worker.send([\n 'profile',\n {\n id,\n durationMs,\n worker:\n targetWorker === localProcessName ? '__none__' : targetWorker,\n workerIndex: targetWorkerIndex,\n },\n ]);\n\n // Wait for duration plus grace period for IPC transfer\n await sleep(durationMs + 500);\n } finally {\n unsubscribe();\n }\n } catch (err) {\n lc.warn?.('Failed to dispatch profile request to child workers:', err);\n }\n }\n\n const localProfile = await localProfilePromise;\n if (localProfile) {\n const localName =\n targetWorker ?? (singleProcessMode() ? 'zero-cache' : localProcessName);\n responses.set(localName, localProfile);\n }\n\n if (access === 'operator') {\n for (const [name, prof] of responses) {\n responses.set(name, redactProfile(prof));\n }\n }\n\n // If a single specific worker was requested, return that profile directly\n if (targetWorker !== undefined && targetWorker !== 'all') {\n let matchedProfile = responses.get(targetWorker);\n if (!matchedProfile) {\n // Try matching by prefix or substring (e.g. 'syncer' matches 'syncer-0')\n for (const [name, prof] of responses.entries()) {\n if (name === targetWorker || name.startsWith(`${targetWorker}-`)) {\n matchedProfile = prof;\n break;\n }\n }\n }\n\n if (matchedProfile) {\n void res\n .header('Content-Type', 'application/json')\n .header(\n 'Content-Disposition',\n `attachment; filename=\"${targetWorker}.cpuprofile\"`,\n )\n .send(matchedProfile);\n return;\n }\n\n void res.code(404).send({\n error: `No profile captured for worker \"${targetWorker}\". Available: ${[...responses.keys()].join(', ')}`,\n });\n return;\n }\n\n // Default (Option A): Return multi-process profile bundle as a JSON map\n const bundle: Record<string, unknown> = {};\n for (const [name, prof] of responses.entries()) {\n bundle[name] = prof;\n }\n\n void res\n .header('Content-Type', 'application/json')\n .header(\n 'Content-Disposition',\n `attachment; filename=\"zero-profile-bundle-${Date.now()}.json\"`,\n )\n .send(bundle);\n}\n\nexport async function handleProfrmzRequest(\n lc: LogContext,\n config: NormalizedZeroConfig,\n req: FastifyRequest,\n res: FastifyReply,\n getWorker?: (() => Promise<Worker>) | undefined,\n): Promise<void> {\n const credentials = auth(req);\n if (getOperatorAccess(lc, config, credentials?.pass) === undefined) {\n void res\n .code(401)\n .header('WWW-Authenticate', 'Basic realm=\"Profrmz Protected Area\"')\n .send('Unauthorized');\n return;\n }\n\n // In distributed mode, proxy to the upstream Replication Manager. The\n // credentials are forwarded, and the Replication Manager's /profz decides\n // for itself what access they grant (and so what to redact).\n if (config.changeStreamer.uri) {\n const upstreamURL = new URL('/profz', config.changeStreamer.uri);\n if (upstreamURL.protocol === 'ws:') {\n upstreamURL.protocol = 'http:';\n } else if (upstreamURL.protocol === 'wss:') {\n upstreamURL.protocol = 'https:';\n }\n const incomingURL = new URL(req.url, 'http://localhost');\n for (const [key, value] of incomingURL.searchParams.entries()) {\n upstreamURL.searchParams.set(key, value);\n }\n\n try {\n const headers: Record<string, string> = {};\n if (req.headers.authorization) {\n headers.authorization = req.headers.authorization;\n }\n const response = await fetch(upstreamURL.toString(), {\n method: 'GET',\n headers,\n });\n\n res.code(response.status);\n const contentType = response.headers.get('content-type');\n if (contentType) {\n res.header('content-type', contentType);\n }\n const contentDisposition = response.headers.get('content-disposition');\n if (contentDisposition) {\n res.header('content-disposition', contentDisposition);\n }\n\n const body = await response.text();\n return res.send(body);\n } catch (err) {\n lc.error?.(`Failed to proxy /profrmz to RM at ${upstreamURL}:`, err);\n return res.code(502).send({\n error: `Failed to proxy profiling request to RM at ${upstreamURL}: ${String(err)}`,\n });\n }\n }\n\n // In single-node mode, profile local change-streamer / RM process\n return handleProfzRequest(lc, config, req, res, getWorker, 'change-streamer');\n}\n\n// V8 names the code for a regular expression `RegExp: <source>`.\nconst REGEXP_NAME_PREFIX = 'RegExp:';\nconst REDACTED_REGEXP_NAME = 'RegExp: <redacted>';\n\n/**\n * Returns a copy of the V8 CPU `profile` with the source of every regular\n * expression replaced, since a pattern built at runtime can contain\n * application data (e.g. a `LIKE` pattern from a client query). The time spent\n * in regular expressions is still attributed to them.\n *\n * A `profile` that is not in the expected format is withheld rather than\n * served unredacted.\n *\n * @visibleForTesting\n */\nexport function redactProfile(profile: unknown): unknown {\n const {nodes} = (profile ?? {}) as Partial<Profiler.Profile>;\n if (!Array.isArray(nodes)) {\n return {error: 'Profile withheld: unrecognized format'};\n }\n return {\n ...(profile as Profiler.Profile),\n nodes: nodes.map(node =>\n node.callFrame?.functionName?.startsWith(REGEXP_NAME_PREFIX)\n ? {\n ...node,\n callFrame: {...node.callFrame, functionName: REDACTED_REGEXP_NAME},\n }\n : node,\n ),\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"profz.js","names":[],"sources":["../../../../../zero-cache/src/services/profz.ts"],"sourcesContent":["import {randomUUID} from 'node:crypto';\nimport type {Profiler} from 'node:inspector';\nimport type {LogContext} from '@rocicorp/logger';\nimport auth from 'basic-auth';\nimport type {FastifyReply, FastifyRequest} from 'fastify';\nimport {sleep} from '../../../shared/src/sleep.ts';\nimport type {NormalizedZeroConfig} from '../config/normalize.ts';\nimport {getOperatorAccess} from '../config/zero-config.ts';\nimport {\n singleProcessMode,\n subscribeToMessageType,\n type ProfileResponse,\n type ProfileResponseMessage,\n type Worker,\n} from '../types/processes.ts';\nimport {CpuProfiler} from '../types/profiler.ts';\nimport {URLParams} from '../types/url-params.ts';\n\n/**\n * Serves CPU profiles. This is available to the operator password as well as\n * the admin password (see `--operator-password`), so it must never serve\n * application data to `operator` access.\n *\n * A V8 CPU profile describes code, not data: function names, script URLs and\n * positions, and sample timings. The exception is a regular expression, which\n * V8 names after its source (`RegExp: <source>`). zero-cache compiles `LIKE`\n * and `ILIKE` patterns from client queries into regular expressions, so their\n * sources are redacted for `operator` access. See {@link redactProfile}.\n */\nexport async function handleProfzRequest(\n lc: LogContext,\n config: Pick<NormalizedZeroConfig, 'adminPassword' | 'operatorPassword'>,\n req: FastifyRequest,\n res: FastifyReply,\n getWorker?: (() => Promise<Worker>) | undefined,\n forcedWorker?: string | undefined,\n localProcessName: string = 'dispatcher',\n): Promise<void> {\n const credentials = auth(req);\n const access = getOperatorAccess(lc, config, credentials?.pass);\n if (access === undefined) {\n void res\n .code(401)\n .header('WWW-Authenticate', 'Basic realm=\"Profz Protected Area\"')\n .send('Unauthorized');\n return;\n }\n\n const params = new URLParams(new URL(req.url, 'http://localhost'));\n const rawDuration =\n params.getInteger('duration', false) ??\n params.getInteger('seconds', false) ??\n 5;\n const durationSec = Math.max(1, Math.min(60, rawDuration));\n const durationMs = durationSec * 1000;\n const targetWorker = forcedWorker ?? params.get('worker', false) ?? undefined;\n const targetWorkerIndex = params.getInteger('index', false) ?? undefined;\n const id = randomUUID();\n\n const responses = new Map<string, unknown>();\n\n // Profile dispatcher / local process if requested, when profiling all processes,\n // or when no worker dispatcher is available (single process / standalone mode)\n const shouldProfileLocal =\n singleProcessMode() ||\n getWorker === undefined ||\n targetWorker === undefined ||\n targetWorker === localProcessName;\n\n const localProfilePromise = shouldProfileLocal\n ? CpuProfiler.profile(durationMs).catch(err => {\n lc.warn?.('Failed to capture local CPU profile:', err);\n return null;\n })\n : Promise.resolve(null);\n\n if (!singleProcessMode() && getWorker !== undefined) {\n try {\n const worker = await getWorker();\n const onMessage = (msg: ProfileResponse) => {\n if (msg.id === id && msg.profile) {\n responses.set(msg.name, msg.profile);\n } else if (msg.id === id && msg.error) {\n lc.warn?.(`Worker ${msg.name} profile error: ${msg.error}`);\n }\n };\n\n // The worker is long-lived (and shared across requests), so the\n // per-request handler must be removed once this request is done.\n const unsubscribe = subscribeToMessageType<ProfileResponseMessage>(\n worker,\n 'profileResponse',\n onMessage,\n );\n try {\n worker.send([\n 'profile',\n {\n id,\n durationMs,\n worker:\n targetWorker === localProcessName ? '__none__' : targetWorker,\n workerIndex: targetWorkerIndex,\n },\n ]);\n\n // Wait for profile duration, then wait for child workers to transfer\n // profiles over IPC. Under heavy production load, serializing and sending\n // large CPU profiles can take several seconds.\n const graceMs =\n params.getInteger('graceMs', false) ??\n params.getInteger('timeout', false) ??\n 5000;\n await sleep(durationMs);\n\n const pollMs = 50;\n let waited = 0;\n let lastCount = responses.size;\n let quietMs = 0;\n while (waited < graceMs) {\n await sleep(pollMs);\n waited += pollMs;\n\n if (targetWorker && targetWorker !== 'all') {\n const hasTarget = [...responses.keys()].some(\n name =>\n name === targetWorker || name.startsWith(`${targetWorker}-`),\n );\n if (hasTarget) {\n break;\n }\n } else if (responses.size !== lastCount) {\n lastCount = responses.size;\n quietMs = 0;\n } else if (responses.size > 0) {\n quietMs += pollMs;\n if (quietMs >= 400) {\n break;\n }\n }\n }\n } finally {\n unsubscribe();\n }\n } catch (err) {\n lc.warn?.('Failed to dispatch profile request to child workers:', err);\n }\n }\n\n const localProfile = await localProfilePromise;\n if (localProfile) {\n const localName =\n targetWorker ?? (singleProcessMode() ? 'zero-cache' : localProcessName);\n responses.set(localName, localProfile);\n }\n\n if (access === 'operator') {\n for (const [name, prof] of responses) {\n responses.set(name, redactProfile(prof));\n }\n }\n\n // If a single specific worker was requested, return that profile directly\n if (targetWorker !== undefined && targetWorker !== 'all') {\n let matchedProfile = responses.get(targetWorker);\n if (!matchedProfile) {\n // Try matching by prefix or substring (e.g. 'syncer' matches 'syncer-0')\n for (const [name, prof] of responses.entries()) {\n if (name === targetWorker || name.startsWith(`${targetWorker}-`)) {\n matchedProfile = prof;\n break;\n }\n }\n }\n\n if (matchedProfile) {\n void res\n .header('Content-Type', 'application/json')\n .header(\n 'Content-Disposition',\n `attachment; filename=\"${targetWorker}.cpuprofile\"`,\n )\n .send(matchedProfile);\n return;\n }\n\n void res.code(404).send({\n error: `No profile captured for worker \"${targetWorker}\". Available: ${[...responses.keys()].join(', ')}`,\n });\n return;\n }\n\n // Default (Option A): Return multi-process profile bundle as a JSON map\n const bundle: Record<string, unknown> = {};\n for (const [name, prof] of responses.entries()) {\n bundle[name] = prof;\n }\n\n void res\n .header('Content-Type', 'application/json')\n .header(\n 'Content-Disposition',\n `attachment; filename=\"zero-profile-bundle-${Date.now()}.json\"`,\n )\n .send(bundle);\n}\n\nexport async function handleProfrmzRequest(\n lc: LogContext,\n config: NormalizedZeroConfig,\n req: FastifyRequest,\n res: FastifyReply,\n getWorker?: (() => Promise<Worker>) | undefined,\n): Promise<void> {\n const credentials = auth(req);\n if (getOperatorAccess(lc, config, credentials?.pass) === undefined) {\n void res\n .code(401)\n .header('WWW-Authenticate', 'Basic realm=\"Profrmz Protected Area\"')\n .send('Unauthorized');\n return;\n }\n\n // In distributed mode, proxy to the upstream Replication Manager. The\n // credentials are forwarded, and the Replication Manager's /profz decides\n // for itself what access they grant (and so what to redact).\n if (config.changeStreamer.uri) {\n const upstreamURL = new URL('/profz', config.changeStreamer.uri);\n if (upstreamURL.protocol === 'ws:') {\n upstreamURL.protocol = 'http:';\n } else if (upstreamURL.protocol === 'wss:') {\n upstreamURL.protocol = 'https:';\n }\n const incomingURL = new URL(req.url, 'http://localhost');\n for (const [key, value] of incomingURL.searchParams.entries()) {\n upstreamURL.searchParams.set(key, value);\n }\n\n try {\n const headers: Record<string, string> = {};\n if (req.headers.authorization) {\n headers.authorization = req.headers.authorization;\n }\n const response = await fetch(upstreamURL.toString(), {\n method: 'GET',\n headers,\n });\n\n res.code(response.status);\n const contentType = response.headers.get('content-type');\n if (contentType) {\n res.header('content-type', contentType);\n }\n const contentDisposition = response.headers.get('content-disposition');\n if (contentDisposition) {\n res.header('content-disposition', contentDisposition);\n }\n\n const body = await response.text();\n return res.send(body);\n } catch (err) {\n lc.error?.(`Failed to proxy /profrmz to RM at ${upstreamURL}:`, err);\n return res.code(502).send({\n error: `Failed to proxy profiling request to RM at ${upstreamURL}: ${String(err)}`,\n });\n }\n }\n\n // In single-node mode, profile local change-streamer / RM process\n return handleProfzRequest(lc, config, req, res, getWorker, 'change-streamer');\n}\n\n// V8 names the code for a regular expression `RegExp: <source>`.\nconst REGEXP_NAME_PREFIX = 'RegExp:';\nconst REDACTED_REGEXP_NAME = 'RegExp: <redacted>';\n\n/**\n * Returns a copy of the V8 CPU `profile` with the source of every regular\n * expression replaced, since a pattern built at runtime can contain\n * application data (e.g. a `LIKE` pattern from a client query). The time spent\n * in regular expressions is still attributed to them.\n *\n * A `profile` that is not in the expected format is withheld rather than\n * served unredacted.\n *\n * @visibleForTesting\n */\nexport function redactProfile(profile: unknown): unknown {\n const {nodes} = (profile ?? {}) as Partial<Profiler.Profile>;\n if (!Array.isArray(nodes)) {\n return {error: 'Profile withheld: unrecognized format'};\n }\n return {\n ...(profile as Profiler.Profile),\n nodes: nodes.map(node =>\n node.callFrame?.functionName?.startsWith(REGEXP_NAME_PREFIX)\n ? {\n ...node,\n callFrame: {...node.callFrame, functionName: REDACTED_REGEXP_NAME},\n }\n : node,\n ),\n };\n}\n"],"mappings":"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"}
|
|
@@ -208,7 +208,7 @@ function getReplicatedIndexes(db) {
|
|
|
208
208
|
return listIndexes(db).map(({ tableName: table, columns, unique }) => ({
|
|
209
209
|
table,
|
|
210
210
|
unique,
|
|
211
|
-
columns: Object.entries(columns).
|
|
211
|
+
columns: Object.entries(columns).map(([column, dir]) => ({
|
|
212
212
|
column,
|
|
213
213
|
dir
|
|
214
214
|
}))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replication-status.js","names":["#dbRunner","#publishFn","#publish","#timer","#totalIndexes","#now","#count","#current","#completedMs"],"sources":["../../../../../../zero-cache/src/services/replicator/replication-status.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {createSilentLogContext} from '../../../../shared/src/logging-test-utils.ts';\nimport type {JSONObject} from '../../../../zero-events/src/json.ts';\nimport type {\n IndexingStatus,\n ReplicatedIndex,\n ReplicatedTable,\n ReplicationStage,\n ReplicationState,\n ReplicationStatusEvent,\n Status,\n} from '../../../../zero-events/src/status.ts';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport {computeZqlSpecs, listIndexes} from '../../db/lite-tables.ts';\nimport type {LiteIndexSpec, LiteTableSpec} from '../../db/specs.ts';\nimport {\n makeErrorDetails,\n publishCriticalEvent,\n} from '../../observability/events.ts';\n\nconst byKeys = (a: [string, unknown], b: [string, unknown]) =>\n a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;\n\nexport class ReplicationStatusPublisher {\n readonly #dbRunner: <T>(lc: LogContext, fn: (db: Database) => T) => T;\n readonly #publishFn: typeof publishCriticalEvent;\n #timer: NodeJS.Timeout | undefined;\n\n static forTesting() {\n return ReplicationStatusPublisher.forReplicaFile(':memory:');\n }\n\n static forRunningTransaction(tx: Database, publishFn = publishCriticalEvent) {\n return new ReplicationStatusPublisher((_lc, fn) => fn(tx), publishFn);\n }\n\n static forReplicaFile(file: string, publishFn = publishCriticalEvent) {\n return new ReplicationStatusPublisher((lc, fn) => {\n const db = new Database(lc, file, {readonly: true});\n try {\n return fn(db);\n } finally {\n db.close();\n }\n }, publishFn);\n }\n\n constructor(\n dbRunner: <T>(lc: LogContext, fn: (db: Database) => T) => T,\n publishFn = publishCriticalEvent,\n ) {\n this.#dbRunner = dbRunner;\n this.#publishFn = publishFn;\n }\n\n publish(\n lc: LogContext,\n stage: ReplicationStage,\n description?: string,\n interval = 0,\n extraState?: () => Partial<ReplicationState>,\n now = new Date(),\n ): this {\n void this.#publish(lc, stage, description, interval, extraState, now);\n return this;\n }\n\n /**\n * Like {@link publish}, but waits (for at most `maxWaitMs`) for the event\n * to be sent. Use this before a long synchronous operation (e.g. creating\n * an index) that blocks the event loop and would otherwise prevent the\n * event from being sent until the operation completes.\n */\n async publishAndFlush(\n lc: LogContext,\n stage: ReplicationStage,\n description?: string,\n interval = 0,\n extraState?: () => Partial<ReplicationState>,\n maxWaitMs = 1000,\n ): Promise<void> {\n const published = this.#publish(\n lc,\n stage,\n description,\n interval,\n extraState,\n );\n let timer: NodeJS.Timeout | undefined;\n try {\n await Promise.race([\n published,\n new Promise<void>(resolve => {\n timer = setTimeout(resolve, maxWaitMs);\n }),\n ]);\n } finally {\n clearTimeout(timer);\n }\n }\n\n #publish(\n lc: LogContext,\n stage: ReplicationStage,\n description: string | undefined,\n interval: number,\n extraState: (() => Partial<ReplicationState>) | undefined,\n now = new Date(),\n ): Promise<void> {\n this.stop();\n\n const event = this.#dbRunner(lc, db =>\n replicationStatusEvent(lc, db, stage, 'OK', description, now),\n );\n if (event.state) {\n event.state = {\n ...event.state,\n ...extraState?.(),\n };\n }\n const published = this.#publishFn(lc, event);\n\n if (interval) {\n this.#timer = setInterval(\n () => this.publish(lc, stage, description, interval, extraState),\n interval,\n );\n }\n return published;\n }\n\n async publishAndThrowError(\n lc: LogContext,\n stage: ReplicationStage,\n e: unknown,\n ): Promise<never> {\n this.stop();\n const event = this.#dbRunner(lc, db =>\n replicationStatusError(lc, stage, e, db),\n );\n await this.#publishFn(lc, event);\n throw e;\n }\n\n stop(): this {\n clearInterval(this.#timer);\n return this;\n }\n}\n\n/**\n * Tracks the progress of a sequence of index creations for reporting in\n * the `indexingStatus` field of {@link ReplicationState}.\n */\nexport class IndexingProgress {\n readonly #totalIndexes: number | undefined;\n readonly #now: () => number;\n #count = 0;\n #completedMs = 0;\n #current:\n | {index: LiteIndexSpec; start: number; elapsedMs: number | undefined}\n | undefined;\n\n /**\n * @param totalIndexes The total number of indexes to be created, if known.\n */\n constructor(totalIndexes?: number, now = () => performance.now()) {\n this.#totalIndexes = totalIndexes;\n this.#now = now;\n }\n\n /** Marks the start of creating the `index`. */\n start(index: LiteIndexSpec) {\n this.#count++;\n this.#current = {index, start: this.#now(), elapsedMs: undefined};\n }\n\n /**\n * Restarts the timer of the current index, e.g. to exclude time spent\n * reporting its start from its creation time.\n */\n restartTimer() {\n if (this.#current) {\n this.#current.start = this.#now();\n }\n }\n\n /**\n * Marks the completion of the index passed to the last call to\n * {@link start}, returning the milliseconds it took to create.\n */\n finish(): number {\n const current = this.#current;\n if (!current) {\n return 0;\n }\n if (current.elapsedMs === undefined) {\n current.elapsedMs = this.#now() - current.start;\n this.#completedMs += current.elapsedMs;\n }\n return current.elapsedMs;\n }\n\n /** The `extraState` for {@link ReplicationStatusPublisher.publish}. */\n readonly state = (): Partial<ReplicationState> => {\n const status = this.status();\n return status ? {indexingStatus: status} : {};\n };\n\n status(): IndexingStatus | undefined {\n const current = this.#current;\n if (!current) {\n return undefined;\n }\n const {index, start, elapsedMs} = current;\n const done = elapsedMs !== undefined;\n return {\n name: index.name,\n table: index.tableName,\n columns: Object.keys(index.columns),\n unique: index.unique,\n index: this.#count,\n totalIndexes: this.#totalIndexes,\n elapsedMs: Math.round(done ? elapsedMs : this.#now() - start),\n completedMs: Math.round(\n done ? this.#completedMs - elapsedMs : this.#completedMs,\n ),\n done,\n };\n }\n}\n\nexport async function publishReplicationError(\n lc: LogContext,\n stage: ReplicationStage,\n description: string,\n errorDetails?: JSONObject,\n now = new Date(),\n) {\n const event: ReplicationStatusEvent = {\n type: 'zero/events/status/replication/v1',\n component: 'replication',\n status: 'ERROR',\n stage,\n description,\n errorDetails,\n time: now.toISOString(),\n };\n await publishCriticalEvent(lc, event);\n}\n\nexport function replicationStatusError(\n lc: LogContext,\n stage: ReplicationStage,\n e: unknown,\n db?: Database,\n now = new Date(),\n) {\n const event = replicationStatusEvent(lc, db, stage, 'ERROR', String(e), now);\n event.errorDetails = makeErrorDetails(e);\n return event;\n}\n\n// Exported for testing.\nexport function replicationStatusEvent(\n lc: LogContext,\n db: Database | undefined,\n stage: ReplicationStage,\n status: Status,\n description?: string,\n now = new Date(),\n): ReplicationStatusEvent {\n const start = performance.now();\n try {\n return {\n type: 'zero/events/status/replication/v1',\n component: 'replication',\n status,\n stage,\n description,\n time: now.toISOString(),\n state: {\n tables: db ? getReplicatedTables(db) : [],\n indexes: db ? getReplicatedIndexes(db) : [],\n replicaSize: db ? getReplicaSize(db) : undefined,\n },\n };\n } catch (e) {\n lc.warn?.(`Unable to create full ReplicationStatusEvent`, e);\n return {\n type: 'zero/events/status/replication/v1',\n component: 'replication',\n status,\n stage,\n description,\n time: now.toISOString(),\n state: {\n tables: [],\n indexes: [],\n replicaSize: 0,\n },\n };\n } finally {\n const elapsed = (performance.now() - start).toFixed(3);\n lc.debug?.(`computed schema for replication event (${elapsed} ms)`);\n }\n}\n\nfunction getReplicatedTables(db: Database): ReplicatedTable[] {\n const fullTables = new Map<string, LiteTableSpec>();\n const clientSchema = computeZqlSpecs(\n createSilentLogContext(), // avoid logging warnings about indexes\n db,\n {includeBackfillingColumns: false},\n new Map(),\n fullTables,\n );\n\n // oxlint-disable-next-line e18e/prefer-array-to-sorted\n return [...fullTables.entries()].sort(byKeys).map(([table, spec]) => ({\n table,\n columns: Object.entries(spec.columns)\n .sort(byKeys)\n .map(([column, spec]) => ({\n column,\n upstreamType: spec.dataType.split('|')[0],\n clientType: clientSchema.get(table)?.zqlSpec[column]?.type ?? null,\n })),\n }));\n}\n\nfunction getReplicatedIndexes(db: Database): ReplicatedIndex[] {\n return listIndexes(db).map(({tableName: table, columns, unique}) => ({\n table,\n unique,\n columns: Object.entries(columns)\n .sort(byKeys)\n .map(([column, dir]) => ({column, dir})),\n }));\n}\n\nfunction getReplicaSize(db: Database) {\n const [{page_count: pageCount}] = db.pragma<{page_count: number}>(\n 'page_count',\n );\n const [{page_size: pageSize}] = db.pragma<{page_size: number}>('page_size');\n return pageCount * pageSize;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"replication-status.js","names":["#dbRunner","#publishFn","#publish","#timer","#totalIndexes","#now","#count","#current","#completedMs"],"sources":["../../../../../../zero-cache/src/services/replicator/replication-status.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {createSilentLogContext} from '../../../../shared/src/logging-test-utils.ts';\nimport type {JSONObject} from '../../../../zero-events/src/json.ts';\nimport type {\n IndexingStatus,\n ReplicatedIndex,\n ReplicatedTable,\n ReplicationStage,\n ReplicationState,\n ReplicationStatusEvent,\n Status,\n} from '../../../../zero-events/src/status.ts';\nimport {Database} from '../../../../zqlite/src/db.ts';\nimport {computeZqlSpecs, listIndexes} from '../../db/lite-tables.ts';\nimport type {LiteIndexSpec, LiteTableSpec} from '../../db/specs.ts';\nimport {\n makeErrorDetails,\n publishCriticalEvent,\n} from '../../observability/events.ts';\n\nconst byKeys = (a: [string, unknown], b: [string, unknown]) =>\n a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;\n\nexport class ReplicationStatusPublisher {\n readonly #dbRunner: <T>(lc: LogContext, fn: (db: Database) => T) => T;\n readonly #publishFn: typeof publishCriticalEvent;\n #timer: NodeJS.Timeout | undefined;\n\n static forTesting() {\n return ReplicationStatusPublisher.forReplicaFile(':memory:');\n }\n\n static forRunningTransaction(tx: Database, publishFn = publishCriticalEvent) {\n return new ReplicationStatusPublisher((_lc, fn) => fn(tx), publishFn);\n }\n\n static forReplicaFile(file: string, publishFn = publishCriticalEvent) {\n return new ReplicationStatusPublisher((lc, fn) => {\n const db = new Database(lc, file, {readonly: true});\n try {\n return fn(db);\n } finally {\n db.close();\n }\n }, publishFn);\n }\n\n constructor(\n dbRunner: <T>(lc: LogContext, fn: (db: Database) => T) => T,\n publishFn = publishCriticalEvent,\n ) {\n this.#dbRunner = dbRunner;\n this.#publishFn = publishFn;\n }\n\n publish(\n lc: LogContext,\n stage: ReplicationStage,\n description?: string,\n interval = 0,\n extraState?: () => Partial<ReplicationState>,\n now = new Date(),\n ): this {\n void this.#publish(lc, stage, description, interval, extraState, now);\n return this;\n }\n\n /**\n * Like {@link publish}, but waits (for at most `maxWaitMs`) for the event\n * to be sent. Use this before a long synchronous operation (e.g. creating\n * an index) that blocks the event loop and would otherwise prevent the\n * event from being sent until the operation completes.\n */\n async publishAndFlush(\n lc: LogContext,\n stage: ReplicationStage,\n description?: string,\n interval = 0,\n extraState?: () => Partial<ReplicationState>,\n maxWaitMs = 1000,\n ): Promise<void> {\n const published = this.#publish(\n lc,\n stage,\n description,\n interval,\n extraState,\n );\n let timer: NodeJS.Timeout | undefined;\n try {\n await Promise.race([\n published,\n new Promise<void>(resolve => {\n timer = setTimeout(resolve, maxWaitMs);\n }),\n ]);\n } finally {\n clearTimeout(timer);\n }\n }\n\n #publish(\n lc: LogContext,\n stage: ReplicationStage,\n description: string | undefined,\n interval: number,\n extraState: (() => Partial<ReplicationState>) | undefined,\n now = new Date(),\n ): Promise<void> {\n this.stop();\n\n const event = this.#dbRunner(lc, db =>\n replicationStatusEvent(lc, db, stage, 'OK', description, now),\n );\n if (event.state) {\n event.state = {\n ...event.state,\n ...extraState?.(),\n };\n }\n const published = this.#publishFn(lc, event);\n\n if (interval) {\n this.#timer = setInterval(\n () => this.publish(lc, stage, description, interval, extraState),\n interval,\n );\n }\n return published;\n }\n\n async publishAndThrowError(\n lc: LogContext,\n stage: ReplicationStage,\n e: unknown,\n ): Promise<never> {\n this.stop();\n const event = this.#dbRunner(lc, db =>\n replicationStatusError(lc, stage, e, db),\n );\n await this.#publishFn(lc, event);\n throw e;\n }\n\n stop(): this {\n clearInterval(this.#timer);\n return this;\n }\n}\n\n/**\n * Tracks the progress of a sequence of index creations for reporting in\n * the `indexingStatus` field of {@link ReplicationState}.\n */\nexport class IndexingProgress {\n readonly #totalIndexes: number | undefined;\n readonly #now: () => number;\n #count = 0;\n #completedMs = 0;\n #current:\n | {index: LiteIndexSpec; start: number; elapsedMs: number | undefined}\n | undefined;\n\n /**\n * @param totalIndexes The total number of indexes to be created, if known.\n */\n constructor(totalIndexes?: number, now = () => performance.now()) {\n this.#totalIndexes = totalIndexes;\n this.#now = now;\n }\n\n /** Marks the start of creating the `index`. */\n start(index: LiteIndexSpec) {\n this.#count++;\n this.#current = {index, start: this.#now(), elapsedMs: undefined};\n }\n\n /**\n * Restarts the timer of the current index, e.g. to exclude time spent\n * reporting its start from its creation time.\n */\n restartTimer() {\n if (this.#current) {\n this.#current.start = this.#now();\n }\n }\n\n /**\n * Marks the completion of the index passed to the last call to\n * {@link start}, returning the milliseconds it took to create.\n */\n finish(): number {\n const current = this.#current;\n if (!current) {\n return 0;\n }\n if (current.elapsedMs === undefined) {\n current.elapsedMs = this.#now() - current.start;\n this.#completedMs += current.elapsedMs;\n }\n return current.elapsedMs;\n }\n\n /** The `extraState` for {@link ReplicationStatusPublisher.publish}. */\n readonly state = (): Partial<ReplicationState> => {\n const status = this.status();\n return status ? {indexingStatus: status} : {};\n };\n\n status(): IndexingStatus | undefined {\n const current = this.#current;\n if (!current) {\n return undefined;\n }\n const {index, start, elapsedMs} = current;\n const done = elapsedMs !== undefined;\n return {\n name: index.name,\n table: index.tableName,\n columns: Object.keys(index.columns),\n unique: index.unique,\n index: this.#count,\n totalIndexes: this.#totalIndexes,\n elapsedMs: Math.round(done ? elapsedMs : this.#now() - start),\n completedMs: Math.round(\n done ? this.#completedMs - elapsedMs : this.#completedMs,\n ),\n done,\n };\n }\n}\n\nexport async function publishReplicationError(\n lc: LogContext,\n stage: ReplicationStage,\n description: string,\n errorDetails?: JSONObject,\n now = new Date(),\n) {\n const event: ReplicationStatusEvent = {\n type: 'zero/events/status/replication/v1',\n component: 'replication',\n status: 'ERROR',\n stage,\n description,\n errorDetails,\n time: now.toISOString(),\n };\n await publishCriticalEvent(lc, event);\n}\n\nexport function replicationStatusError(\n lc: LogContext,\n stage: ReplicationStage,\n e: unknown,\n db?: Database,\n now = new Date(),\n) {\n const event = replicationStatusEvent(lc, db, stage, 'ERROR', String(e), now);\n event.errorDetails = makeErrorDetails(e);\n return event;\n}\n\n// Exported for testing.\nexport function replicationStatusEvent(\n lc: LogContext,\n db: Database | undefined,\n stage: ReplicationStage,\n status: Status,\n description?: string,\n now = new Date(),\n): ReplicationStatusEvent {\n const start = performance.now();\n try {\n return {\n type: 'zero/events/status/replication/v1',\n component: 'replication',\n status,\n stage,\n description,\n time: now.toISOString(),\n state: {\n tables: db ? getReplicatedTables(db) : [],\n indexes: db ? getReplicatedIndexes(db) : [],\n replicaSize: db ? getReplicaSize(db) : undefined,\n },\n };\n } catch (e) {\n lc.warn?.(`Unable to create full ReplicationStatusEvent`, e);\n return {\n type: 'zero/events/status/replication/v1',\n component: 'replication',\n status,\n stage,\n description,\n time: now.toISOString(),\n state: {\n tables: [],\n indexes: [],\n replicaSize: 0,\n },\n };\n } finally {\n const elapsed = (performance.now() - start).toFixed(3);\n lc.debug?.(`computed schema for replication event (${elapsed} ms)`);\n }\n}\n\nfunction getReplicatedTables(db: Database): ReplicatedTable[] {\n const fullTables = new Map<string, LiteTableSpec>();\n const clientSchema = computeZqlSpecs(\n createSilentLogContext(), // avoid logging warnings about indexes\n db,\n {includeBackfillingColumns: false},\n new Map(),\n fullTables,\n );\n\n // oxlint-disable-next-line e18e/prefer-array-to-sorted\n return [...fullTables.entries()].sort(byKeys).map(([table, spec]) => ({\n table,\n columns: Object.entries(spec.columns)\n .sort(byKeys)\n .map(([column, spec]) => ({\n column,\n upstreamType: spec.dataType.split('|')[0],\n clientType: clientSchema.get(table)?.zqlSpec[column]?.type ?? null,\n })),\n }));\n}\n\nfunction getReplicatedIndexes(db: Database): ReplicatedIndex[] {\n return listIndexes(db).map(({tableName: table, columns, unique}) => ({\n table,\n unique,\n columns: Object.entries(columns).map(([column, dir]) => ({column, dir})),\n }));\n}\n\nfunction getReplicaSize(db: Database) {\n const [{page_count: pageCount}] = db.pragma<{page_count: number}>(\n 'page_count',\n );\n const [{page_size: pageSize}] = db.pragma<{page_size: number}>('page_size');\n return pageCount * pageSize;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ResetPipelinesReason } from './snapshotter.ts';
|
|
2
|
+
export type AdvancementOutcome = {
|
|
3
|
+
readonly outcome: 'success' | 'error';
|
|
4
|
+
} | {
|
|
5
|
+
readonly outcome: 'reset';
|
|
6
|
+
readonly reason: ResetPipelinesReason;
|
|
7
|
+
};
|
|
8
|
+
export type AdvancementTimings = {
|
|
9
|
+
/** Setup plus time between time-slice yields, including asynchronous I/O. */
|
|
10
|
+
readonly processingTimeMs: number;
|
|
11
|
+
/** Synchronous snapshot, change-count, CVR updater, and poke preparation. */
|
|
12
|
+
readonly setupTimeMs: number;
|
|
13
|
+
/** Elapsed wall time, including time-slice waits and output cleanup. */
|
|
14
|
+
readonly wallTimeMs: number;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Counts and times every advancement attempt, including work abandoned by a
|
|
18
|
+
* reset. The existing sync.advance-time histogram measures successes only.
|
|
19
|
+
* Outcome and reset reason are bounded labels; client and query IDs are not
|
|
20
|
+
* recorded here. Recording once per attempt adds no clocks to row processing.
|
|
21
|
+
*/
|
|
22
|
+
export declare class AdvancementMetrics {
|
|
23
|
+
#private;
|
|
24
|
+
record({ processingTimeMs, setupTimeMs, wallTimeMs }: AdvancementTimings, outcome: AdvancementOutcome): void;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=advancement-metrics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"advancement-metrics.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/view-syncer/advancement-metrics.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,kBAAkB,CAAC;AAE3D,MAAM,MAAM,kBAAkB,GAC1B;IAAC,QAAQ,CAAC,OAAO,EAAE,SAAS,GAAG,OAAO,CAAA;CAAC,GACvC;IAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAA;CAAC,CAAC;AAEvE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,6EAA6E;IAC7E,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,6EAA6E;IAC7E,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,wEAAwE;IACxE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B,CAAC;AAEF;;;;;GAKG;AACH,qBAAa,kBAAkB;;IAqB7B,MAAM,CACJ,EAAC,gBAAgB,EAAE,WAAW,EAAE,UAAU,EAAC,EAAE,kBAAkB,EAC/D,OAAO,EAAE,kBAAkB,GAC1B,IAAI,CAQN;CACF"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { getOrCreateLatencyHistogram } from "../../observability/metrics.js";
|
|
2
|
+
//#region ../zero-cache/src/services/view-syncer/advancement-metrics.ts
|
|
3
|
+
/**
|
|
4
|
+
* Counts and times every advancement attempt, including work abandoned by a
|
|
5
|
+
* reset. The existing sync.advance-time histogram measures successes only.
|
|
6
|
+
* Outcome and reset reason are bounded labels; client and query IDs are not
|
|
7
|
+
* recorded here. Recording once per attempt adds no clocks to row processing.
|
|
8
|
+
*/
|
|
9
|
+
var AdvancementMetrics = class {
|
|
10
|
+
#processingTime = getOrCreateLatencyHistogram("sync", "advance-attempt-time", "Processing time for an advancement attempt, including setup and asynchronous I/O. Recorded for successful, reset, and failed attempts; excludes only time-slice waits.");
|
|
11
|
+
#setupTime = getOrCreateLatencyHistogram("sync", "advance-attempt-setup-time", "Synchronous setup time for an advancement attempt: snapshot acquisition, change counting, CVR updater construction, and poke preparation.");
|
|
12
|
+
#wallTime = getOrCreateLatencyHistogram("sync", "advance-attempt-wall-time", "Elapsed wall time for an advancement attempt, including time-slice waits, CVR flush, and output cleanup.");
|
|
13
|
+
record({ processingTimeMs, setupTimeMs, wallTimeMs }, outcome) {
|
|
14
|
+
const attributes = {
|
|
15
|
+
outcome: outcome.outcome,
|
|
16
|
+
...outcome.outcome === "reset" && { reset_reason: outcome.reason }
|
|
17
|
+
};
|
|
18
|
+
this.#processingTime.recordMs(processingTimeMs, attributes);
|
|
19
|
+
this.#setupTime.recordMs(setupTimeMs, attributes);
|
|
20
|
+
this.#wallTime.recordMs(wallTimeMs, attributes);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
export { AdvancementMetrics };
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=advancement-metrics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"advancement-metrics.js","names":["#processingTime","#setupTime","#wallTime"],"sources":["../../../../../../zero-cache/src/services/view-syncer/advancement-metrics.ts"],"sourcesContent":["import {getOrCreateLatencyHistogram} from '../../observability/metrics.ts';\nimport type {ResetPipelinesReason} from './snapshotter.ts';\n\nexport type AdvancementOutcome =\n | {readonly outcome: 'success' | 'error'}\n | {readonly outcome: 'reset'; readonly reason: ResetPipelinesReason};\n\nexport type AdvancementTimings = {\n /** Setup plus time between time-slice yields, including asynchronous I/O. */\n readonly processingTimeMs: number;\n /** Synchronous snapshot, change-count, CVR updater, and poke preparation. */\n readonly setupTimeMs: number;\n /** Elapsed wall time, including time-slice waits and output cleanup. */\n readonly wallTimeMs: number;\n};\n\n/**\n * Counts and times every advancement attempt, including work abandoned by a\n * reset. The existing sync.advance-time histogram measures successes only.\n * Outcome and reset reason are bounded labels; client and query IDs are not\n * recorded here. Recording once per attempt adds no clocks to row processing.\n */\nexport class AdvancementMetrics {\n readonly #processingTime = getOrCreateLatencyHistogram(\n 'sync',\n 'advance-attempt-time',\n 'Processing time for an advancement attempt, including setup and ' +\n 'asynchronous I/O. Recorded for successful, reset, and failed attempts; ' +\n 'excludes only time-slice waits.',\n );\n readonly #setupTime = getOrCreateLatencyHistogram(\n 'sync',\n 'advance-attempt-setup-time',\n 'Synchronous setup time for an advancement attempt: snapshot acquisition, ' +\n 'change counting, CVR updater construction, and poke preparation.',\n );\n readonly #wallTime = getOrCreateLatencyHistogram(\n 'sync',\n 'advance-attempt-wall-time',\n 'Elapsed wall time for an advancement attempt, including time-slice ' +\n 'waits, CVR flush, and output cleanup.',\n );\n\n record(\n {processingTimeMs, setupTimeMs, wallTimeMs}: AdvancementTimings,\n outcome: AdvancementOutcome,\n ): void {\n const attributes = {\n outcome: outcome.outcome,\n ...(outcome.outcome === 'reset' && {reset_reason: outcome.reason}),\n };\n this.#processingTime.recordMs(processingTimeMs, attributes);\n this.#setupTime.recordMs(setupTimeMs, attributes);\n this.#wallTime.recordMs(wallTimeMs, attributes);\n }\n}\n"],"mappings":";;;;;;;;AAsBA,IAAa,qBAAb,MAAgC;CAC9B,kBAA2B,4BACzB,QACA,wBACA,wKAGF;CACA,aAAsB,4BACpB,QACA,8BACA,2IAEF;CACA,YAAqB,4BACnB,QACA,6BACA,0GAEF;CAEA,OACE,EAAC,kBAAkB,aAAa,cAChC,SACM;EACN,MAAM,aAAa;GACjB,SAAS,QAAQ;GACjB,GAAI,QAAQ,YAAY,WAAW,EAAC,cAAc,QAAQ,OAAM;EAClE;EACA,KAAKA,gBAAgB,SAAS,kBAAkB,UAAU;EAC1D,KAAKC,WAAW,SAAS,aAAa,UAAU;EAChD,KAAKC,UAAU,SAAS,YAAY,UAAU;CAChD;AACF"}
|
|
@@ -50,6 +50,14 @@ export type HydrationStats = {
|
|
|
50
50
|
};
|
|
51
51
|
export type PipelineHydrationReason = 'query-set-sync' | 'unchanged-query-rehydrate';
|
|
52
52
|
type PipelineStopReason = 'replace-query' | 'remove-query' | 'reset' | 'destroy';
|
|
53
|
+
export type AdvancementProgress = {
|
|
54
|
+
/** Raw ChangeLog entries read, including skipped entries and the current change. */
|
|
55
|
+
readonly changesScanned: number;
|
|
56
|
+
/** Yielded source changes whose push processing completed. */
|
|
57
|
+
readonly changesProcessed: number;
|
|
58
|
+
/** Raw entries omitted by the snapshot diff. */
|
|
59
|
+
readonly changesSkipped: number;
|
|
60
|
+
};
|
|
53
61
|
export type Timer = {
|
|
54
62
|
elapsedLap: () => number;
|
|
55
63
|
totalElapsed: () => number;
|
|
@@ -174,6 +182,8 @@ export declare class PipelineDriver {
|
|
|
174
182
|
version: string;
|
|
175
183
|
numChanges: number;
|
|
176
184
|
changes: Iterable<RowChange | 'yield'>;
|
|
185
|
+
/** Available throughout iteration and after completion or an abort. */
|
|
186
|
+
progress: () => AdvancementProgress;
|
|
177
187
|
};
|
|
178
188
|
}
|
|
179
189
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline-driver.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/view-syncer/pipeline-driver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAQjD,OAAO,KAAK,EAAC,GAAG,EAAe,MAAM,sCAAsC,CAAC;AAC5E,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,gDAAgD,CAAC;AACjF,OAAO,KAAK,EAAC,GAAG,EAAC,MAAM,uCAAuC,CAAC;AAQ/D,OAAO,EAAC,UAAU,EAAC,MAAM,wCAAwC,CAAC;AAGlE,OAAO,EAIL,KAAK,KAAK,EAIX,MAAM,qCAAqC,CAAC;AAY7C,OAAO,KAAK,EACV,WAAW,EAEZ,MAAM,iDAAiD,CAAC;AAMzD,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,4CAA4C,CAAC;AASnF,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAC,SAAS,EAAE,UAAU,EAAC,MAAM,6BAA6B,CAAC;AAEvE,OAAO,KAAK,EAAC,cAAc,EAAgB,MAAM,mBAAmB,CAAC;AAMrE,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,oCAAoC,CAAC;AAC1E,OAAO,EAAC,KAAK,MAAM,EAAC,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAC,KAAK,OAAO,EAAC,MAAM,uBAAuB,CAAC;AAMnD,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,6BAA6B,CAAC;AAEtE,OAAO,EAAa,KAAK,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAC7D,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAEjD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,kBAAkB,CAAC;AAGlD,KAAK,KAAK,CAAC,EAAE,SAAS,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,KAAK,CAAC,IAAI;IAC1D,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;IAClB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC;IACrB,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,MAAM,GAAG,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;AAE3C,MAAM,MAAM,SAAS,GAAG,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;AAEjD,MAAM,MAAM,OAAO,GAAG,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;AAE7C,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;AA0BrD,MAAM,MAAM,SAAS,GAAG;IACtB,QAAQ,CAAC,cAAc,EAAE,GAAG,CAAC;IAC7B,QAAQ,CAAC,WAAW,CAAC,EAAE,GAAG,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACzC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,qCAAqC;IACrC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,qEAAqE;IACrE,QAAQ,CAAC,YAAY,EAAE,SAAS,WAAW,EAAE,CAAC;IAC9C,0EAA0E;IAC1E,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;CAC5B,CAAC;AAeF,MAAM,MAAM,uBAAuB,GAC/B,gBAAgB,GAChB,2BAA2B,CAAC;AAEhC,KAAK,kBAAkB,GACnB,eAAe,GACf,cAAc,GACd,OAAO,GACP,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"pipeline-driver.d.ts","sourceRoot":"","sources":["../../../../../../zero-cache/src/services/view-syncer/pipeline-driver.ts"],"names":[],"mappings":"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"}
|
|
@@ -679,17 +679,26 @@ var PipelineDriver = class {
|
|
|
679
679
|
const diff = this.#snapshotter.advance(this.#tableSpecs, this.#allTableNames, this.#tables, "divergent");
|
|
680
680
|
const { prev, curr, changes } = diff;
|
|
681
681
|
this.#lc.debug?.(`advance ${prev.version} => ${curr.version}: ${changes} changes`);
|
|
682
|
+
const progress = { changesProcessed: 0 };
|
|
682
683
|
return {
|
|
683
684
|
version: curr.version,
|
|
684
685
|
numChanges: changes,
|
|
685
|
-
changes: this.#trackRowSetSignatures(this.#advance(diff, timer, changes))
|
|
686
|
+
changes: this.#trackRowSetSignatures(this.#advance(diff, timer, changes, progress)),
|
|
687
|
+
progress: () => ({
|
|
688
|
+
changesScanned: diff.changesScanned,
|
|
689
|
+
changesProcessed: progress.changesProcessed,
|
|
690
|
+
changesSkipped: diff.changesSkipped
|
|
691
|
+
})
|
|
686
692
|
};
|
|
687
693
|
}
|
|
688
|
-
*#advance(diff, timer, numChanges) {
|
|
694
|
+
*#advance(diff, timer, numChanges, progress) {
|
|
689
695
|
assert(this.#hydrateContext === null, "Cannot advance while hydration is in progress");
|
|
690
696
|
const totalHydrationTimeMs = this.totalHydrationTimeMs();
|
|
691
697
|
const advanceContext = {
|
|
692
698
|
timer,
|
|
699
|
+
progress,
|
|
700
|
+
diff,
|
|
701
|
+
completedChanges: 0,
|
|
693
702
|
totalHydrationTimeMs,
|
|
694
703
|
numChanges,
|
|
695
704
|
reservedRows: void 0,
|
|
@@ -708,15 +717,20 @@ var PipelineDriver = class {
|
|
|
708
717
|
for (const table of this.#tables.values()) table.setDeferWrites(deferWrites);
|
|
709
718
|
this.#lc.debug?.(`starting pipeline advancement of ${numChanges} changes with an advancement time limited based on total hydration time of ${totalHydrationTimeMs} ms (${deferWrites ? "deferred" : "write-through"}).`);
|
|
710
719
|
for (const { table, prevValues: probedPrevValues, nextValue, rowKey } of diff) {
|
|
720
|
+
advanceContext.completedChanges = diff.changesScanned - 1;
|
|
711
721
|
if (this.#shouldAdvanceYieldMaybeAbortAdvance()) yield "yield";
|
|
712
722
|
const start = timer.totalElapsed();
|
|
713
723
|
advanceContext.currentChangeStartMs = start;
|
|
714
724
|
advanceContext.currentTable = table;
|
|
715
725
|
let holds = "fits";
|
|
726
|
+
let pushCompleted = false;
|
|
716
727
|
try {
|
|
717
728
|
try {
|
|
718
729
|
const tableSource = this.#tables.get(table);
|
|
719
|
-
if (!tableSource)
|
|
730
|
+
if (!tableSource) {
|
|
731
|
+
pushCompleted = true;
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
720
734
|
const primaryKey = mustGetPrimaryKey(this.#primaryKeys, table);
|
|
721
735
|
if (nextValue !== null && !this.#reserveSecondRow(advanceContext, rowKey, primaryKey)) holds = "rows";
|
|
722
736
|
const prevValues = tableSource.reconcilePendingConflicts(probedPrevValues, nextValue, rowKey, this.#tableSpecs.get(table)?.tableSpec.uniqueKeys ?? []);
|
|
@@ -728,8 +742,13 @@ var PipelineDriver = class {
|
|
|
728
742
|
}
|
|
729
743
|
if (nextValue) if (editOldRow) yield* this.#push(tableSource, makeSourceChangeEdit(nextValue, editOldRow));
|
|
730
744
|
else yield* this.#push(tableSource, makeSourceChangeAdd(nextValue));
|
|
745
|
+
pushCompleted = true;
|
|
731
746
|
} finally {
|
|
732
747
|
advanceContext.pos++;
|
|
748
|
+
if (pushCompleted) {
|
|
749
|
+
progress.changesProcessed++;
|
|
750
|
+
advanceContext.completedChanges = diff.changesScanned;
|
|
751
|
+
}
|
|
733
752
|
}
|
|
734
753
|
this.#shouldAdvanceYieldMaybeAbortAdvance(false);
|
|
735
754
|
if (holds === "fits") holds = this.#holdPendingRows(advanceContext);
|
|
@@ -740,6 +759,7 @@ var PipelineDriver = class {
|
|
|
740
759
|
this.#advanceTime.recordMs(elapsed, { table });
|
|
741
760
|
if (holds !== "fits") yield* this.#writeThrough(advanceContext, diff, holds);
|
|
742
761
|
}
|
|
762
|
+
advanceContext.completedChanges = diff.changesScanned;
|
|
743
763
|
const { curr } = diff;
|
|
744
764
|
for (const table of this.#tables.values()) table.setDB(curr.db.db);
|
|
745
765
|
this.#ensureCostModelExistsIfEnabled(curr.db.db);
|
|
@@ -858,13 +878,31 @@ var PipelineDriver = class {
|
|
|
858
878
|
}
|
|
859
879
|
};
|
|
860
880
|
}
|
|
881
|
+
/** Compute once for an admitted log, without clocks or allocations per change. */
|
|
882
|
+
#advancementLogFields({ timer, diff, progress, completedChanges, queryStats, totalHydrationTimeMs }) {
|
|
883
|
+
const advancementTimeMs = timer.totalElapsed();
|
|
884
|
+
let queryPushTimeMs = 0;
|
|
885
|
+
for (const stats of queryStats.values()) queryPushTimeMs += stats.timeMs;
|
|
886
|
+
return {
|
|
887
|
+
advancementTimeMs,
|
|
888
|
+
queryPushTimeMs,
|
|
889
|
+
outsideQueryPushTimeMs: Math.max(0, advancementTimeMs - queryPushTimeMs),
|
|
890
|
+
totalHydrationTimeMs,
|
|
891
|
+
changesScanned: diff.changesScanned,
|
|
892
|
+
changesCompleted: completedChanges,
|
|
893
|
+
changesProcessed: progress.changesProcessed,
|
|
894
|
+
changesSkipped: diff.changesSkipped
|
|
895
|
+
};
|
|
896
|
+
}
|
|
861
897
|
/**
|
|
862
898
|
* Logs each query whose pushes took longer than the slow advance
|
|
863
899
|
* threshold, at most once per query shape per
|
|
864
900
|
* {@link SLOW_ADVANCE_LOG_WINDOW_MS}.
|
|
865
901
|
*/
|
|
866
|
-
#logSlowAdvances(
|
|
902
|
+
#logSlowAdvances(context) {
|
|
867
903
|
if (!this.#lc.warn) return;
|
|
904
|
+
const { queryStats, numChanges } = context;
|
|
905
|
+
let advancementFields;
|
|
868
906
|
for (const [queryID, stats] of queryStats) {
|
|
869
907
|
if (stats.timeMs <= this.#logConfig.slowAdvanceThreshold) continue;
|
|
870
908
|
const query = this.#queryForLog(queryID);
|
|
@@ -877,6 +915,7 @@ var PipelineDriver = class {
|
|
|
877
915
|
advanceTimeMs: stats.timeMs,
|
|
878
916
|
changes: stats.changes,
|
|
879
917
|
advancementChanges: numChanges,
|
|
918
|
+
...advancementFields ??= this.#advancementLogFields(context),
|
|
880
919
|
timeMsByTable: Object.fromEntries(stats.timeMsByTable),
|
|
881
920
|
...suppressed > 0 && { suppressedSinceLastLog: suppressed },
|
|
882
921
|
zql: query.shape.zql
|
|
@@ -886,10 +925,12 @@ var PipelineDriver = class {
|
|
|
886
925
|
/**
|
|
887
926
|
* Logs the queries that took the most time in an advancement that timed
|
|
888
927
|
* out, since the reset that follows would otherwise not say what was slow.
|
|
889
|
-
* Throttled per query shape of the slowest query
|
|
928
|
+
* Throttled per query shape of the slowest query, with a shared fallback
|
|
929
|
+
* when the timeout happens before any query push timings are recorded.
|
|
890
930
|
*/
|
|
891
|
-
#logAdvanceTimeout(
|
|
931
|
+
#logAdvanceTimeout(context, reset) {
|
|
892
932
|
if (!this.#lc.warn) return;
|
|
933
|
+
const { queryStats, numChanges } = context;
|
|
893
934
|
const slowest = [...queryStats].toSorted(([, a], [, b]) => b.timeMs - a.timeMs).slice(0, ADVANCE_TIMEOUT_LOG_MAX_QUERIES).flatMap(([queryID, stats]) => {
|
|
894
935
|
const query = this.#queryForLog(queryID);
|
|
895
936
|
return query ? [{
|
|
@@ -897,14 +938,15 @@ var PipelineDriver = class {
|
|
|
897
938
|
stats
|
|
898
939
|
}] : [];
|
|
899
940
|
});
|
|
900
|
-
|
|
901
|
-
const suppressed = slowAdvanceLogThrottle.admit(`timeout:${
|
|
941
|
+
const leadingQuery = slowest[0]?.query;
|
|
942
|
+
const suppressed = slowAdvanceLogThrottle.admit(leadingQuery ? `timeout:${leadingQuery.queryName ?? ""}:${leadingQuery.shape.hash}` : "timeout:outside-query-push");
|
|
902
943
|
if (suppressed === void 0) return;
|
|
903
944
|
const summary = slowest.map(({ query, stats }) => `${query.queryName ?? query.fields.queryHash} (${Math.round(stats.timeMs)} ms)`).join(", ");
|
|
904
|
-
this.#lc.warn(`Advancement of ${numChanges} changes timed out. The queries that took the most time: ${summary}
|
|
945
|
+
this.#lc.warn(`Advancement of ${numChanges} changes timed out. ` + (summary ? `The queries that took the most time: ${summary}` : "No query push timings were recorded."), {
|
|
905
946
|
zeroEvent: "query-advance-timeout",
|
|
906
947
|
reason: reset.message,
|
|
907
948
|
advancementChanges: numChanges,
|
|
949
|
+
...this.#advancementLogFields(context),
|
|
908
950
|
queries: slowest.map(({ query, stats }) => ({
|
|
909
951
|
...query.fields,
|
|
910
952
|
advanceTimeMs: stats.timeMs,
|
|
@@ -937,14 +979,14 @@ var PipelineDriver = class {
|
|
|
937
979
|
* checks; a single pathological push always resets.
|
|
938
980
|
*/
|
|
939
981
|
#shouldAdvanceYieldMaybeAbortAdvance(checkYield = true) {
|
|
940
|
-
const { currentChangeStartMs,
|
|
982
|
+
const { currentChangeStartMs, completedChanges, numChanges, timer: advanceTimer, totalHydrationTimeMs } = must(this.#advanceContext);
|
|
941
983
|
const elapsed = advanceTimer.totalElapsed();
|
|
942
984
|
const currentChangeElapsedMs = currentChangeStartMs === void 0 ? void 0 : elapsed - currentChangeStartMs;
|
|
943
|
-
if (currentChangeElapsedMs !== void 0 && shouldResetSlowCurrentChange(currentChangeElapsedMs, totalHydrationTimeMs)) this.#throwSlowCurrentChangeReset(
|
|
944
|
-
const projectedTotalTimeMs = projectedAdvancementTimeMs(elapsed,
|
|
945
|
-
const shouldFinish = shouldFinishLateAdvancement(
|
|
946
|
-
if (!shouldFinish && shouldResetProjectedAdvancement(elapsed, projectedTotalTimeMs,
|
|
947
|
-
if (!shouldFinish && elapsed > MIN_ADVANCEMENT_TIME_LIMIT_MS && elapsed > totalHydrationTimeMs / 2 && (projectedTotalTimeMs === void 0 || projectedTotalTimeMs - elapsed > totalHydrationTimeMs)) throw new ResetPipelinesSignal(`Advancement exceeded timeout at ${
|
|
985
|
+
if (currentChangeElapsedMs !== void 0 && shouldResetSlowCurrentChange(currentChangeElapsedMs, totalHydrationTimeMs)) this.#throwSlowCurrentChangeReset(completedChanges, numChanges, elapsed, currentChangeElapsedMs, totalHydrationTimeMs);
|
|
986
|
+
const projectedTotalTimeMs = projectedAdvancementTimeMs(elapsed, completedChanges, numChanges);
|
|
987
|
+
const shouldFinish = shouldFinishLateAdvancement(completedChanges, numChanges);
|
|
988
|
+
if (!shouldFinish && shouldResetProjectedAdvancement(elapsed, projectedTotalTimeMs, completedChanges, numChanges, totalHydrationTimeMs)) this.#throwProjectedAdvancementReset(completedChanges, numChanges, elapsed, projectedTotalTimeMs, totalHydrationTimeMs);
|
|
989
|
+
if (!shouldFinish && elapsed > MIN_ADVANCEMENT_TIME_LIMIT_MS && elapsed > totalHydrationTimeMs / 2 && (projectedTotalTimeMs === void 0 || projectedTotalTimeMs - elapsed > totalHydrationTimeMs)) throw new ResetPipelinesSignal(`Advancement exceeded timeout at ${completedChanges} of ${numChanges} changes after ${elapsed} ms. Advancement time limited based on total hydration time of ${totalHydrationTimeMs} ms.`, "advancement-timeout");
|
|
948
990
|
return checkYield && advanceTimer.elapsedLap() > this.#yieldThresholdMs();
|
|
949
991
|
}
|
|
950
992
|
/**
|