@mandujs/core 0.53.2 → 0.54.0
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/README.md +654 -654
- package/package.json +2 -2
- package/src/a11y/__tests__/run-audit.test.ts +333 -333
- package/src/a11y/fix-hints.ts +76 -76
- package/src/a11y/index.ts +18 -18
- package/src/a11y/run-audit.ts +394 -394
- package/src/a11y/types.ts +125 -125
- package/src/auth/__tests__/login.test.ts +1 -1
- package/src/auth/__tests__/password.test.ts +122 -122
- package/src/auth/__tests__/tokens.test.ts +274 -274
- package/src/auth/__tests__/verification.test.ts +274 -274
- package/src/auth/index.ts +76 -76
- package/src/auth/login.ts +225 -225
- package/src/auth/password.ts +120 -120
- package/src/auth/reset.ts +243 -243
- package/src/auth/tokens.ts +612 -612
- package/src/auth/verification.ts +253 -253
- package/src/brain/__tests__/redactor.test.ts +94 -94
- package/src/brain/adapters/__tests__/_helpers.ts +83 -83
- package/src/brain/adapters/__tests__/anthropic-oauth.test.ts +196 -196
- package/src/brain/adapters/__tests__/chatgpt-auth.test.ts +193 -193
- package/src/brain/adapters/__tests__/openai-oauth.test.ts +209 -209
- package/src/brain/adapters/__tests__/resolver.test.ts +143 -143
- package/src/brain/adapters/anthropic-oauth.ts +1 -1
- package/src/brain/adapters/chatgpt-auth.ts +300 -300
- package/src/brain/adapters/index.ts +319 -319
- package/src/brain/adapters/oauth-flow.ts +439 -439
- package/src/brain/consent.ts +240 -240
- package/src/brain/credentials.ts +396 -396
- package/src/bundler/__tests__/build-runner.ts +113 -113
- package/src/bundler/__tests__/dev-reliability.test.ts +619 -619
- package/src/bundler/__tests__/extended-watch.test.ts +711 -711
- package/src/bundler/__tests__/fast-refresh.test.ts +10 -10
- package/src/bundler/__tests__/hdr.test.ts +353 -353
- package/src/bundler/__tests__/hmr-client.test.ts +532 -532
- package/src/bundler/__tests__/manifest-schema.test.ts +266 -266
- package/src/bundler/__tests__/prod-smoke.test.ts +138 -138
- package/src/bundler/__tests__/reverse-import-graph.test.ts +519 -519
- package/src/bundler/__tests__/slot-dispatch.test.ts +573 -573
- package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -286
- package/src/bundler/__tests__/vendor-cache.test.ts +455 -455
- package/src/bundler/budget.ts +404 -404
- package/src/bundler/build.test.ts +179 -179
- package/src/bundler/build.ts +55 -55
- package/src/bundler/dev.ts +42 -42
- package/src/bundler/manifest-schema.ts +301 -301
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +263 -263
- package/src/bundler/plugins/__tests__/react-compiler-config.test.ts +83 -83
- package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -110
- package/src/bundler/plugins/block-generated-imports.ts +155 -155
- package/src/bundler/plugins/index.ts +83 -83
- package/src/bundler/plugins/react-compiler-config.ts +108 -108
- package/src/bundler/plugins/react-compiler-lint.ts +253 -253
- package/src/bundler/plugins/react-compiler.ts +162 -162
- package/src/bundler/reverse-import-graph.ts +339 -339
- package/src/bundler/safe-build.test.ts +201 -201
- package/src/bundler/safe-build.ts +103 -103
- package/src/bundler/scenario-matrix.ts +229 -229
- package/src/bundler/types.ts +10 -10
- package/src/bundler/vendor-cache-types.ts +130 -130
- package/src/bundler/vendor-cache.ts +526 -526
- package/src/change/snapshot.ts +18 -18
- package/src/client/Form.tsx +105 -105
- package/src/client/Link.tsx +9 -9
- package/src/client/__tests__/use-sse.test.ts +153 -153
- package/src/client/globals.ts +1 -1
- package/src/client/hooks.ts +362 -362
- package/src/client/hydrate.ts +340 -340
- package/src/client/prefetch-helper.ts +55 -55
- package/src/client/router.ts +11 -11
- package/src/client/runtime.ts +47 -47
- package/src/client/serialize.ts +404 -404
- package/src/client/use-fetch.ts +6 -6
- package/src/client/use-head.ts +197 -197
- package/src/client/use-sse.ts +378 -378
- package/src/client/window-state.ts +101 -101
- package/src/components/Image.tsx +162 -162
- package/src/config/validate.ts +3 -3
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/collection.ts +8 -8
- package/src/content/content-layer.ts +7 -7
- package/src/content/data-store.ts +245 -245
- package/src/content/frontmatter.ts +189 -189
- package/src/content/loader-context.ts +171 -171
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +172 -172
- package/src/content/loaders/glob.ts +253 -253
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/prebuild.test.ts +571 -571
- package/src/content/prebuild.ts +636 -636
- package/src/content/schema.ts +20 -20
- package/src/content/sidebar.ts +630 -630
- package/src/content/slug.ts +110 -110
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/define.ts +11 -11
- package/src/contract/index.ts +1 -1
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +410 -410
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/route-helpers.ts +1 -1
- package/src/contract/rpc.ts +443 -443
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/db/__tests__/db.test.ts +482 -482
- package/src/db/index.ts +138 -138
- package/src/db/migrations/history-table.ts +345 -345
- package/src/db/migrations/index.ts +3 -3
- package/src/db/migrations/lock.ts +324 -324
- package/src/db/migrations/runner.ts +650 -650
- package/src/deploy/cache.ts +140 -140
- package/src/deploy/compile/vercel.ts +344 -344
- package/src/deploy/index.ts +87 -87
- package/src/deploy/inference/brain.ts +268 -268
- package/src/deploy/inference/context.ts +182 -182
- package/src/deploy/inference/filling-extract.ts +245 -245
- package/src/deploy/inference/heuristic.ts +182 -182
- package/src/deploy/intent.ts +173 -173
- package/src/deploy/plan.ts +178 -178
- package/src/design/__tests__/agents-link.test.ts +109 -109
- package/src/design/__tests__/extract-patch-diff.test.ts +265 -265
- package/src/design/__tests__/lint.test.ts +110 -110
- package/src/design/__tests__/parser.test.ts +195 -195
- package/src/design/__tests__/tailwind-theme.test.ts +229 -229
- package/src/design/agents-link.ts +165 -165
- package/src/design/diff.ts +138 -138
- package/src/design/extract.ts +285 -285
- package/src/design/index.ts +102 -102
- package/src/design/lint.ts +209 -209
- package/src/design/parser.ts +555 -555
- package/src/design/patch.ts +241 -241
- package/src/design/scaffold.ts +147 -147
- package/src/design/tailwind-theme.ts +441 -441
- package/src/design/types.ts +210 -210
- package/src/desktop/__tests__/webview-fallback.test.ts +254 -254
- package/src/desktop/__tests__/window.test.ts +248 -248
- package/src/desktop/__tests__/worker.test.ts +266 -266
- package/src/desktop/index.ts +43 -43
- package/src/desktop/types.ts +158 -158
- package/src/desktop/worker.ts +180 -180
- package/src/dev-error-overlay/__tests__/overlay-injector.test.ts +241 -241
- package/src/dev-error-overlay/index.ts +30 -30
- package/src/dev-error-overlay/overlay-injector.ts +243 -243
- package/src/dev-error-overlay/overlay-styles.ts +52 -52
- package/src/dev-error-overlay/types.ts +66 -66
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +25 -25
- package/src/devtools/client/catchers/error-catcher.ts +344 -344
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/mandu-character.tsx +331 -331
- package/src/devtools/client/components/overlay.tsx +368 -368
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +30 -30
- package/src/devtools/client/components/panel/islands-panel.tsx +320 -320
- package/src/devtools/client/components/panel/network-panel.tsx +291 -291
- package/src/devtools/client/components/panel/panel-container.tsx +500 -500
- package/src/devtools/client/components/panel/preview-panel.tsx +46 -46
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/client/persistence.ts +335 -335
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +265 -265
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +450 -450
- package/src/devtools/types.ts +35 -35
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +233 -233
- package/src/devtools/worker/worker-manager.ts +410 -410
- package/src/diagnose/__tests__/checks.test.ts +451 -380
- package/src/diagnose/checks.ts +832 -720
- package/src/diagnose/index.ts +17 -16
- package/src/diagnose/run.ts +93 -91
- package/src/diagnose/types.ts +53 -53
- package/src/email/__tests__/email.test.ts +355 -355
- package/src/email/index.ts +282 -282
- package/src/email/smtp.ts +64 -64
- package/src/error/domains.ts +265 -265
- package/src/error/result.ts +60 -60
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/__tests__/session-sqlite.test.ts +454 -454
- package/src/filling/auth.ts +308 -308
- package/src/filling/cookie-codec.ts +299 -299
- package/src/filling/deps.ts +265 -265
- package/src/filling/session-sqlite.ts +617 -617
- package/src/filling/sse.ts +5 -5
- package/src/filling/ws.ts +78 -78
- package/src/generator/index.ts +3 -3
- package/src/guard/__tests__/design-inline-class.test.ts +219 -219
- package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -347
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/define-rule.ts +243 -243
- package/src/guard/design-inline-class.ts +393 -393
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/graph.ts +1 -1
- package/src/guard/healing.ts +36 -36
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/reporter.ts +442 -442
- package/src/guard/rule-presets.ts +379 -379
- package/src/guard/semantic-slots.ts +1 -1
- package/src/guard/suggestions.ts +358 -358
- package/src/guard/tsgolint-bridge.ts +512 -512
- package/src/guard/types.ts +348 -348
- package/src/guard/watcher.ts +405 -405
- package/src/i18n/define.ts +126 -126
- package/src/i18n/index.ts +52 -52
- package/src/i18n/message-registry.ts +173 -173
- package/src/i18n/types.ts +112 -112
- package/src/id/index.ts +105 -105
- package/src/index.ts +2 -2
- package/src/kitchen/api/agent-devtools-api.ts +779 -544
- package/src/kitchen/api/errors-grouping.ts +126 -0
- package/src/kitchen/kitchen-handler.ts +192 -63
- package/src/kitchen/kitchen-ui.ts +842 -464
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/middleware/bridge.ts +147 -147
- package/src/middleware/compose.ts +134 -134
- package/src/middleware/compress.ts +62 -62
- package/src/middleware/cors.ts +47 -47
- package/src/middleware/csrf.ts +328 -328
- package/src/middleware/define.ts +132 -132
- package/src/middleware/jwt.ts +134 -134
- package/src/middleware/logger.ts +58 -58
- package/src/middleware/oauth/__tests__/oauth.test.ts +1 -1
- package/src/middleware/oauth/index.ts +505 -505
- package/src/middleware/oauth/providers.ts +115 -115
- package/src/middleware/rate-limit/index.ts +522 -522
- package/src/middleware/rate-limit/sqlite-store.ts +382 -382
- package/src/middleware/scheduler-cron.ts +96 -96
- package/src/middleware/secure/__tests__/secure.test.ts +360 -360
- package/src/middleware/secure/csp.ts +193 -193
- package/src/middleware/session.ts +174 -174
- package/src/middleware/timeout.ts +55 -55
- package/src/observability/logger-adapter.ts +36 -36
- package/src/observability/sqlite-store.ts +254 -254
- package/src/openapi/generator.ts +1 -1
- package/src/openapi/openapi.test.ts +43 -43
- package/src/perf/__tests__/user-marks.test.ts +354 -354
- package/src/perf/index.ts +133 -133
- package/src/plugins/__tests__/lifecycle-integration.test.ts +272 -272
- package/src/plugins/__tests__/runner.test.ts +409 -409
- package/src/plugins/define.ts +124 -124
- package/src/plugins/examples/dep-check-plugin.ts +80 -80
- package/src/plugins/examples/prerender-cache-plugin.ts +111 -111
- package/src/plugins/examples/sitemap-plugin.ts +65 -65
- package/src/plugins/runner.ts +361 -361
- package/src/plugins/types.ts +368 -368
- package/src/report/index.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +32 -32
- package/src/resource/ddl/__tests__/diff.test.ts +639 -639
- package/src/resource/ddl/__tests__/emit.test.ts +823 -823
- package/src/resource/ddl/__tests__/snapshot.test.ts +499 -499
- package/src/resource/ddl/emit.ts +559 -559
- package/src/resource/ddl/persistence-types.ts +218 -218
- package/src/resource/ddl/type-map.ts +223 -223
- package/src/resource/ddl/types.ts +232 -232
- package/src/resource/generator-repo.ts +630 -630
- package/src/resource/schema.ts +1 -1
- package/src/router/fs-patterns.test.ts +96 -96
- package/src/router/fs-routes.ts +6 -6
- package/src/routes/index.ts +74 -74
- package/src/routes/metadata-routes.ts +427 -427
- package/src/routes/types.ts +341 -341
- package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -141
- package/src/runtime/__tests__/hdr-client.test.ts +223 -223
- package/src/runtime/__tests__/http-errors.test.ts +117 -117
- package/src/runtime/__tests__/not-found.test.ts +152 -152
- package/src/runtime/adapter.ts +47 -47
- package/src/runtime/boundary.tsx +252 -252
- package/src/runtime/cache.ts +494 -494
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/fast-refresh-runtime.ts +322 -322
- package/src/runtime/handler.ts +65 -65
- package/src/runtime/handlers.ts +300 -300
- package/src/runtime/http-errors.ts +113 -113
- package/src/runtime/image-handler.ts +1 -1
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/middleware.ts +264 -264
- package/src/runtime/not-found.ts +93 -93
- package/src/runtime/openapi-endpoint.ts +236 -236
- package/src/runtime/ppr.ts +74 -74
- package/src/runtime/registry.ts +171 -171
- package/src/runtime/router.ts +105 -105
- package/src/runtime/server.ts +67 -67
- package/src/runtime/shims.ts +48 -48
- package/src/runtime/ssr.ts +24 -31
- package/src/runtime/streaming-ssr.ts +32 -37
- package/src/runtime/trace.ts +144 -144
- package/src/scheduler/validate.ts +169 -169
- package/src/seo/index.ts +219 -219
- package/src/seo/integration/ssr.ts +306 -306
- package/src/seo/render/basic.ts +435 -435
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +197 -197
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +127 -127
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +104 -104
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +588 -588
- package/src/slot/validator.ts +39 -39
- package/src/storage/s3/__tests__/s3.test.ts +479 -479
- package/src/storage/s3/index.ts +412 -412
- package/src/testing/__tests__/assertions.test.ts +632 -632
- package/src/testing/__tests__/reporter.test.ts +454 -454
- package/src/testing/assertions.ts +986 -986
- package/src/testing/db.ts +157 -157
- package/src/testing/mocks.ts +203 -203
- package/src/testing/session.ts +190 -190
- package/src/types/branded.ts +56 -56
- package/src/types/index.ts +1 -1
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
- package/src/watcher/watcher.ts +18 -18
|
@@ -1,354 +1,354 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Phase 18.ψ — user-marks tests
|
|
3
|
-
*
|
|
4
|
-
* Covers the three public entry points (`time`, `timeAsync`, `createPerf`)
|
|
5
|
-
* plus the `/_mandu/heap` dashboard snapshot and OTel integration. Fixture
|
|
6
|
-
* style mirrors `tests/perf/perf.test.ts`: mutate `process.env.MANDU_PERF`,
|
|
7
|
-
* call the exported `_resetCacheForTesting` + `_resetUserMarksForTesting`
|
|
8
|
-
* helpers, restore between tests.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
describe,
|
|
13
|
-
it,
|
|
14
|
-
expect,
|
|
15
|
-
beforeEach,
|
|
16
|
-
afterEach,
|
|
17
|
-
} from "bun:test";
|
|
18
|
-
import {
|
|
19
|
-
time,
|
|
20
|
-
timeAsync,
|
|
21
|
-
createPerf,
|
|
22
|
-
collectPerfSnapshot,
|
|
23
|
-
recordFrameworkMeasurement,
|
|
24
|
-
_resetUserMarksForTesting,
|
|
25
|
-
_peekLastMarkForTesting,
|
|
26
|
-
PERF_BUFFER_LIMIT,
|
|
27
|
-
} from "../user-marks";
|
|
28
|
-
import {
|
|
29
|
-
isPerfEnabled,
|
|
30
|
-
_resetCacheForTesting,
|
|
31
|
-
} from "../index";
|
|
32
|
-
import {
|
|
33
|
-
Tracer,
|
|
34
|
-
setTracer,
|
|
35
|
-
resetTracer,
|
|
36
|
-
type Span,
|
|
37
|
-
type SpanExporter,
|
|
38
|
-
} from "../../observability/tracing";
|
|
39
|
-
|
|
40
|
-
function enablePerf(): void {
|
|
41
|
-
process.env.MANDU_PERF = "1";
|
|
42
|
-
_resetCacheForTesting();
|
|
43
|
-
_resetUserMarksForTesting();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function disablePerf(): void {
|
|
47
|
-
delete process.env.MANDU_PERF;
|
|
48
|
-
_resetCacheForTesting();
|
|
49
|
-
_resetUserMarksForTesting();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
describe("@mandujs/core/perf — user marks (time / timeAsync / createPerf)", () => {
|
|
53
|
-
afterEach(() => {
|
|
54
|
-
disablePerf();
|
|
55
|
-
resetTracer();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// ─── 1. Basic lifecycle ────────────────────────────────────────────────
|
|
59
|
-
|
|
60
|
-
describe("time(): mark lifecycle", () => {
|
|
61
|
-
beforeEach(() => enablePerf());
|
|
62
|
-
|
|
63
|
-
it("returns an end() function that records a non-negative duration", async () => {
|
|
64
|
-
const end = time("db-query");
|
|
65
|
-
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
66
|
-
const elapsedMs = end();
|
|
67
|
-
|
|
68
|
-
expect(typeof elapsedMs).toBe("number");
|
|
69
|
-
expect(elapsedMs).toBeGreaterThanOrEqual(0);
|
|
70
|
-
expect(Number.isFinite(elapsedMs)).toBe(true);
|
|
71
|
-
|
|
72
|
-
const last = _peekLastMarkForTesting();
|
|
73
|
-
expect(last?.name).toBe("db-query");
|
|
74
|
-
expect(last?.category).toBe("user");
|
|
75
|
-
expect(last?.durationMs).toBe(elapsedMs);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("end() is idempotent — second call returns 0 without double-recording", () => {
|
|
79
|
-
const end = time("idem");
|
|
80
|
-
const first = end();
|
|
81
|
-
const second = end();
|
|
82
|
-
|
|
83
|
-
expect(first).toBeGreaterThanOrEqual(0);
|
|
84
|
-
expect(second).toBe(0);
|
|
85
|
-
|
|
86
|
-
// Only one entry recorded.
|
|
87
|
-
const snap = collectPerfSnapshot();
|
|
88
|
-
const entry = snap.histogram.find((h) => h.name === "idem");
|
|
89
|
-
expect(entry?.count).toBe(1);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("custom category is honored in the histogram entry", () => {
|
|
93
|
-
const end = time("ssr-slot", { category: "ssr" });
|
|
94
|
-
end();
|
|
95
|
-
const last = _peekLastMarkForTesting();
|
|
96
|
-
expect(last?.category).toBe("ssr");
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// ─── 2. Async scope ────────────────────────────────────────────────────
|
|
101
|
-
|
|
102
|
-
describe("timeAsync(): async scope", () => {
|
|
103
|
-
beforeEach(() => enablePerf());
|
|
104
|
-
|
|
105
|
-
it("forwards the resolved value", async () => {
|
|
106
|
-
const value = await timeAsync("fetch-user", async () => {
|
|
107
|
-
await new Promise((resolve) => setTimeout(resolve, 2));
|
|
108
|
-
return { id: 123, name: "alice" };
|
|
109
|
-
});
|
|
110
|
-
expect(value).toEqual({ id: 123, name: "alice" });
|
|
111
|
-
const last = _peekLastMarkForTesting();
|
|
112
|
-
expect(last?.name).toBe("fetch-user");
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("forwards sync return values", async () => {
|
|
116
|
-
const value = await timeAsync("sync-op", () => 42);
|
|
117
|
-
expect(value).toBe(42);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("rethrows errors and still records the mark", async () => {
|
|
121
|
-
const boom = new Error("kaboom");
|
|
122
|
-
await expect(
|
|
123
|
-
timeAsync("failing", async () => {
|
|
124
|
-
throw boom;
|
|
125
|
-
}),
|
|
126
|
-
).rejects.toThrow("kaboom");
|
|
127
|
-
|
|
128
|
-
// Mark recorded even on error path.
|
|
129
|
-
const last = _peekLastMarkForTesting();
|
|
130
|
-
expect(last?.name).toBe("failing");
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
// ─── 3. Zero overhead when disabled ────────────────────────────────────
|
|
135
|
-
|
|
136
|
-
describe("disabled (MANDU_PERF unset)", () => {
|
|
137
|
-
beforeEach(() => disablePerf());
|
|
138
|
-
|
|
139
|
-
it("time() returns a shared no-op end function and records nothing", () => {
|
|
140
|
-
expect(isPerfEnabled()).toBe(false);
|
|
141
|
-
const end = time("ignored");
|
|
142
|
-
const result = end();
|
|
143
|
-
expect(result).toBe(0);
|
|
144
|
-
expect(_peekLastMarkForTesting()).toBeUndefined();
|
|
145
|
-
|
|
146
|
-
const snap = collectPerfSnapshot();
|
|
147
|
-
expect(snap.enabled).toBe(false);
|
|
148
|
-
expect(snap.totalCount).toBe(0);
|
|
149
|
-
expect(snap.histogram).toEqual([]);
|
|
150
|
-
expect(snap.recent).toEqual([]);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("timeAsync() invokes fn directly and returns its value", async () => {
|
|
154
|
-
const result = await timeAsync("ignored-async", async () => "ok");
|
|
155
|
-
expect(result).toBe("ok");
|
|
156
|
-
expect(_peekLastMarkForTesting()).toBeUndefined();
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it("createPerf() with enabled=false returns a no-op instance", () => {
|
|
160
|
-
const perf = createPerf({ enabled: false });
|
|
161
|
-
expect(perf.enabled).toBe(false);
|
|
162
|
-
const end = perf.time("nope");
|
|
163
|
-
expect(end()).toBe(0);
|
|
164
|
-
expect(_peekLastMarkForTesting()).toBeUndefined();
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
// ─── 4. createPerf scoped instance ─────────────────────────────────────
|
|
169
|
-
|
|
170
|
-
describe("createPerf()", () => {
|
|
171
|
-
it("defaults to the global gate when enabled is unset", () => {
|
|
172
|
-
enablePerf();
|
|
173
|
-
const perf = createPerf();
|
|
174
|
-
expect(perf.enabled).toBe(true);
|
|
175
|
-
perf.time("scoped")();
|
|
176
|
-
expect(_peekLastMarkForTesting()?.name).toBe("scoped");
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it("can force-enable when the global gate is off", async () => {
|
|
180
|
-
disablePerf();
|
|
181
|
-
const perf = createPerf({ enabled: false });
|
|
182
|
-
// Even a force-enable=false under a disabled global is no-op.
|
|
183
|
-
expect(perf.enabled).toBe(false);
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// ─── 5. OTel tracer integration ────────────────────────────────────────
|
|
188
|
-
|
|
189
|
-
describe("OTel integration", () => {
|
|
190
|
-
it("creates a child span when tracing is enabled, with mandu.perf.category attribute", () => {
|
|
191
|
-
enablePerf();
|
|
192
|
-
const captured: Span[] = [];
|
|
193
|
-
const capturing: SpanExporter = {
|
|
194
|
-
export(spans: Span[]): void {
|
|
195
|
-
captured.push(...spans);
|
|
196
|
-
},
|
|
197
|
-
};
|
|
198
|
-
setTracer(new Tracer({ enabled: true, customExporter: capturing }));
|
|
199
|
-
|
|
200
|
-
const end = time("db-query", { category: "user" });
|
|
201
|
-
end();
|
|
202
|
-
|
|
203
|
-
expect(captured.length).toBe(1);
|
|
204
|
-
expect(captured[0]!.name).toBe("db-query");
|
|
205
|
-
expect(captured[0]!.attributes["mandu.perf.category"]).toBe("user");
|
|
206
|
-
expect(
|
|
207
|
-
typeof captured[0]!.attributes["mandu.perf.duration_ms"],
|
|
208
|
-
).toBe("number");
|
|
209
|
-
expect(captured[0]!.status).toBe("ok");
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it("opt-out via { trace: false } skips span creation", () => {
|
|
213
|
-
enablePerf();
|
|
214
|
-
const captured: Span[] = [];
|
|
215
|
-
setTracer(
|
|
216
|
-
new Tracer({
|
|
217
|
-
enabled: true,
|
|
218
|
-
customExporter: {
|
|
219
|
-
export: (spans) => {
|
|
220
|
-
captured.push(...spans);
|
|
221
|
-
},
|
|
222
|
-
},
|
|
223
|
-
}),
|
|
224
|
-
);
|
|
225
|
-
|
|
226
|
-
const end = time("silent", { trace: false });
|
|
227
|
-
end();
|
|
228
|
-
|
|
229
|
-
expect(captured.length).toBe(0);
|
|
230
|
-
// Still recorded in histogram.
|
|
231
|
-
expect(_peekLastMarkForTesting()?.name).toBe("silent");
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it("timeAsync marks span status=error on thrown error", async () => {
|
|
235
|
-
enablePerf();
|
|
236
|
-
const captured: Span[] = [];
|
|
237
|
-
setTracer(
|
|
238
|
-
new Tracer({
|
|
239
|
-
enabled: true,
|
|
240
|
-
customExporter: {
|
|
241
|
-
export: (spans) => {
|
|
242
|
-
captured.push(...spans);
|
|
243
|
-
},
|
|
244
|
-
},
|
|
245
|
-
}),
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
await expect(
|
|
249
|
-
timeAsync("boom", async () => {
|
|
250
|
-
throw new Error("nope");
|
|
251
|
-
}),
|
|
252
|
-
).rejects.toThrow("nope");
|
|
253
|
-
|
|
254
|
-
expect(captured.length).toBe(1);
|
|
255
|
-
expect(captured[0]!.status).toBe("error");
|
|
256
|
-
expect(captured[0]!.errorMessage).toBe("nope");
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
// ─── 6. Histogram math ─────────────────────────────────────────────────
|
|
261
|
-
|
|
262
|
-
describe("histogram", () => {
|
|
263
|
-
beforeEach(() => enablePerf());
|
|
264
|
-
|
|
265
|
-
it("computes count / mean / min / max correctly over multiple marks", () => {
|
|
266
|
-
// Synthesize durations via the framework bridge so we get deterministic values.
|
|
267
|
-
recordFrameworkMeasurement("widget", 10);
|
|
268
|
-
recordFrameworkMeasurement("widget", 20);
|
|
269
|
-
recordFrameworkMeasurement("widget", 30);
|
|
270
|
-
recordFrameworkMeasurement("widget", 40);
|
|
271
|
-
|
|
272
|
-
const snap = collectPerfSnapshot();
|
|
273
|
-
const entry = snap.histogram.find((h) => h.name === "widget");
|
|
274
|
-
expect(entry).toBeDefined();
|
|
275
|
-
expect(entry!.count).toBe(4);
|
|
276
|
-
expect(entry!.mean).toBeCloseTo(25, 5);
|
|
277
|
-
expect(entry!.min).toBe(10);
|
|
278
|
-
expect(entry!.max).toBe(40);
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it("computes percentiles via nearest-rank", () => {
|
|
282
|
-
// Fixed dataset of 100 observations for a stable p95/p99.
|
|
283
|
-
for (let i = 1; i <= 100; i++) {
|
|
284
|
-
recordFrameworkMeasurement("stable", i);
|
|
285
|
-
}
|
|
286
|
-
const snap = collectPerfSnapshot();
|
|
287
|
-
const entry = snap.histogram.find((h) => h.name === "stable");
|
|
288
|
-
expect(entry).toBeDefined();
|
|
289
|
-
expect(entry!.count).toBe(100);
|
|
290
|
-
// Nearest-rank: p50 = value at rank ceil(0.5 * 100) = 50 → 50
|
|
291
|
-
expect(entry!.p50).toBe(50);
|
|
292
|
-
expect(entry!.p95).toBe(95);
|
|
293
|
-
expect(entry!.p99).toBe(99);
|
|
294
|
-
});
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
// ─── 7. LRU ring-buffer eviction ───────────────────────────────────────
|
|
298
|
-
|
|
299
|
-
describe("ring-buffer eviction", () => {
|
|
300
|
-
beforeEach(() => enablePerf());
|
|
301
|
-
|
|
302
|
-
it("drops oldest entries once the buffer is full", () => {
|
|
303
|
-
// Push 5 marks over the limit; the first 5 should be evicted.
|
|
304
|
-
const total = PERF_BUFFER_LIMIT + 5;
|
|
305
|
-
for (let i = 0; i < total; i++) {
|
|
306
|
-
recordFrameworkMeasurement(`mark-${i}`, i % 50);
|
|
307
|
-
}
|
|
308
|
-
const snap = collectPerfSnapshot();
|
|
309
|
-
expect(snap.totalCount).toBe(total);
|
|
310
|
-
expect(snap.bufferedCount).toBe(PERF_BUFFER_LIMIT);
|
|
311
|
-
// Recent[0] should be the 5th-most-recent entry (since we only ask for 50).
|
|
312
|
-
expect(snap.recent.length).toBeLessThanOrEqual(50);
|
|
313
|
-
// Newest last: the final entry should be mark-{total-1}.
|
|
314
|
-
const newest = snap.recent[snap.recent.length - 1];
|
|
315
|
-
expect(newest?.name).toBe(`mark-${total - 1}`);
|
|
316
|
-
});
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
// ─── 8. Dashboard snapshot shape ───────────────────────────────────────
|
|
320
|
-
|
|
321
|
-
describe("collectPerfSnapshot() dashboard payload", () => {
|
|
322
|
-
beforeEach(() => enablePerf());
|
|
323
|
-
|
|
324
|
-
it("returns the documented shape with histogram sorted by count", () => {
|
|
325
|
-
// Three marks named A, two named B, one named C → sort order A, B, C.
|
|
326
|
-
for (let i = 0; i < 3; i++) recordFrameworkMeasurement("A", 1);
|
|
327
|
-
for (let i = 0; i < 2; i++) recordFrameworkMeasurement("B", 2);
|
|
328
|
-
recordFrameworkMeasurement("C", 3);
|
|
329
|
-
|
|
330
|
-
const snap = collectPerfSnapshot();
|
|
331
|
-
expect(snap.enabled).toBe(true);
|
|
332
|
-
expect(snap.totalCount).toBe(6);
|
|
333
|
-
expect(snap.bufferLimit).toBe(PERF_BUFFER_LIMIT);
|
|
334
|
-
expect(snap.histogram.map((h) => h.name)).toEqual(["A", "B", "C"]);
|
|
335
|
-
});
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
// ─── 9. recordFrameworkMeasurement gate ────────────────────────────────
|
|
339
|
-
|
|
340
|
-
describe("recordFrameworkMeasurement() gate", () => {
|
|
341
|
-
it("is a no-op when perf is disabled", () => {
|
|
342
|
-
disablePerf();
|
|
343
|
-
recordFrameworkMeasurement("ignored", 999);
|
|
344
|
-
expect(_peekLastMarkForTesting()).toBeUndefined();
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
it("silently rejects non-finite or negative durations", () => {
|
|
348
|
-
enablePerf();
|
|
349
|
-
recordFrameworkMeasurement("bad", Number.NaN);
|
|
350
|
-
recordFrameworkMeasurement("bad", -5);
|
|
351
|
-
expect(_peekLastMarkForTesting()).toBeUndefined();
|
|
352
|
-
});
|
|
353
|
-
});
|
|
354
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Phase 18.ψ — user-marks tests
|
|
3
|
+
*
|
|
4
|
+
* Covers the three public entry points (`time`, `timeAsync`, `createPerf`)
|
|
5
|
+
* plus the `/_mandu/heap` dashboard snapshot and OTel integration. Fixture
|
|
6
|
+
* style mirrors `tests/perf/perf.test.ts`: mutate `process.env.MANDU_PERF`,
|
|
7
|
+
* call the exported `_resetCacheForTesting` + `_resetUserMarksForTesting`
|
|
8
|
+
* helpers, restore between tests.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
describe,
|
|
13
|
+
it,
|
|
14
|
+
expect,
|
|
15
|
+
beforeEach,
|
|
16
|
+
afterEach,
|
|
17
|
+
} from "bun:test";
|
|
18
|
+
import {
|
|
19
|
+
time,
|
|
20
|
+
timeAsync,
|
|
21
|
+
createPerf,
|
|
22
|
+
collectPerfSnapshot,
|
|
23
|
+
recordFrameworkMeasurement,
|
|
24
|
+
_resetUserMarksForTesting,
|
|
25
|
+
_peekLastMarkForTesting,
|
|
26
|
+
PERF_BUFFER_LIMIT,
|
|
27
|
+
} from "../user-marks";
|
|
28
|
+
import {
|
|
29
|
+
isPerfEnabled,
|
|
30
|
+
_resetCacheForTesting,
|
|
31
|
+
} from "../index";
|
|
32
|
+
import {
|
|
33
|
+
Tracer,
|
|
34
|
+
setTracer,
|
|
35
|
+
resetTracer,
|
|
36
|
+
type Span,
|
|
37
|
+
type SpanExporter,
|
|
38
|
+
} from "../../observability/tracing";
|
|
39
|
+
|
|
40
|
+
function enablePerf(): void {
|
|
41
|
+
process.env.MANDU_PERF = "1";
|
|
42
|
+
_resetCacheForTesting();
|
|
43
|
+
_resetUserMarksForTesting();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function disablePerf(): void {
|
|
47
|
+
delete process.env.MANDU_PERF;
|
|
48
|
+
_resetCacheForTesting();
|
|
49
|
+
_resetUserMarksForTesting();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("@mandujs/core/perf — user marks (time / timeAsync / createPerf)", () => {
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
disablePerf();
|
|
55
|
+
resetTracer();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ─── 1. Basic lifecycle ────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
describe("time(): mark lifecycle", () => {
|
|
61
|
+
beforeEach(() => enablePerf());
|
|
62
|
+
|
|
63
|
+
it("returns an end() function that records a non-negative duration", async () => {
|
|
64
|
+
const end = time("db-query");
|
|
65
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
66
|
+
const elapsedMs = end();
|
|
67
|
+
|
|
68
|
+
expect(typeof elapsedMs).toBe("number");
|
|
69
|
+
expect(elapsedMs).toBeGreaterThanOrEqual(0);
|
|
70
|
+
expect(Number.isFinite(elapsedMs)).toBe(true);
|
|
71
|
+
|
|
72
|
+
const last = _peekLastMarkForTesting();
|
|
73
|
+
expect(last?.name).toBe("db-query");
|
|
74
|
+
expect(last?.category).toBe("user");
|
|
75
|
+
expect(last?.durationMs).toBe(elapsedMs);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("end() is idempotent — second call returns 0 without double-recording", () => {
|
|
79
|
+
const end = time("idem");
|
|
80
|
+
const first = end();
|
|
81
|
+
const second = end();
|
|
82
|
+
|
|
83
|
+
expect(first).toBeGreaterThanOrEqual(0);
|
|
84
|
+
expect(second).toBe(0);
|
|
85
|
+
|
|
86
|
+
// Only one entry recorded.
|
|
87
|
+
const snap = collectPerfSnapshot();
|
|
88
|
+
const entry = snap.histogram.find((h) => h.name === "idem");
|
|
89
|
+
expect(entry?.count).toBe(1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("custom category is honored in the histogram entry", () => {
|
|
93
|
+
const end = time("ssr-slot", { category: "ssr" });
|
|
94
|
+
end();
|
|
95
|
+
const last = _peekLastMarkForTesting();
|
|
96
|
+
expect(last?.category).toBe("ssr");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ─── 2. Async scope ────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
describe("timeAsync(): async scope", () => {
|
|
103
|
+
beforeEach(() => enablePerf());
|
|
104
|
+
|
|
105
|
+
it("forwards the resolved value", async () => {
|
|
106
|
+
const value = await timeAsync("fetch-user", async () => {
|
|
107
|
+
await new Promise((resolve) => setTimeout(resolve, 2));
|
|
108
|
+
return { id: 123, name: "alice" };
|
|
109
|
+
});
|
|
110
|
+
expect(value).toEqual({ id: 123, name: "alice" });
|
|
111
|
+
const last = _peekLastMarkForTesting();
|
|
112
|
+
expect(last?.name).toBe("fetch-user");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("forwards sync return values", async () => {
|
|
116
|
+
const value = await timeAsync("sync-op", () => 42);
|
|
117
|
+
expect(value).toBe(42);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("rethrows errors and still records the mark", async () => {
|
|
121
|
+
const boom = new Error("kaboom");
|
|
122
|
+
await expect(
|
|
123
|
+
timeAsync("failing", async () => {
|
|
124
|
+
throw boom;
|
|
125
|
+
}),
|
|
126
|
+
).rejects.toThrow("kaboom");
|
|
127
|
+
|
|
128
|
+
// Mark recorded even on error path.
|
|
129
|
+
const last = _peekLastMarkForTesting();
|
|
130
|
+
expect(last?.name).toBe("failing");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ─── 3. Zero overhead when disabled ────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
describe("disabled (MANDU_PERF unset)", () => {
|
|
137
|
+
beforeEach(() => disablePerf());
|
|
138
|
+
|
|
139
|
+
it("time() returns a shared no-op end function and records nothing", () => {
|
|
140
|
+
expect(isPerfEnabled()).toBe(false);
|
|
141
|
+
const end = time("ignored");
|
|
142
|
+
const result = end();
|
|
143
|
+
expect(result).toBe(0);
|
|
144
|
+
expect(_peekLastMarkForTesting()).toBeUndefined();
|
|
145
|
+
|
|
146
|
+
const snap = collectPerfSnapshot();
|
|
147
|
+
expect(snap.enabled).toBe(false);
|
|
148
|
+
expect(snap.totalCount).toBe(0);
|
|
149
|
+
expect(snap.histogram).toEqual([]);
|
|
150
|
+
expect(snap.recent).toEqual([]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("timeAsync() invokes fn directly and returns its value", async () => {
|
|
154
|
+
const result = await timeAsync("ignored-async", async () => "ok");
|
|
155
|
+
expect(result).toBe("ok");
|
|
156
|
+
expect(_peekLastMarkForTesting()).toBeUndefined();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("createPerf() with enabled=false returns a no-op instance", () => {
|
|
160
|
+
const perf = createPerf({ enabled: false });
|
|
161
|
+
expect(perf.enabled).toBe(false);
|
|
162
|
+
const end = perf.time("nope");
|
|
163
|
+
expect(end()).toBe(0);
|
|
164
|
+
expect(_peekLastMarkForTesting()).toBeUndefined();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ─── 4. createPerf scoped instance ─────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
describe("createPerf()", () => {
|
|
171
|
+
it("defaults to the global gate when enabled is unset", () => {
|
|
172
|
+
enablePerf();
|
|
173
|
+
const perf = createPerf();
|
|
174
|
+
expect(perf.enabled).toBe(true);
|
|
175
|
+
perf.time("scoped")();
|
|
176
|
+
expect(_peekLastMarkForTesting()?.name).toBe("scoped");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("can force-enable when the global gate is off", async () => {
|
|
180
|
+
disablePerf();
|
|
181
|
+
const perf = createPerf({ enabled: false });
|
|
182
|
+
// Even a force-enable=false under a disabled global is no-op.
|
|
183
|
+
expect(perf.enabled).toBe(false);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ─── 5. OTel tracer integration ────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
describe("OTel integration", () => {
|
|
190
|
+
it("creates a child span when tracing is enabled, with mandu.perf.category attribute", () => {
|
|
191
|
+
enablePerf();
|
|
192
|
+
const captured: Span[] = [];
|
|
193
|
+
const capturing: SpanExporter = {
|
|
194
|
+
export(spans: Span[]): void {
|
|
195
|
+
captured.push(...spans);
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
setTracer(new Tracer({ enabled: true, customExporter: capturing }));
|
|
199
|
+
|
|
200
|
+
const end = time("db-query", { category: "user" });
|
|
201
|
+
end();
|
|
202
|
+
|
|
203
|
+
expect(captured.length).toBe(1);
|
|
204
|
+
expect(captured[0]!.name).toBe("db-query");
|
|
205
|
+
expect(captured[0]!.attributes["mandu.perf.category"]).toBe("user");
|
|
206
|
+
expect(
|
|
207
|
+
typeof captured[0]!.attributes["mandu.perf.duration_ms"],
|
|
208
|
+
).toBe("number");
|
|
209
|
+
expect(captured[0]!.status).toBe("ok");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("opt-out via { trace: false } skips span creation", () => {
|
|
213
|
+
enablePerf();
|
|
214
|
+
const captured: Span[] = [];
|
|
215
|
+
setTracer(
|
|
216
|
+
new Tracer({
|
|
217
|
+
enabled: true,
|
|
218
|
+
customExporter: {
|
|
219
|
+
export: (spans) => {
|
|
220
|
+
captured.push(...spans);
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const end = time("silent", { trace: false });
|
|
227
|
+
end();
|
|
228
|
+
|
|
229
|
+
expect(captured.length).toBe(0);
|
|
230
|
+
// Still recorded in histogram.
|
|
231
|
+
expect(_peekLastMarkForTesting()?.name).toBe("silent");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("timeAsync marks span status=error on thrown error", async () => {
|
|
235
|
+
enablePerf();
|
|
236
|
+
const captured: Span[] = [];
|
|
237
|
+
setTracer(
|
|
238
|
+
new Tracer({
|
|
239
|
+
enabled: true,
|
|
240
|
+
customExporter: {
|
|
241
|
+
export: (spans) => {
|
|
242
|
+
captured.push(...spans);
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
}),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
await expect(
|
|
249
|
+
timeAsync("boom", async () => {
|
|
250
|
+
throw new Error("nope");
|
|
251
|
+
}),
|
|
252
|
+
).rejects.toThrow("nope");
|
|
253
|
+
|
|
254
|
+
expect(captured.length).toBe(1);
|
|
255
|
+
expect(captured[0]!.status).toBe("error");
|
|
256
|
+
expect(captured[0]!.errorMessage).toBe("nope");
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// ─── 6. Histogram math ─────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
describe("histogram", () => {
|
|
263
|
+
beforeEach(() => enablePerf());
|
|
264
|
+
|
|
265
|
+
it("computes count / mean / min / max correctly over multiple marks", () => {
|
|
266
|
+
// Synthesize durations via the framework bridge so we get deterministic values.
|
|
267
|
+
recordFrameworkMeasurement("widget", 10);
|
|
268
|
+
recordFrameworkMeasurement("widget", 20);
|
|
269
|
+
recordFrameworkMeasurement("widget", 30);
|
|
270
|
+
recordFrameworkMeasurement("widget", 40);
|
|
271
|
+
|
|
272
|
+
const snap = collectPerfSnapshot();
|
|
273
|
+
const entry = snap.histogram.find((h) => h.name === "widget");
|
|
274
|
+
expect(entry).toBeDefined();
|
|
275
|
+
expect(entry!.count).toBe(4);
|
|
276
|
+
expect(entry!.mean).toBeCloseTo(25, 5);
|
|
277
|
+
expect(entry!.min).toBe(10);
|
|
278
|
+
expect(entry!.max).toBe(40);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("computes percentiles via nearest-rank", () => {
|
|
282
|
+
// Fixed dataset of 100 observations for a stable p95/p99.
|
|
283
|
+
for (let i = 1; i <= 100; i++) {
|
|
284
|
+
recordFrameworkMeasurement("stable", i);
|
|
285
|
+
}
|
|
286
|
+
const snap = collectPerfSnapshot();
|
|
287
|
+
const entry = snap.histogram.find((h) => h.name === "stable");
|
|
288
|
+
expect(entry).toBeDefined();
|
|
289
|
+
expect(entry!.count).toBe(100);
|
|
290
|
+
// Nearest-rank: p50 = value at rank ceil(0.5 * 100) = 50 → 50
|
|
291
|
+
expect(entry!.p50).toBe(50);
|
|
292
|
+
expect(entry!.p95).toBe(95);
|
|
293
|
+
expect(entry!.p99).toBe(99);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// ─── 7. LRU ring-buffer eviction ───────────────────────────────────────
|
|
298
|
+
|
|
299
|
+
describe("ring-buffer eviction", () => {
|
|
300
|
+
beforeEach(() => enablePerf());
|
|
301
|
+
|
|
302
|
+
it("drops oldest entries once the buffer is full", () => {
|
|
303
|
+
// Push 5 marks over the limit; the first 5 should be evicted.
|
|
304
|
+
const total = PERF_BUFFER_LIMIT + 5;
|
|
305
|
+
for (let i = 0; i < total; i++) {
|
|
306
|
+
recordFrameworkMeasurement(`mark-${i}`, i % 50);
|
|
307
|
+
}
|
|
308
|
+
const snap = collectPerfSnapshot();
|
|
309
|
+
expect(snap.totalCount).toBe(total);
|
|
310
|
+
expect(snap.bufferedCount).toBe(PERF_BUFFER_LIMIT);
|
|
311
|
+
// Recent[0] should be the 5th-most-recent entry (since we only ask for 50).
|
|
312
|
+
expect(snap.recent.length).toBeLessThanOrEqual(50);
|
|
313
|
+
// Newest last: the final entry should be mark-{total-1}.
|
|
314
|
+
const newest = snap.recent[snap.recent.length - 1];
|
|
315
|
+
expect(newest?.name).toBe(`mark-${total - 1}`);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// ─── 8. Dashboard snapshot shape ───────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
describe("collectPerfSnapshot() dashboard payload", () => {
|
|
322
|
+
beforeEach(() => enablePerf());
|
|
323
|
+
|
|
324
|
+
it("returns the documented shape with histogram sorted by count", () => {
|
|
325
|
+
// Three marks named A, two named B, one named C → sort order A, B, C.
|
|
326
|
+
for (let i = 0; i < 3; i++) recordFrameworkMeasurement("A", 1);
|
|
327
|
+
for (let i = 0; i < 2; i++) recordFrameworkMeasurement("B", 2);
|
|
328
|
+
recordFrameworkMeasurement("C", 3);
|
|
329
|
+
|
|
330
|
+
const snap = collectPerfSnapshot();
|
|
331
|
+
expect(snap.enabled).toBe(true);
|
|
332
|
+
expect(snap.totalCount).toBe(6);
|
|
333
|
+
expect(snap.bufferLimit).toBe(PERF_BUFFER_LIMIT);
|
|
334
|
+
expect(snap.histogram.map((h) => h.name)).toEqual(["A", "B", "C"]);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// ─── 9. recordFrameworkMeasurement gate ────────────────────────────────
|
|
339
|
+
|
|
340
|
+
describe("recordFrameworkMeasurement() gate", () => {
|
|
341
|
+
it("is a no-op when perf is disabled", () => {
|
|
342
|
+
disablePerf();
|
|
343
|
+
recordFrameworkMeasurement("ignored", 999);
|
|
344
|
+
expect(_peekLastMarkForTesting()).toBeUndefined();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("silently rejects non-finite or negative durations", () => {
|
|
348
|
+
enablePerf();
|
|
349
|
+
recordFrameworkMeasurement("bad", Number.NaN);
|
|
350
|
+
recordFrameworkMeasurement("bad", -5);
|
|
351
|
+
expect(_peekLastMarkForTesting()).toBeUndefined();
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
});
|