From b5f5a5a5805a73bce1dbb330ef3a15731208fbb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 22 Oct 2026 07:01:03 +0200 Subject: [PATCH] perf(i18n): build the every-locale match indexes on first use Seven matchers built their index over every shipped catalog at module load (the GLP-1 tag map, the nutrient and metric names of the MCP reads, the SNOMED and LOINC lab names, the Coach's health words, the lab-name pattern), so loading the worker graph read all eight catalogs at boot. Each index is now built the first time it is asked for. The boot profile records the change: the longest busy stretch of the main thread at boot fell from 753 ms to 241 ms, the live heap after five idle minutes from 164 to 152 MB, and the catalog chunk sources (two of 9.7 MB) are gone. --- scripts/bench/results/v1.45.0-boot-after.json | 1648 +++++++++++++++++ scripts/bench/results/v1.45.0-boot-after.md | 298 +++ src/lib/ai/coach/refusal.ts | 16 +- src/lib/documents/auto-stage-labs.ts | 14 +- src/lib/fhir/lab-loinc.ts | 16 +- src/lib/fhir/lab-qualitative.ts | 13 +- src/lib/mcp/__tests__/rich-reads.test.ts | 4 +- src/lib/mcp/nutrients-read.ts | 24 +- src/lib/mcp/rich-reads.ts | 17 +- .../medications/glp1-side-effect-tag-match.ts | 13 +- 10 files changed, 2033 insertions(+), 30 deletions(-) create mode 100644 scripts/bench/results/v1.45.0-boot-after.json create mode 100644 scripts/bench/results/v1.45.0-boot-after.md diff --git a/scripts/bench/results/v1.45.0-boot-after.json b/scripts/bench/results/v1.45.0-boot-after.json new file mode 100644 index 000000000..cb435b3cb --- /dev/null +++ b/scripts/bench/results/v1.45.0-boot-after.json @@ -0,0 +1,1648 @@ +{ + "meta": { + "label": "v1.45.0-boot-after", + "commit": "a859adb59", + "date": "2026-10-22T04:49:50.419Z", + "node": "v22.23.2", + "cpus": 14, + "loadAverage": [ + 8.6, + 9.4, + 10.7 + ], + "heapMb": 792, + "idleSeconds": 300, + "healthyAtMs": 850, + "readyAtMs": 3038, + "stallMaxMs": 0 + }, + "bootLines": [ + "3038 ms: boot.steps {\"boot_queue_count\":122,\"boot_steps\":\"readiness_summary=2,key_check=187,time_zone_check=1,pulse_method_sync=25,monitors=3,worker_import=63,boss_start=266,load_worker_lifecycle=47,load_integration_sync=119,register_integration_sync=100,load_status=52,register_status=70,load_rollup=12,register_rollup=27,load_reminders=11,register_reminders=37,load_maintenance=50,register_maintena" + ], + "stalls": [], + "heapLines": [ + { + "atMs": 5524, + "heap_used_mb": 166, + "heap_total_mb": 192, + "heap_limit_mb": 840, + "heap_used_ratio": 0.197, + "new_space_mb": 12, + "old_space_mb": 91, + "code_space_mb": 3, + "large_object_space_mb": 53, + "new_large_object_space_mb": 0, + "code_large_object_space_mb": 0, + "trusted_space_mb": 6, + "read_only_space_mb": 0, + "other_spaces_mb": 0, + "rss_mb": 430, + "external_mb": 12, + "array_buffers_mb": 1, + "active_requests": 0, + "active_jobs": 0, + "in_flight_dropped": 0, + "uptime_s": 6 + }, + { + "atMs": 305529, + "heap_used_mb": 153, + "heap_total_mb": 163, + "heap_limit_mb": 840, + "heap_used_ratio": 0.182, + "new_space_mb": 1, + "old_space_mb": 90, + "code_space_mb": 4, + "large_object_space_mb": 53, + "new_large_object_space_mb": 0, + "code_large_object_space_mb": 0, + "trusted_space_mb": 5, + "read_only_space_mb": 0, + "other_spaces_mb": 0, + "rss_mb": 599, + "external_mb": 11, + "array_buffers_mb": 0, + "active_requests": 0, + "active_jobs": 0, + "in_flight_dropped": 0, + "uptime_s": 306 + } + ], + "markCompacts": [ + { + "atMs": 189, + "beforeMb": 20.2, + "afterMb": 17.1 + }, + { + "atMs": 374, + "beforeMb": 55.3, + "afterMb": 52.6 + }, + { + "atMs": 458, + "beforeMb": 83.2, + "afterMb": 81.1 + }, + { + "atMs": 1036, + "beforeMb": 148.4, + "afterMb": 132.7 + }, + { + "atMs": 16410, + "beforeMb": 159.6, + "afterMb": 153.2 + }, + { + "atMs": 17036, + "beforeMb": 153.4, + "afterMb": 151 + }, + { + "atMs": 61902, + "beforeMb": 167.5, + "afterMb": 157.5 + }, + { + "atMs": 245952, + "beforeMb": 183.4, + "afterMb": 152.6 + }, + { + "atMs": 303192, + "beforeMb": 165.6, + "afterMb": 151.8 + }, + { + "atMs": 303207, + "beforeMb": 151.8, + "afterMb": 151.8 + } + ], + "tasks": [ + { + "startMs": 6, + "durationMs": 175, + "byModule": [ + [ + "npm:next", + 47.2 + ], + [ + "node:internal/modules/cjs/loader", + 34.8 + ], + [ + "(native)", + 34.5 + ], + [ + "node:internal/bootstrap/realm", + 25.7 + ], + [ + "node:internal/deps/undici/undici", + 8.8 + ], + [ + "node:path", + 6.3 + ], + [ + "node:internal/modules/package_json_reader", + 3 + ], + [ + "node:internal/net", + 3 + ], + [ + "node:internal/modules/esm/resolve", + 2.5 + ], + [ + "node:fs", + 1.5 + ], + [ + "node:buffer", + 1.5 + ], + [ + "node:internal/deps/cjs-module-lexer/lexer", + 1.5 + ], + [ + "node:internal/util", + 1.5 + ], + [ + "node:events", + 1.5 + ], + [ + "node:internal/modules/helpers", + 1.1 + ] + ], + "byFunction": [ + [ + "wrapSafe node:internal/modules/cjs/loader", + 32.2 + ], + [ + "compileForInternalLoader node:internal/bootstrap/realm", + 25.7 + ], + [ + "startServer npm:next", + 10.5 + ], + [ + "internalModuleStat (native)", + 9.5 + ], + [ + "_export npm:next", + 7.5 + ], + [ + "__require node:internal/deps/undici/undici", + 7.3 + ], + [ + "(garbage collector) (native)", + 5.8 + ], + [ + "readFileUtf8 (native)", + 5.8 + ], + [ + "resolve node:path", + 4.5 + ], + [ + "__nccwpck_require__ npm:next", + 4.5 + ], + [ + "(anonymous) npm:next", + 4.2 + ], + [ + "compileFunction (native)", + 3 + ], + [ + "ZodType npm:next", + 3 + ], + [ + "dlopen (native)", + 3 + ], + [ + "(anonymous) node:internal/modules/cjs/loader", + 2.6 + ] + ], + "byEntry": [] + }, + { + "startMs": 203, + "durationMs": 241, + "byModule": [ + [ + "(native)", + 52.7 + ], + [ + "node:internal/modules/cjs/loader", + 40.1 + ], + [ + "turbopack:///[turbopack]/nodejs/runtime/build-base.ts", + 31.7 + ], + [ + "wasm://wasm/00d1cb12", + 28.7 + ], + [ + "npm:@prisma/client", + 28 + ], + [ + "npm:next", + 16.3 + ], + [ + "node:internal/modules/esm/utils", + 12 + ], + [ + "node:fs", + 4.5 + ], + [ + "npm:zod", + 3 + ], + [ + "node:internal/modules/helpers", + 3 + ], + [ + "npm:pg", + 3 + ], + [ + "node:internal/fs/utils", + 1.5 + ], + [ + "src/lib/validations/entry-instant.ts", + 1.5 + ], + [ + "npm:undici", + 1.5 + ], + [ + "node:internal/bootstrap/realm", + 1.5 + ] + ], + "byFunction": [ + [ + "wrapSafe node:internal/modules/cjs/loader", + 37.1 + ], + [ + "instantiateModule turbopack:///[turbopack]/nodejs/runtime/build-base.ts", + 31.7 + ], + [ + "(garbage collector) (native)", + 30.6 + ], + [ + "#u npm:@prisma/client", + 22.4 + ], + [ + "compileSourceTextModule node:internal/modules/esm/utils", + 12 + ], + [ + "wasm-function[1454] wasm://wasm/00d1cb12", + 6.1 + ], + [ + "internalModuleStat (native)", + 5.8 + ], + [ + "readFileUtf8 (native)", + 4.5 + ], + [ + "js-to-wasm (native)", + 4.5 + ], + [ + "wasm-function[2550] wasm://wasm/00d1cb12", + 4.5 + ], + [ + "(anonymous) npm:next", + 4.3 + ], + [ + "readFileSync node:fs", + 3 + ], + [ + "toRealPath node:internal/modules/helpers", + 3 + ], + [ + "(anonymous) npm:pg", + 3 + ], + [ + "Module (native)", + 3 + ] + ], + "byEntry": [ + [ + "src/instrumentation.ts", + 80.8 + ], + [ + "src/lib/db-session.ts", + 49.5 + ], + [ + "src/lib/boot/startup-state.ts", + 25.6 + ], + [ + "src/lib/boot/boot-steps.ts", + 25.6 + ], + [ + "src/lib/logging/background.ts", + 25.6 + ], + [ + "src/lib/process-type.ts", + 25.5 + ], + [ + "src/lib/logging/context.ts", + 13.6 + ], + [ + "src/lib/validations/measurement.ts", + 12.1 + ], + [ + "src/lib/logging/redact.ts", + 10.5 + ], + [ + "src/lib/validations/entry-instant.ts", + 4.6 + ], + [ + "src/generated/prisma/internal/class.ts", + 3 + ], + [ + "src/lib/medications/scheduling/hhmm.ts", + 1.5 + ], + [ + "src/lib/env.ts", + 1.5 + ] + ] + }, + { + "startMs": 501, + "durationMs": 178, + "byModule": [ + [ + "node:internal/modules/cjs/loader", + 53.9 + ], + [ + "turbopack:///[turbopack]/nodejs/runtime/build-base.ts", + 37.5 + ], + [ + "npm:next", + 18 + ], + [ + "npm:zod", + 12 + ], + [ + "node:internal/modules/esm/utils", + 10.5 + ], + [ + "(native)", + 8.7 + ], + [ + "node:fs", + 6 + ], + [ + "turbopack:///[turbopack]/shared/runtime/async-module.ts", + 4.5 + ], + [ + "npm:undici", + 4.5 + ], + [ + "node:internal/url", + 1.8 + ], + [ + "node:internal/modules/esm/resolve", + 1.5 + ], + [ + "node:internal/deps/cjs-module-lexer/lexer", + 1.5 + ], + [ + "node:internal/modules/esm/module_job", + 1.5 + ], + [ + "node:internal/deps/undici/undici", + 1.5 + ], + [ + "node:internal/promise_hooks", + 1.5 + ] + ], + "byFunction": [ + [ + "wrapSafe node:internal/modules/cjs/loader", + 52.4 + ], + [ + "instantiateModule turbopack:///[turbopack]/nodejs/runtime/build-base.ts", + 37.5 + ], + [ + "compileSourceTextModule node:internal/modules/esm/utils", + 10.5 + ], + [ + "(anonymous) npm:zod", + 6.1 + ], + [ + "(anonymous) npm:next", + 6 + ], + [ + "asyncModule turbopack:///[turbopack]/shared/runtime/async-module.ts", + 4.5 + ], + [ + "readFileSync node:fs", + 3 + ], + [ + "(garbage collector) (native)", + 3 + ], + [ + "realpathSync node:fs", + 2.9 + ], + [ + "Z npm:zod", + 2.9 + ], + [ + "finalizeResolution node:internal/modules/esm/resolve", + 1.5 + ], + [ + "pathToFileURL node:internal/url", + 1.5 + ], + [ + "a npm:next", + 1.5 + ], + [ + "./dist/esm/server/app-render/manifests-singleton.js npm:next", + 1.5 + ], + [ + "a.d npm:next", + 1.5 + ] + ], + "byEntry": [ + [ + "src/app/api/health/route.ts", + 61.4 + ], + [ + "src/lib/api-errors.ts", + 27 + ], + [ + "src/lib/jobs/job-failures.ts", + 14.9 + ], + [ + "src/lib/jobs/offhost-backup.ts", + 14.9 + ], + [ + "src/lib/managed-profiles/handover-access.ts", + 14.9 + ], + [ + "src/lib/sharing/acting-session.ts", + 14.9 + ], + [ + "src/lib/db-session.ts", + 13.4 + ], + [ + "src/lib/export/environment-backup.ts", + 13.4 + ], + [ + "src/lib/export/full-backup-payload.ts", + 13.4 + ], + [ + "src/lib/logging/background.ts", + 10.5 + ], + [ + "src/lib/auth/secure-cookie.ts", + 10.5 + ], + [ + "src/lib/notifications/types.ts", + 10.5 + ], + [ + "src/lib/logging/redact.ts", + 10.5 + ], + [ + "src/lib/validations/measurement.ts", + 9 + ], + [ + "src/instrumentation.ts", + 8.7 + ] + ] + }, + { + "startMs": 833, + "durationMs": 184, + "byModule": [ + [ + "turbopack:///[turbopack]/nodejs/runtime/build-base.ts", + 38.9 + ], + [ + "node:internal/modules/cjs/loader", + 28.6 + ], + [ + "(native)", + 22.8 + ], + [ + "src/lib/i18n/server-catalogs.ts", + 15.1 + ], + [ + "npm:yaml", + 15 + ], + [ + "node:fs", + 12.1 + ], + [ + "npm:zod", + 9 + ], + [ + "turbopack:///[turbopack]/shared/runtime/async-module.ts", + 5.9 + ], + [ + "turbopack:///[turbopack]/shared/runtime/runtime-utils.ts", + 4.5 + ], + [ + "npm:semver", + 3 + ], + [ + "npm:next", + 2.3 + ], + [ + "turbopack:///[turbopack]/nodejs/runtime/runtime-base.ts", + 1.5 + ], + [ + "node:internal/bootstrap/realm", + 1.5 + ], + [ + "src/lib/labs/unit-conversion/registry.ts", + 1.5 + ], + [ + "src/lib/validations/user-prefs.ts", + 1.5 + ] + ], + "byFunction": [ + [ + "instantiateModule turbopack:///[turbopack]/nodejs/runtime/build-base.ts", + 38.9 + ], + [ + "wrapSafe node:internal/modules/cjs/loader", + 28.6 + ], + [ + "(garbage collector) (native)", + 18.5 + ], + [ + "(anonymous) src/lib/i18n/server-catalogs.ts", + 15.1 + ], + [ + "readFileSync node:fs", + 12.1 + ], + [ + "z npm:zod", + 4.5 + ], + [ + "asyncModule turbopack:///[turbopack]/shared/runtime/async-module.ts", + 4.5 + ], + [ + "readFileUtf8 (native)", + 4.3 + ], + [ + "(anonymous) npm:zod", + 3 + ], + [ + "next npm:yaml", + 3 + ], + [ + "loadChunkAsync turbopack:///[turbopack]/nodejs/runtime/runtime-base.ts", + 1.5 + ], + [ + "compileForInternalLoader node:internal/bootstrap/realm", + 1.5 + ], + [ + "esmExport turbopack:///[turbopack]/shared/runtime/runtime-utils.ts", + 1.5 + ], + [ + "(anonymous) src/lib/labs/unit-conversion/registry.ts", + 1.5 + ], + [ + "(anonymous) src/lib/validations/user-prefs.ts", + 1.5 + ] + ], + "byEntry": [ + [ + "src/lib/jobs/reminder-worker.ts", + 129.1 + ], + [ + "src/lib/jobs/reminder/google-health-sync.ts", + 88.5 + ], + [ + "src/lib/whoop/sync.ts", + 79.5 + ], + [ + "src/lib/whoop/sync-body.ts", + 63 + ], + [ + "src/lib/insights/bucket-series.ts", + 60 + ], + [ + "src/lib/whoop/oauth-descriptor.ts", + 60 + ], + [ + "src/lib/whoop/sync-core.ts", + 60 + ], + [ + "src/instrumentation.ts", + 43.4 + ], + [ + "src/lib/import/unzip-export-xml.ts", + 31.4 + ], + [ + "src/lib/jobs/integration-status-null-probe.ts", + 31.4 + ], + [ + "src/lib/ai/coach/memory/propose-plan.ts", + 30.1 + ], + [ + "src/lib/ai/coach/refusal.ts", + 27.1 + ], + [ + "src/lib/ai/coach/outbound-guard.ts", + 25.6 + ], + [ + "src/lib/i18n/server-catalogs.ts", + 24.1 + ], + [ + "src/lib/insights/snapshot-hash.ts", + 23.9 + ] + ] + }, + { + "startMs": 1099, + "durationMs": 54, + "byModule": [ + [ + "node:internal/modules/cjs/loader", + 23.9 + ], + [ + "turbopack:///[turbopack]/nodejs/runtime/build-base.ts", + 10.6 + ], + [ + "turbopack:///[turbopack]/shared/runtime/async-module.ts", + 5.8 + ], + [ + "src/lib/daily/today-overview.ts", + 4.5 + ], + [ + "src/lib/jobs/reminder-worker.ts", + 3 + ], + [ + "node:fs", + 3 + ], + [ + "npm:pg-boss", + 1.5 + ], + [ + "npm:zod", + 1.5 + ] + ], + "byFunction": [ + [ + "wrapSafe node:internal/modules/cjs/loader", + 23.9 + ], + [ + "instantiateModule turbopack:///[turbopack]/nodejs/runtime/build-base.ts", + 10.6 + ], + [ + "asyncModule turbopack:///[turbopack]/shared/runtime/async-module.ts", + 5.8 + ], + [ + "w src/lib/daily/today-overview.ts", + 4.5 + ], + [ + "readFileSync node:fs", + 3 + ], + [ + "work npm:pg-boss", + 1.5 + ], + [ + "(anonymous) src/lib/jobs/reminder-worker.ts", + 1.5 + ], + [ + "d9 npm:zod", + 1.5 + ], + [ + "v src/lib/jobs/reminder-worker.ts", + 1.5 + ] + ], + "byEntry": [ + [ + "src/lib/jobs/reminder-worker.ts", + 52.3 + ], + [ + "src/lib/jobs/reminder/insights-handlers.ts", + 22.3 + ], + [ + "src/lib/ai/prompts/arrival-reaction.ts", + 12 + ], + [ + "src/lib/daily/score-steady.ts", + 10.5 + ], + [ + "src/lib/dashboard/bands.ts", + 6 + ], + [ + "src/lib/jobs/data-arrival.ts", + 4.5 + ], + [ + "src/lib/dashboard/snapshot-read.ts", + 4.5 + ], + [ + "src/lib/daily/today-overview.ts", + 4.5 + ], + [ + "src/lib/daily/priority-item-key.ts", + 4.5 + ], + [ + "src/lib/ai/prompts/status-batch.ts", + 3 + ], + [ + "src/lib/jobs/reminder/registrar-shared.ts", + 1.5 + ], + [ + "src/lib/jobs/reminder/register-integration-sync.ts", + 1.5 + ], + [ + "src/lib/ai/prompts/base-system.ts", + 1.5 + ], + [ + "src/lib/insights/derived/derived-assessment-ai.ts", + 1.5 + ], + [ + "src/lib/measurements/list/sleep-nights.ts", + 1.5 + ] + ] + }, + { + "startMs": 1309, + "durationMs": 51, + "byModule": [ + [ + "node:internal/modules/cjs/loader", + 28 + ], + [ + "turbopack:///[turbopack]/nodejs/runtime/build-base.ts", + 6 + ], + [ + "node:fs", + 4.5 + ], + [ + "turbopack:///[turbopack]/shared/runtime/async-module.ts", + 3 + ], + [ + "turbopack:///[turbopack]/nodejs/runtime/runtime-base.ts", + 1.5 + ], + [ + "src/lib/jobs/failure-condition.ts", + 1.5 + ], + [ + "src/lib/labs/parse-reference-range.ts", + 1.5 + ], + [ + "npm:zod", + 1.5 + ], + [ + "node:internal/linkedlist", + 1.5 + ], + [ + "(native)", + 0.9 + ], + [ + "node:buffer", + 0.6 + ] + ], + "byFunction": [ + [ + "wrapSafe node:internal/modules/cjs/loader", + 28 + ], + [ + "instantiateModule turbopack:///[turbopack]/nodejs/runtime/build-base.ts", + 6 + ], + [ + "readFileSync node:fs", + 4.5 + ], + [ + "asyncModule turbopack:///[turbopack]/shared/runtime/async-module.ts", + 3 + ], + [ + "loadChunkAsync turbopack:///[turbopack]/nodejs/runtime/runtime-base.ts", + 1.5 + ], + [ + "(anonymous) src/lib/jobs/failure-condition.ts", + 1.5 + ], + [ + "r src/lib/labs/parse-reference-range.ts", + 1.5 + ], + [ + "u npm:zod", + 1.5 + ], + [ + "isEmpty node:internal/linkedlist", + 1.5 + ], + [ + "(garbage collector) (native)", + 0.9 + ], + [ + "getEncodingOps node:buffer", + 0.6 + ] + ], + "byEntry": [ + [ + "src/lib/jobs/reminder-worker.ts", + 48.2 + ], + [ + "src/lib/jobs/reminder/ops-handlers.ts", + 14.1 + ], + [ + "src/lib/medications/ingredients/label-extract.ts", + 3 + ], + [ + "src/lib/export/restore-backup.ts", + 2.1 + ], + [ + "src/lib/jobs/failure-condition.ts", + 1.5 + ], + [ + "src/lib/documents/ai-runs/summary-run.ts", + 1.5 + ], + [ + "src/lib/documents/auto-stage-labs.ts", + 1.5 + ], + [ + "src/lib/documents/ai-runs/store.ts", + 1.5 + ], + [ + "src/lib/labs/parse-reference-range.ts", + 1.5 + ], + [ + "src/lib/labs/ocr-upload.ts", + 1.5 + ], + [ + "src/lib/jobs/document-content-index-backfill.ts", + 1.5 + ], + [ + "src/lib/labs/ocr-extract.ts", + 1.5 + ], + [ + "src/lib/dawarich/sync.ts", + 1.5 + ], + [ + "src/lib/validations/dawarich.ts", + 1.5 + ], + [ + "src/lib/jobs/auth-ledger-cleanup.ts", + 1.5 + ] + ] + } + ], + "bootSelf": [ + [ + "(native)", + 230.8 + ], + [ + "node:internal/modules/cjs/loader", + 223.6 + ], + [ + "turbopack:///[turbopack]/nodejs/runtime/build-base.ts", + 124.7 + ], + [ + "npm:next", + 98.8 + ], + [ + "wasm://wasm/00d1cb12", + 62.6 + ], + [ + "npm:@prisma/client", + 56.2 + ], + [ + "node:fs", + 34.6 + ], + [ + "node:internal/bootstrap/realm", + 28.7 + ], + [ + "npm:pg", + 28.3 + ], + [ + "npm:zod", + 27 + ], + [ + "npm:pg-boss", + 24.6 + ], + [ + "node:internal/modules/esm/utils", + 22.5 + ], + [ + "turbopack:///[turbopack]/shared/runtime/async-module.ts", + 20.6 + ], + [ + "npm:luxon", + 17 + ], + [ + "src/lib/i18n/server-catalogs.ts", + 15.1 + ], + [ + "npm:yaml", + 15 + ], + [ + "npm:pg-pool", + 13 + ], + [ + "node:internal/deps/undici/undici", + 10.4 + ], + [ + "node:path", + 7.8 + ], + [ + "turbopack:///[turbopack]/shared/runtime/runtime-utils.ts", + 7.5 + ], + [ + "node:internal/async_hooks", + 7.5 + ], + [ + "node:internal/process/task_queues", + 7.3 + ], + [ + "node:internal/modules/package_json_reader", + 6 + ], + [ + "npm:undici", + 6 + ], + [ + "npm:pg-protocol", + 6 + ], + [ + "npm:cron-parser", + 6 + ], + [ + "node:internal/stream_base_commons", + 5.7 + ], + [ + "node:events", + 5.5 + ], + [ + "node:internal/timers", + 5.5 + ], + [ + "node:internal/streams/writable", + 5.3 + ], + [ + "node:internal/async_local_storage/async_hooks", + 5.3 + ], + [ + "node:buffer", + 5.2 + ], + [ + "node:internal/util", + 4.5 + ], + [ + "npm:%40prisma", + 4.5 + ], + [ + "node:internal/crypto/random", + 4.5 + ], + [ + "src/lib/daily/today-overview.ts", + 4.5 + ], + [ + "node:internal/modules/helpers", + 4.2 + ], + [ + "node:internal/modules/esm/resolve", + 4 + ], + [ + "node:internal/deps/cjs-module-lexer/lexer", + 3 + ], + [ + "node:internal/net", + 3 + ] + ], + "heap": { + "probes": { + "parsed locale catalogs": { + "count": 8, + "retainedMb": 7 + } + }, + "totalMb": 160.1, + "nodeCount": 1275329, + "byType": [ + [ + "string", + 68.7 + ], + [ + "array", + 26.4 + ], + [ + "code", + 21.1 + ], + [ + "object", + 14.1 + ], + [ + "hidden", + 8.9 + ], + [ + "native", + 8 + ], + [ + "object shape", + 5.9 + ], + [ + "closure", + 5.2 + ], + [ + "concatenated string", + 1.3 + ], + [ + "regexp", + 0.2 + ], + [ + "sliced string", + 0.2 + ], + [ + "number", + 0.1 + ], + [ + "synthetic", + 0 + ], + [ + "symbol", + 0 + ], + [ + "bigint", + 0 + ] + ], + "byConstructor": [ + [ + "array:(object elements)", + 20.4, + 23797 + ], + [ + "object:Object", + 10.2, + 274231 + ], + [ + "native:system / JSArrayBufferData", + 7.6, + 171 + ], + [ + "array:(object properties)", + 3.6, + 6512 + ], + [ + "array:", + 2.4, + 10502 + ], + [ + "closure:", + 1.6, + 29042 + ], + [ + "object:system / Context", + 1.6, + 28873 + ], + [ + "closure:native_bind", + 1, + 20782 + ], + [ + "object:Array", + 0.6, + 19679 + ], + [ + "object:a", + 0.4, + 4438 + ], + [ + "native:Node / histogram", + 0.4, + 1 + ], + [ + "closure:get", + 0.3, + 4736 + ], + [ + "object:d", + 0.3, + 4425 + ], + [ + "object:Set", + 0.2, + 5673 + ], + [ + "closure:set", + 0.2, + 3432 + ], + [ + "object:Module", + 0.2, + 1958 + ], + [ + "closure:i", + 0.1, + 1581 + ], + [ + "closure:n", + 0.1, + 1572 + ], + [ + "object:Promise", + 0.1, + 1277 + ], + [ + "object:n", + 0.1, + 1881 + ], + [ + "object:ZodOptional", + 0.1, + 236 + ], + [ + "object:_TstNode", + 0.1, + 939 + ], + [ + "native:Node / parser_buffer", + 0.1, + 1 + ], + [ + "object:TCP", + 0, + 8 + ], + [ + "closure:callbackTrampoline", + 0, + 1 + ], + [ + "closure:promiseInitHook", + 0, + 1 + ], + [ + "closure:promiseBeforeHook", + 0, + 1 + ], + [ + "closure:promiseAfterHook", + 0, + 1 + ], + [ + "object:Signal", + 0, + 4 + ], + [ + "object:HTTPParser", + 0, + 2 + ] + ], + "largestStrings": [ + [ + 5.6, + "module.exports=[234494,A=>{\"use strict\";var e,C,g,I,Q=Object.defineProperty,B={},s={$:()=>i,bgBlack:" + ], + [ + 5.6, + "module.exports=[17856,A=>{\"use strict\";var e,C,g,I,Q=Object.defineProperty,B={},s={$:()=>i,bgBlack:(" + ], + [ + 5.6, + "module.exports=[17856,A=>{\"use strict\";var e,C,g,I,Q=Object.defineProperty,B={},s={$:()=>i,bgBlack:(" + ], + [ + 4.4, + "const wasm = \"AGFzbQEAAAABtgRLYAN/f38Bf2ACf38Bf2ACf38AYAF/AGADf39/AGAEf39/fwBgBX9/f39/AGABfwF/YAZ/f3" + ], + [ + 4.4, + "AGFzbQEAAAABtgRLYAN/f38Bf2ACf38Bf2ACf38AYAF/AGADf39/AGAEf39/fwBgBX9/f39/AGABfwF/YAZ/f39/f38AYAR/f39/" + ], + [ + 1.9, + "lliADOAUngIEAACgKwAgBgAAoSsAIAcAAKErACAIAACiKwAgCQAAoysAIAoAAKQrACALAAClKwAgDAAApisAIA0AAKcrACARAACS" + ], + [ + 1.3, + "(()=>{var e,t,r={\"./dist/build/webpack/alias/react-dom-server.js\"(e,t,r){\"use strict\";var n;function" + ], + [ + 1.2, + "module.exports=[842865,(e,t,r)=>{let i=e.r(522734);t.exports=function(e){return e?/-----BEGIN ([A-Z\\" + ], + [ + 1.2, + "module.exports=[842865,(e,t,r)=>{let i=e.r(522734);t.exports=function(e){return e?/-----BEGIN ([A-Z\\" + ], + [ + 1.1, + "module.exports=[799092,(e,t,n)=>{\"use strict\";Object.defineProperty(n,\"__esModule\",{value:!0}),Objec" + ], + [ + 1.1, + "// v1.4.34 web-freeze. Schema is locked additive-only until iOS launch // + v1.5.0 marker. See .plan" + ], + [ + 1.1, + "module.exports=[971743,(A,e,t)=>{\"use strict\";let r=A.r(57328),{addAbortListener:s}=A.r(687769),{Rea" + ], + [ + 1, + "module.exports=[835717,(A,e,t)=>{\"use strict\";let r=A.r(57328),{addAbortListener:s}=A.r(687769),{Rea" + ], + [ + 0.8, + "module.exports=[767189,e=>e.a(async(t,a)=>{try{var n=e.i(64105),r=e.i(563616),i=t([n]);[n]=i.then?(a" + ], + [ + 0.7, + "module.exports=[594108,806176,674750,98746,t=>{\"use strict\";var i;let r;function n(t){return t}funct" + ], + [ + 0.7, + "module.exports=[144056,t=>{\"use strict\";let i,n;function r(t){return t}function a(t){return t}functi" + ], + [ + 0.6, + "module.exports=[992657,e=>e.a(async(t,a)=>{try{var r=e.i(851258),n=t([r]);async function i(e,t,a,n){" + ], + [ + 0.6, + "module.exports=[388046,e=>{\"use strict\";class t{value;next;constructor(e){this.value=e}}class n{#e;#" + ], + [ + 0.6, + "module.exports=[388046,e=>{\"use strict\";class t{value;next;constructor(e){this.value=e}}class n{#e;#" + ], + [ + 0.5, + "module.exports=[120630,(e,t,n)=>{\"use strict\";Object.defineProperty(n,\"__esModule\",{value:!0}),n.def" + ], + [ + 0.5, + "module.exports=[120630,(e,t,n)=>{\"use strict\";Object.defineProperty(n,\"__esModule\",{value:!0}),n.def" + ], + [ + 0.5, + "module.exports=[120630,(e,t,n)=>{\"use strict\";Object.defineProperty(n,\"__esModule\",{value:!0}),n.def" + ], + [ + 0.5, + "module.exports=[120630,(e,t,n)=>{\"use strict\";Object.defineProperty(n,\"__esModule\",{value:!0}),n.def" + ], + [ + 0.5, + "module.exports=[120630,(e,t,n)=>{\"use strict\";Object.defineProperty(n,\"__esModule\",{value:!0}),n.def" + ], + [ + 0.5, + "module.exports=[120630,(e,t,n)=>{\"use strict\";Object.defineProperty(n,\"__esModule\",{value:!0}),n.def" + ] + ], + "topRetainers": [ + { + "mb": 33.9, + "name": "object:t", + "path": "[1] > [10] > 60 > extension > global > prisma > [1]" + }, + { + "mb": 7.9, + "name": "object:global", + "path": "[1] > [10] > 60 > extension" + }, + { + "mb": 7.4, + "name": "object:system / Context", + "path": "Node / PrincipalRealm > base_object_list > Node / BaseObjectList > [25] > Node / ModuleWrap > native_to_javascript > ModuleWrap > 2 > [4] > context" + }, + { + "mb": 5.6, + "name": "object:Module", + "path": "[1] > [10] > 97 > process > nextTick > patchedNextTick > context > system / Context > require > require > cache > .next/server/chunks/_1mg8fms._.js" + }, + { + "mb": 5.6, + "name": "code:.next/server/chu", + "path": "require > cache > Object > .next/server/chunks/_1kwfnjg._.js > Module > exports > Array > [1] > > shared > script" + }, + { + "mb": 5.6, + "name": "code:.next/server/chu", + "path": "global > prisma > [1] > t > _engineConfig > Object > compilerWasm > Object > getRuntime > getRuntime > shared > script" + }, + { + "mb": 4.4, + "name": "object:Generator", + "path": "[6] > principal_realm > Node / PrincipalRealm > base_object_list > Node / BaseObjectList > [26] > Node / ModuleWrap > native_to_javascript > ModuleWrap > 2 > [4]" + }, + { + "mb": 4.4, + "name": "string:AGFzbQEAAAABtgRLYAN/f38Bf2ACf38Bf2ACf38AYAF/AGADf39/AGAEf39/", + "path": "[1] > [21] > 62203" + }, + { + "mb": 3.3, + "name": "object:Object", + "path": "system / Context > require > require > cache > Object > .next/server/chunks/[turbopack]_runtime.js > Module > exports > > context > moduleCache" + } + ], + "sourceStringsMb": 39.8 + } +} diff --git a/scripts/bench/results/v1.45.0-boot-after.md b/scripts/bench/results/v1.45.0-boot-after.md new file mode 100644 index 000000000..4ec08b032 --- /dev/null +++ b/scripts/bench/results/v1.45.0-boot-after.md @@ -0,0 +1,298 @@ +# Boot profile v1.45.0-boot-after + +Commit a859adb59, 2026-10-22T04:49:50.419Z. Production build, every role, heap ceiling 792 MB, 300 s idle. 14 cores, load average 8.6,9.4,10.7 at the end. + +- first answer from /api/health: 850 ms; background role ready: 3038 ms +- event-loop stalls logged: 0, max 0 ms + +## Stalls + + +## Heap samples + +| at s | used MB | old | large obj | code | external | array buffers | rss | +| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| 6 | 166 | 91 | 53 | 3 | 12 | 1 | 430 | +| 306 | 153 | 90 | 53 | 4 | 11 | 0 | 599 | + +## Full collections (mark-compact) + +10 mark-compacts during the run. Used heap before the collection, and what survived it (the live heap): + +| at s | before MB | after MB | +| ---: | ---: | ---: | +| 0 | 20.2 | 17.1 | +| 0 | 55.3 | 52.6 | +| 0 | 83.2 | 81.1 | +| 1 | 148.4 | 132.7 | +| 16 | 159.6 | 153.2 | +| 17 | 153.4 | 151 | +| 62 | 167.5 | 157.5 | +| 246 | 183.4 | 152.6 | +| 303 | 165.6 | 151.8 | +| 303 | 151.8 | 151.8 | + +## Main-thread busy stretches over 50 ms without an idle sample, first two minutes + +### at 6 ms, 175 ms + +| self time by module | ms | +| --- | ---: | +| npm:next | 47.2 | +| node:internal/modules/cjs/loader | 34.8 | +| (native) | 34.5 | +| node:internal/bootstrap/realm | 25.7 | +| node:internal/deps/undici/undici | 8.8 | +| node:path | 6.3 | +| node:internal/modules/package_json_reader | 3 | +| node:internal/net | 3 | +| node:internal/modules/esm/resolve | 2.5 | +| node:fs | 1.5 | + +| inclusive, application modules | ms | +| --- | ---: | + +### at 203 ms, 241 ms + +| self time by module | ms | +| --- | ---: | +| (native) | 52.7 | +| node:internal/modules/cjs/loader | 40.1 | +| turbopack:///[turbopack]/nodejs/runtime/build-base.ts | 31.7 | +| wasm://wasm/00d1cb12 | 28.7 | +| npm:@prisma/client | 28 | +| npm:next | 16.3 | +| node:internal/modules/esm/utils | 12 | +| node:fs | 4.5 | +| npm:zod | 3 | +| node:internal/modules/helpers | 3 | + +| inclusive, application modules | ms | +| --- | ---: | +| src/instrumentation.ts | 80.8 | +| src/lib/db-session.ts | 49.5 | +| src/lib/boot/startup-state.ts | 25.6 | +| src/lib/boot/boot-steps.ts | 25.6 | +| src/lib/logging/background.ts | 25.6 | +| src/lib/process-type.ts | 25.5 | +| src/lib/logging/context.ts | 13.6 | +| src/lib/validations/measurement.ts | 12.1 | +| src/lib/logging/redact.ts | 10.5 | +| src/lib/validations/entry-instant.ts | 4.6 | + +### at 501 ms, 178 ms + +| self time by module | ms | +| --- | ---: | +| node:internal/modules/cjs/loader | 53.9 | +| turbopack:///[turbopack]/nodejs/runtime/build-base.ts | 37.5 | +| npm:next | 18 | +| npm:zod | 12 | +| node:internal/modules/esm/utils | 10.5 | +| (native) | 8.7 | +| node:fs | 6 | +| turbopack:///[turbopack]/shared/runtime/async-module.ts | 4.5 | +| npm:undici | 4.5 | +| node:internal/url | 1.8 | + +| inclusive, application modules | ms | +| --- | ---: | +| src/app/api/health/route.ts | 61.4 | +| src/lib/api-errors.ts | 27 | +| src/lib/jobs/job-failures.ts | 14.9 | +| src/lib/jobs/offhost-backup.ts | 14.9 | +| src/lib/managed-profiles/handover-access.ts | 14.9 | +| src/lib/sharing/acting-session.ts | 14.9 | +| src/lib/db-session.ts | 13.4 | +| src/lib/export/environment-backup.ts | 13.4 | +| src/lib/export/full-backup-payload.ts | 13.4 | +| src/lib/logging/background.ts | 10.5 | + +### at 833 ms, 184 ms + +| self time by module | ms | +| --- | ---: | +| turbopack:///[turbopack]/nodejs/runtime/build-base.ts | 38.9 | +| node:internal/modules/cjs/loader | 28.6 | +| (native) | 22.8 | +| src/lib/i18n/server-catalogs.ts | 15.1 | +| npm:yaml | 15 | +| node:fs | 12.1 | +| npm:zod | 9 | +| turbopack:///[turbopack]/shared/runtime/async-module.ts | 5.9 | +| turbopack:///[turbopack]/shared/runtime/runtime-utils.ts | 4.5 | +| npm:semver | 3 | + +| inclusive, application modules | ms | +| --- | ---: | +| src/lib/jobs/reminder-worker.ts | 129.1 | +| src/lib/jobs/reminder/google-health-sync.ts | 88.5 | +| src/lib/whoop/sync.ts | 79.5 | +| src/lib/whoop/sync-body.ts | 63 | +| src/lib/insights/bucket-series.ts | 60 | +| src/lib/whoop/oauth-descriptor.ts | 60 | +| src/lib/whoop/sync-core.ts | 60 | +| src/instrumentation.ts | 43.4 | +| src/lib/import/unzip-export-xml.ts | 31.4 | +| src/lib/jobs/integration-status-null-probe.ts | 31.4 | + +### at 1099 ms, 54 ms + +| self time by module | ms | +| --- | ---: | +| node:internal/modules/cjs/loader | 23.9 | +| turbopack:///[turbopack]/nodejs/runtime/build-base.ts | 10.6 | +| turbopack:///[turbopack]/shared/runtime/async-module.ts | 5.8 | +| src/lib/daily/today-overview.ts | 4.5 | +| src/lib/jobs/reminder-worker.ts | 3 | +| node:fs | 3 | +| npm:pg-boss | 1.5 | +| npm:zod | 1.5 | + +| inclusive, application modules | ms | +| --- | ---: | +| src/lib/jobs/reminder-worker.ts | 52.3 | +| src/lib/jobs/reminder/insights-handlers.ts | 22.3 | +| src/lib/ai/prompts/arrival-reaction.ts | 12 | +| src/lib/daily/score-steady.ts | 10.5 | +| src/lib/dashboard/bands.ts | 6 | +| src/lib/jobs/data-arrival.ts | 4.5 | +| src/lib/dashboard/snapshot-read.ts | 4.5 | +| src/lib/daily/today-overview.ts | 4.5 | +| src/lib/daily/priority-item-key.ts | 4.5 | +| src/lib/ai/prompts/status-batch.ts | 3 | + +### at 1309 ms, 51 ms + +| self time by module | ms | +| --- | ---: | +| node:internal/modules/cjs/loader | 28 | +| turbopack:///[turbopack]/nodejs/runtime/build-base.ts | 6 | +| node:fs | 4.5 | +| turbopack:///[turbopack]/shared/runtime/async-module.ts | 3 | +| turbopack:///[turbopack]/nodejs/runtime/runtime-base.ts | 1.5 | +| src/lib/jobs/failure-condition.ts | 1.5 | +| src/lib/labs/parse-reference-range.ts | 1.5 | +| npm:zod | 1.5 | +| node:internal/linkedlist | 1.5 | +| (native) | 0.9 | + +| inclusive, application modules | ms | +| --- | ---: | +| src/lib/jobs/reminder-worker.ts | 48.2 | +| src/lib/jobs/reminder/ops-handlers.ts | 14.1 | +| src/lib/medications/ingredients/label-extract.ts | 3 | +| src/lib/export/restore-backup.ts | 2.1 | +| src/lib/jobs/failure-condition.ts | 1.5 | +| src/lib/documents/ai-runs/summary-run.ts | 1.5 | +| src/lib/documents/auto-stage-labs.ts | 1.5 | +| src/lib/documents/ai-runs/store.ts | 1.5 | +| src/lib/labs/parse-reference-range.ts | 1.5 | +| src/lib/labs/ocr-upload.ts | 1.5 | + +## Self time by module until the worker is ready + +| module | ms | +| --- | ---: | +| (native) | 230.8 | +| node:internal/modules/cjs/loader | 223.6 | +| turbopack:///[turbopack]/nodejs/runtime/build-base.ts | 124.7 | +| npm:next | 98.8 | +| wasm://wasm/00d1cb12 | 62.6 | +| npm:@prisma/client | 56.2 | +| node:fs | 34.6 | +| node:internal/bootstrap/realm | 28.7 | +| npm:pg | 28.3 | +| npm:zod | 27 | +| npm:pg-boss | 24.6 | +| node:internal/modules/esm/utils | 22.5 | +| turbopack:///[turbopack]/shared/runtime/async-module.ts | 20.6 | +| npm:luxon | 17 | +| src/lib/i18n/server-catalogs.ts | 15.1 | +| npm:yaml | 15 | +| npm:pg-pool | 13 | +| node:internal/deps/undici/undici | 10.4 | +| node:path | 7.8 | +| turbopack:///[turbopack]/shared/runtime/runtime-utils.ts | 7.5 | +| node:internal/async_hooks | 7.5 | +| node:internal/process/task_queues | 7.3 | +| node:internal/modules/package_json_reader | 6 | +| npm:undici | 6 | +| npm:pg-protocol | 6 | + +## Heap snapshot after 300 s idle (live heap after a full GC) + +160.1 MB in 1275329 nodes; module source strings over 50 KB: 39.8 MB. + +- parsed locale catalogs: 8 objects, 7 MB retained + +| node type | MB | +| --- | ---: | +| string | 68.7 | +| array | 26.4 | +| code | 21.1 | +| object | 14.1 | +| hidden | 8.9 | +| native | 8 | +| object shape | 5.9 | +| closure | 5.2 | +| concatenated string | 1.3 | +| regexp | 0.2 | +| sliced string | 0.2 | +| number | 0.1 | +| synthetic | 0 | +| symbol | 0 | +| bigint | 0 | + +| constructor | MB | count | +| --- | ---: | ---: | +| array:(object elements) | 20.4 | 23797 | +| object:Object | 10.2 | 274231 | +| native:system / JSArrayBufferData | 7.6 | 171 | +| array:(object properties) | 3.6 | 6512 | +| array: | 2.4 | 10502 | +| closure: | 1.6 | 29042 | +| object:system / Context | 1.6 | 28873 | +| closure:native_bind | 1 | 20782 | +| object:Array | 0.6 | 19679 | +| object:a | 0.4 | 4438 | +| native:Node / histogram | 0.4 | 1 | +| closure:get | 0.3 | 4736 | +| object:d | 0.3 | 4425 | +| object:Set | 0.2 | 5673 | +| closure:set | 0.2 | 3432 | +| object:Module | 0.2 | 1958 | +| closure:i | 0.1 | 1581 | +| closure:n | 0.1 | 1572 | +| object:Promise | 0.1 | 1277 | +| object:n | 0.1 | 1881 | + +| largest retained subtrees | MB | retaining path | +| --- | ---: | --- | +| object:t | 33.9 | [1] > [10] > 60 > extension > global > prisma > [1] | +| object:global | 7.9 | [1] > [10] > 60 > extension | +| object:system / Context | 7.4 | Node / PrincipalRealm > base_object_list > Node / BaseObjectList > [25] > Node / ModuleWrap > native_to_javascript > ModuleWrap > 2 > [4] > context | +| object:Module | 5.6 | [1] > [10] > 97 > process > nextTick > patchedNextTick > context > system / Context > require > require > cache > .n | +| code:.next/server/chu | 5.6 | require > cache > Object > .next/server/chunks/_1kwfnjg._.js > Module > exports > Array > [1] > > shared > script | +| code:.next/server/chu | 5.6 | global > prisma > [1] > t > _engineConfig > Object > compilerWasm > Object > getRuntime > getRuntime > shared > script | +| object:Generator | 4.4 | [6] > principal_realm > Node / PrincipalRealm > base_object_list > Node / BaseObjectList > [26] > Node / ModuleWrap > native_to_javascript > ModuleWrap > 2 > [4 | +| string:AGFzbQEAAAABtgRLYAN/f38Bf2ACf38Bf2ACf38AYAF/AGADf39/AGAEf39/ | 4.4 | [1] > [21] > 62203 | +| object:Object | 3.3 | system / Context > require > require > cache > Object > .next/server/chunks/[turbopack]_runtime.js > Module > export | + +| largest strings | MB | +| --- | ---: | +| module.exports=[234494,A=>{"use strict";var e,C,g,I,Q=Object.defineProperty,B={},s={$:()=> | 5.6 | +| module.exports=[17856,A=>{"use strict";var e,C,g,I,Q=Object.defineProperty,B={},s={$:()=>i | 5.6 | +| module.exports=[17856,A=>{"use strict";var e,C,g,I,Q=Object.defineProperty,B={},s={$:()=>i | 5.6 | +| const wasm = "AGFzbQEAAAABtgRLYAN/f38Bf2ACf38Bf2ACf38AYAF/AGADf39/AGAEf39/fwBgBX9/f39/AGAB | 4.4 | +| AGFzbQEAAAABtgRLYAN/f38Bf2ACf38Bf2ACf38AYAF/AGADf39/AGAEf39/fwBgBX9/f39/AGABfwF/YAZ/f39/f3 | 4.4 | +| lliADOAUngIEAACgKwAgBgAAoSsAIAcAAKErACAIAACiKwAgCQAAoysAIAoAAKQrACALAAClKwAgDAAApisAIA0AAK | 1.9 | +| (()=>{var e,t,r={"./dist/build/webpack/alias/react-dom-server.js"(e,t,r){"use strict";var | 1.3 | +| module.exports=[842865,(e,t,r)=>{let i=e.r(522734);t.exports=function(e){return e?/-----BE | 1.2 | +| module.exports=[842865,(e,t,r)=>{let i=e.r(522734);t.exports=function(e){return e?/-----BE | 1.2 | +| module.exports=[799092,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value: | 1.1 | +| // v1.4.34 web-freeze. Schema is locked additive-only until iOS launch // + v1.5.0 marker. | 1.1 | +| module.exports=[971743,(A,e,t)=>{"use strict";let r=A.r(57328),{addAbortListener:s}=A.r(68 | 1.1 | +| module.exports=[835717,(A,e,t)=>{"use strict";let r=A.r(57328),{addAbortListener:s}=A.r(68 | 1 | +| module.exports=[767189,e=>e.a(async(t,a)=>{try{var n=e.i(64105),r=e.i(563616),i=t([n]);[n] | 0.8 | +| module.exports=[594108,806176,674750,98746,t=>{"use strict";var i;let r;function n(t){retu | 0.7 | diff --git a/src/lib/ai/coach/refusal.ts b/src/lib/ai/coach/refusal.ts index 2a0607496..90a053109 100644 --- a/src/lib/ai/coach/refusal.ts +++ b/src/lib/ai/coach/refusal.ts @@ -388,7 +388,17 @@ function buildDerivedHealthTokens(): ReadonlySet { return tokens; } -const DERIVED_HEALTH_TOKENS = buildDerivedHealthTokens(); +let memoDerivedHealthTokensCache: + ReturnType | undefined; +/** Built on first use, not at module load: it reads every shipped catalog. */ +function memoDerivedHealthTokens(): ReturnType< + typeof memoDerivedHealthTokensBuild +> { + return (memoDerivedHealthTokensCache ??= memoDerivedHealthTokensBuild()); +} +function memoDerivedHealthTokensBuild() { + return buildDerivedHealthTokens(); +} /** * Test seam: the guard suite asserts the derived set is non-empty and that @@ -396,13 +406,13 @@ const DERIVED_HEALTH_TOKENS = buildDerivedHealthTokens(); * metric labels fails the suite rather than quietly narrowing the allow-list. */ export function derivedHealthTokens(): ReadonlySet { - return DERIVED_HEALTH_TOKENS; + return memoDerivedHealthTokens(); } /** True when any whole word of the message is a health noun from the bundles. */ function matchesDerivedHealthToken(message: string): boolean { for (const word of healthWordTokens(message)) { - if (DERIVED_HEALTH_TOKENS.has(word)) return true; + if (memoDerivedHealthTokens().has(word)) return true; } return false; } diff --git a/src/lib/documents/auto-stage-labs.ts b/src/lib/documents/auto-stage-labs.ts index 4250be7df..013e0d9b1 100644 --- a/src/lib/documents/auto-stage-labs.ts +++ b/src/lib/documents/auto-stage-labs.ts @@ -152,11 +152,19 @@ function buildAnalyteNamePattern(): RegExp { ); } -const DERIVED_ANALYTE_NAMES = buildAnalyteNamePattern(); +let derivedAnalyteNamesCache: + ReturnType | undefined; +/** Built on first use, not at module load: it reads every shipped catalog. */ +function derivedAnalyteNames(): ReturnType { + return (derivedAnalyteNamesCache ??= derivedAnalyteNamesBuild()); +} +function derivedAnalyteNamesBuild() { + return buildAnalyteNamePattern(); +} /** Test seam: the guard suite pins that the derived pattern covers every locale. */ export function derivedAnalyteNamePattern(): RegExp { - return DERIVED_ANALYTE_NAMES; + return derivedAnalyteNames(); } /** @@ -175,7 +183,7 @@ export function looksLikeLabDocument(text: string): boolean { for (const signal of LAB_SIGNALS) { const hit = signal.test(folded) || - (signal === ANALYTE_SIGNAL && DERIVED_ANALYTE_NAMES.test(folded)); + (signal === ANALYTE_SIGNAL && derivedAnalyteNames().test(folded)); if (hit && ++hits >= 2) return true; } return false; diff --git a/src/lib/fhir/lab-loinc.ts b/src/lib/fhir/lab-loinc.ts index 8b2a51158..af29c284c 100644 --- a/src/lib/fhir/lab-loinc.ts +++ b/src/lib/fhir/lab-loinc.ts @@ -355,14 +355,24 @@ function buildLocalisedAliases(): Record { return index; } -const LOCALISED_LAB_ALIASES = buildLocalisedAliases(); +let memoLocalisedLabAliasesCache: + ReturnType | undefined; +/** Built on first use, not at module load: it reads every shipped catalog. */ +function memoLocalisedLabAliases(): ReturnType< + typeof memoLocalisedLabAliasesBuild +> { + return (memoLocalisedLabAliasesCache ??= memoLocalisedLabAliasesBuild()); +} +function memoLocalisedLabAliasesBuild() { + return buildLocalisedAliases(); +} /** * Test seam: the guard suite asserts the derived index covers every shipped * locale and contradicts no hand-written alias. Production reads the constant. */ export function localisedLabAliases(): Readonly> { - return LOCALISED_LAB_ALIASES; + return memoLocalisedLabAliases(); } /** @@ -462,7 +472,7 @@ export function resolveLabCoding( // every resolution that worked before working the same way. const resolvedKey = LAB_LOINC_BY_KEY[key] ? key - : (LAB_ALIASES[key] ?? LOCALISED_LAB_ALIASES[key]); + : (LAB_ALIASES[key] ?? memoLocalisedLabAliases()[key]); if (!resolvedKey) return null; const mapping = LAB_LOINC_BY_KEY[resolvedKey]; if (!mapping) return null; diff --git a/src/lib/fhir/lab-qualitative.ts b/src/lib/fhir/lab-qualitative.ts index 7b5448ddc..02ea7bdda 100644 --- a/src/lib/fhir/lab-qualitative.ts +++ b/src/lib/fhir/lab-qualitative.ts @@ -110,7 +110,14 @@ function buildLocalisedIndex(): Record { return index; } -const LOCALISED_SNOMED = buildLocalisedIndex(); +let localisedSnomedCache: ReturnType | undefined; +/** Built on first use, not at module load: it reads every shipped catalog. */ +function localisedSnomed(): ReturnType { + return (localisedSnomedCache ??= localisedSnomedBuild()); +} +function localisedSnomedBuild() { + return buildLocalisedIndex(); +} /** * Test seam: the guard suite asserts the derived index covers every shipped @@ -120,7 +127,7 @@ const LOCALISED_SNOMED = buildLocalisedIndex(); export function localisedQualitativeIndex(): Readonly< Record > { - return LOCALISED_SNOMED; + return localisedSnomed(); } /** @@ -144,7 +151,7 @@ export function qualitativeValueConcept( valueText: string, ): FhirCodeableConcept { const key = normaliseQualitativeKey(valueText); - const concept = HAND_WRITTEN_SNOMED[key] ?? LOCALISED_SNOMED[key]; + const concept = HAND_WRITTEN_SNOMED[key] ?? localisedSnomed()[key]; if (!concept) { return { text: valueText }; } diff --git a/src/lib/mcp/__tests__/rich-reads.test.ts b/src/lib/mcp/__tests__/rich-reads.test.ts index 92f9e1a8b..10273c96e 100644 --- a/src/lib/mcp/__tests__/rich-reads.test.ts +++ b/src/lib/mcp/__tests__/rich-reads.test.ts @@ -42,7 +42,7 @@ import { getLabHistory, resolveRichMetric, MCP_METRIC_STATUS_DISCOVERY, - LOCALISED_METRIC_NAME_COLLISIONS, + localisedMetricNameCollisions, metricStatusDiscoveryRows, } from "../rich-reads"; import { locales } from "@/lib/i18n/config"; @@ -292,7 +292,7 @@ describe("resolveRichMetric — localised metric names", () => { // the set is empty and pinned empty: a NEW collision fails here instead of // silently resolving to whichever entry happened to be built first. it("refuses a word two different metrics claim, and only those words", () => { - expect([...LOCALISED_METRIC_NAME_COLLISIONS].sort()).toEqual([]); + expect([...localisedMetricNameCollisions()].sort()).toEqual([]); }); // The property, not a sample: over every metric and every shipped locale, diff --git a/src/lib/mcp/nutrients-read.ts b/src/lib/mcp/nutrients-read.ts index 9b9cd33d2..cd2e4e365 100644 --- a/src/lib/mcp/nutrients-read.ts +++ b/src/lib/mcp/nutrients-read.ts @@ -85,13 +85,21 @@ export const NUTRIENT_LABELS: Readonly> = * `locales`, so a seventh language starts resolving the moment its bundle * lands, with no edit here. */ -const NUTRIENT_NAME_INDEX = buildLocalisedLabelIndex( - NUTRIENT_CODES.map((code) => ({ - id: code, - messageKey: nutrientLabelKey(code), - value: code, - })), -); +let nutrientNameIndexCache: + ReturnType | undefined; +/** Built on first use, not at module load: it reads every shipped catalog. */ +function nutrientNameIndex(): ReturnType { + return (nutrientNameIndexCache ??= nutrientNameIndexBuild()); +} +function nutrientNameIndexBuild() { + return buildLocalisedLabelIndex( + NUTRIENT_CODES.map((code) => ({ + id: code, + messageKey: nutrientLabelKey(code), + value: code, + })), + ); +} /** Overview mode (no `nutrient` arg) mirrors `GET /api/nutrients`'s bounds. */ const OVERVIEW_DEFAULT_DAYS = 14; @@ -148,7 +156,7 @@ export function resolveNutrientCode(input: string): NutrientCode | null { if (!raw) return null; const key = raw.toLowerCase().replace(/[\s-]+/g, "_"); if (isNutrientCode(key)) return key; - return NUTRIENT_NAME_INDEX.index.get(foldLabel(raw)) ?? null; + return nutrientNameIndex().index.get(foldLabel(raw)) ?? null; } /** diff --git a/src/lib/mcp/rich-reads.ts b/src/lib/mcp/rich-reads.ts index 9d15570fa..a7b8cb000 100644 --- a/src/lib/mcp/rich-reads.ts +++ b/src/lib/mcp/rich-reads.ts @@ -560,7 +560,13 @@ export async function metricStatusDiscoveryRows(userId: string): Promise< // // The alias tables stay as they are and stay English: `resting_hr`, `spo2`, // `vo2max` are protocol slugs, not translations of anything. -const LOCALISED_METRIC_INDEX = (() => { +let localisedMetricIndexCache: + ReturnType | undefined; +/** Built on first use, not at module load: it reads every shipped catalog. */ +function localisedMetricIndex(): ReturnType { + return (localisedMetricIndexCache ??= buildLocalisedMetricIndex()); +} +function buildLocalisedMetricIndex() { const exposed = new Map(); const claim = (metric: RichMetric | null) => { if (metric && !exposed.has(metric.measurementType)) { @@ -579,7 +585,7 @@ const LOCALISED_METRIC_INDEX = (() => { value: exposed.get(type as MeasurementType) ?? null, })); return buildLocalisedLabelIndex(entries); -})(); +} /** * Words two different measurement types claim in some bundle, and which @@ -587,8 +593,9 @@ const LOCALISED_METRIC_INDEX = (() => { * is empty: a NEW member is a regression to look at rather than something to * resolve by coin flip. */ -export const LOCALISED_METRIC_NAME_COLLISIONS: ReadonlySet = - LOCALISED_METRIC_INDEX.ambiguous; +export function localisedMetricNameCollisions(): ReadonlySet { + return localisedMetricIndex().ambiguous; +} /** * Resolve a free-text metric name to a single scalar series. Forgiving for an @@ -632,7 +639,7 @@ export function resolveRichMetric(input: string): RichMetric | null { // on PULSE_WAVE_VELOCITY, because "pulse-wave velocity" contains "puls". // A value-less hit (a metric this surface does not expose) falls through // rather than short-circuiting, so the English behaviour below is untouched. - const localised = LOCALISED_METRIC_INDEX.index.get(foldLabel(raw)); + const localised = localisedMetricIndex().index.get(foldLabel(raw)); if (localised) return localised; // 5. English display-name substring match (e.g. "heart-rate variability"). diff --git a/src/lib/medications/glp1-side-effect-tag-match.ts b/src/lib/medications/glp1-side-effect-tag-match.ts index 1dcd7d880..4ce95264d 100644 --- a/src/lib/medications/glp1-side-effect-tag-match.ts +++ b/src/lib/medications/glp1-side-effect-tag-match.ts @@ -64,7 +64,14 @@ function buildIndex(): Map { return index; } -const TAG_INDEX = buildIndex(); +let tagIndexCache: ReturnType | undefined; +/** Built on first use, not at module load: it reads every shipped catalog. */ +function tagIndex(): ReturnType { + return (tagIndexCache ??= tagIndexBuild()); +} +function tagIndexBuild() { + return buildIndex(); +} /** * Test seam: the guard suite asserts the index covers every locale and that no @@ -74,7 +81,7 @@ export function glp1SideEffectTagIndex(): ReadonlyMap< string, Glp1SideEffectTag > { - return TAG_INDEX; + return tagIndex(); } /** @@ -89,7 +96,7 @@ export function glp1SideEffectTagIndex(): ReadonlyMap< export function resolveGlp1SideEffectTag( raw: string, ): Glp1SideEffectTag | null { - return TAG_INDEX.get(normaliseSideEffectTag(raw)) ?? null; + return tagIndex().get(normaliseSideEffectTag(raw)) ?? null; } /** What one `MoodEntry.tags` column contributed. */