velocious 1.0.603 → 1.0.604

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.
Files changed (72) hide show
  1. package/README.md +23 -1
  2. package/build/configuration.js +19 -0
  3. package/build/database/drivers/base.js +105 -7
  4. package/build/database/drivers/mysql/index.js +21 -6
  5. package/build/database/pool/async-tracked-multi-connection.js +136 -28
  6. package/build/database/pool/base.js +1 -1
  7. package/build/environment-handlers/base.js +16 -0
  8. package/build/environment-handlers/node/cli/commands/test.js +220 -84
  9. package/build/environment-handlers/node.js +40 -1
  10. package/build/src/configuration.d.ts +9 -0
  11. package/build/src/configuration.d.ts.map +1 -1
  12. package/build/src/configuration.js +17 -1
  13. package/build/src/database/drivers/base.d.ts +40 -1
  14. package/build/src/database/drivers/base.d.ts.map +1 -1
  15. package/build/src/database/drivers/base.js +97 -8
  16. package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
  17. package/build/src/database/drivers/mysql/index.js +25 -9
  18. package/build/src/database/pool/async-tracked-multi-connection.d.ts +42 -2
  19. package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
  20. package/build/src/database/pool/async-tracked-multi-connection.js +119 -30
  21. package/build/src/database/pool/base.d.ts +11 -1
  22. package/build/src/database/pool/base.d.ts.map +1 -1
  23. package/build/src/database/pool/base.js +2 -2
  24. package/build/src/environment-handlers/base.d.ts +14 -0
  25. package/build/src/environment-handlers/base.d.ts.map +1 -1
  26. package/build/src/environment-handlers/base.js +15 -1
  27. package/build/src/environment-handlers/node/cli/commands/test.d.ts +22 -0
  28. package/build/src/environment-handlers/node/cli/commands/test.d.ts.map +1 -1
  29. package/build/src/environment-handlers/node/cli/commands/test.js +200 -82
  30. package/build/src/environment-handlers/node.d.ts +14 -0
  31. package/build/src/environment-handlers/node.d.ts.map +1 -1
  32. package/build/src/environment-handlers/node.js +38 -2
  33. package/build/src/testing/test-filter-parser.d.ts +15 -0
  34. package/build/src/testing/test-filter-parser.d.ts.map +1 -1
  35. package/build/src/testing/test-filter-parser.js +57 -6
  36. package/build/src/testing/test-profile-activity.d.ts +7 -0
  37. package/build/src/testing/test-profile-activity.d.ts.map +1 -0
  38. package/build/src/testing/test-profile-activity.js +13 -0
  39. package/build/src/testing/test-profile-context.d.ts +15 -0
  40. package/build/src/testing/test-profile-context.d.ts.map +1 -0
  41. package/build/src/testing/test-profile-context.js +25 -0
  42. package/build/src/testing/test-profile-output.d.ts +30 -0
  43. package/build/src/testing/test-profile-output.d.ts.map +1 -0
  44. package/build/src/testing/test-profile-output.js +204 -0
  45. package/build/src/testing/test-profiler.d.ts +668 -0
  46. package/build/src/testing/test-profiler.d.ts.map +1 -0
  47. package/build/src/testing/test-profiler.js +847 -0
  48. package/build/src/testing/test-runner.d.ts +85 -2
  49. package/build/src/testing/test-runner.d.ts.map +1 -1
  50. package/build/src/testing/test-runner.js +182 -33
  51. package/build/testing/test-filter-parser.js +57 -5
  52. package/build/testing/test-profile-activity.js +14 -0
  53. package/build/testing/test-profile-context.js +28 -0
  54. package/build/testing/test-profile-output.js +240 -0
  55. package/build/testing/test-profiler.js +917 -0
  56. package/build/testing/test-runner.js +196 -33
  57. package/build/tsconfig.tsbuildinfo +1 -1
  58. package/package.json +1 -1
  59. package/src/configuration.js +19 -0
  60. package/src/database/drivers/base.js +105 -7
  61. package/src/database/drivers/mysql/index.js +21 -6
  62. package/src/database/pool/async-tracked-multi-connection.js +136 -28
  63. package/src/database/pool/base.js +1 -1
  64. package/src/environment-handlers/base.js +16 -0
  65. package/src/environment-handlers/node/cli/commands/test.js +220 -84
  66. package/src/environment-handlers/node.js +40 -1
  67. package/src/testing/test-filter-parser.js +57 -5
  68. package/src/testing/test-profile-activity.js +14 -0
  69. package/src/testing/test-profile-context.js +28 -0
  70. package/src/testing/test-profile-output.js +240 -0
  71. package/src/testing/test-profiler.js +917 -0
  72. package/src/testing/test-runner.js +196 -33
package/README.md CHANGED
@@ -26,6 +26,7 @@
26
26
  * Gap-less positional lists with automatic reordering via `actsAsList`, including models with numeric, string, or UUID primary keys (see [docs/acts-as-list.md](docs/acts-as-list.md))
27
27
  * Rails-style nested-attribute writes on frontend-model `save()` (see [docs/nested-attributes.md](docs/nested-attributes.md))
28
28
  * Async-aware test-data factories with inherited traits, graph-first native association autosave, metadata-aware override precedence, callbacks, sequences, linting, and a process-global reload-retention budget that bounds cache-busted re-import memory (see [docs/factories.md](docs/factories.md))
29
+ * Opt-in Benchmark-style test profiling with privacy-safe rich JSON and directly reusable duration-aware shard manifests (see [docs/test-profiling.md](docs/test-profiling.md))
29
30
  * Per-row association counts via `.withCount(...)`, including cohort-safe intersected filters, safe batching of structurally identical aggregates, and automatic IN-list chunking for large parent sets, on frontend and backend queries (see [docs/with-count.md](docs/with-count.md))
30
31
  * Consumer-defined per-row SQL aggregates/computations via `.queryData(...)`, with compatible projections sharing a roundtrip while preserving declared alias-overwrite order and automatic IN-list chunking for large parent sets, on frontend and backend queries (see [docs/query-data.md](docs/query-data.md))
31
32
  * Per-record ability checks via `.abilities(...)` on frontend queries + `record.can(action)` (see [docs/abilities.md](docs/abilities.md))
@@ -227,6 +228,22 @@ Slowest 10 tests:
227
228
 
228
229
  The report is skipped for single-test runs. See [docs/testing-guidelines.md](docs/testing-guidelines.md).
229
230
 
231
+ Add `--profile` for a compact Benchmark-style phase and pool summary. Use
232
+ `--profile-json <path>` for versioned, privacy-safe detail or
233
+ `--timing-manifest-output <path>` to generate a sorted per-file duration map for
234
+ the existing `--timing-manifest` shard input; either output flag implies
235
+ profiling.
236
+
237
+ ```bash
238
+ npx velocious test --profile-json tmp/test-profile.json \
239
+ --timing-manifest-output tmp/test-timings.json
240
+ npx velocious test --groups=4 --group-number=1 \
241
+ --timing-manifest tmp/test-timings.json
242
+ ```
243
+
244
+ See [test profiling](docs/test-profiling.md) for lifecycle accounting, custom
245
+ activity spans, schema, and privacy guarantees.
246
+
230
247
  Prefer waiting for a real signal or condition over sleeping a fixed duration. `waitForEvent(emitter, eventName, {timeoutMs, filter})` resolves the instant a matching event fires (a background job finishing, a model update, a websocket message) and rejects on timeout; for polling an arbitrary condition, use awaitery's `waitFor`.
231
248
 
232
249
  ```js
@@ -1769,7 +1786,12 @@ database: {
1769
1786
 
1770
1787
  `pool.max` caps live async-tracked connections for that pool and defaults to `10` when omitted. When the cap is reached, new checkouts wait until a matching checked-in connection can be handed over or capacity is freed. Set `pool.max` to `null` only when a process is deliberately allowed to open an unbounded number of database connections. The built-in debug endpoint reports each in-use connection's `checkedOutForMs`, each idle connection's `idleForMs`, and queued `pendingCheckouts[].waitingForMs` so production diagnostics can distinguish long-held checkouts from pool-capacity waits.
1771
1788
 
1772
- Debug snapshots also expose cumulative checkout-wait and idle-reaper disposal telemetry. The [MySQL idle-reaping benchmark and methodology](docs/mysql-pool-idle-reaping-research.md) compare the 5-second default with 60 seconds and disabled reaping under a fixed cap; absent representative measured evidence, retain the 5-second default.
1789
+ Debug snapshots also expose cumulative connection-creation, checkout-wait and
1790
+ timeout, idle-reap, and peak-live-connection telemetry. Opt-in test profiles can
1791
+ attribute safe aggregate deltas to their current spans. The [MySQL idle-reaping
1792
+ benchmark and methodology](docs/mysql-pool-idle-reaping-research.md) compare the
1793
+ 5-second default with 60 seconds and disabled reaping under a fixed cap; absent
1794
+ representative measured evidence, retain the 5-second default.
1773
1795
 
1774
1796
  # Websockets
1775
1797
 
@@ -35,6 +35,7 @@ import {frontendModelApiManifest, frontendModelResourceClassFromDefinition, fron
35
35
  import {currentOfflineGrantSigningKey, normalizeOfflineGrantSigningKey} from "./sync/offline-grant.js"
36
36
  import PluginRoutes from "./routes/plugin-routes.js"
37
37
  import restArgsError from "./utils/rest-args-error.js"
38
+ import { validateTestActivityName } from "./testing/test-profile-activity.js"
38
39
  import {validateTimeZone} from "./time-zone.js"
39
40
  import {withTrackedStack} from "./utils/with-tracked-stack.js"
40
41
  import VelociousPackage from "./packages/velocious-package.js"
@@ -3150,6 +3151,24 @@ export default class VelociousConfiguration {
3150
3151
  return await this.getEnvironmentHandler().runWithRequestTiming(requestTiming, callback)
3151
3152
  }
3152
3153
 
3154
+ /**
3155
+ * Profiles an application-defined test activity when an opt-in test profile
3156
+ * context is active. The callback always runs, including outside profiling.
3157
+ * @template T
3158
+ * @param {string} name - Low-cardinality activity identifier.
3159
+ * @param {() => (T | Promise<T>)} callback - Activity callback.
3160
+ * @returns {Promise<T>} - Callback result.
3161
+ */
3162
+ async profileTestActivity(name, callback) {
3163
+ const validatedName = validateTestActivityName(name)
3164
+
3165
+ const context = this.getEnvironmentHandler().getCurrentTestProfileContext()
3166
+
3167
+ if (!context) return await callback()
3168
+
3169
+ return await context.profiler.profileActivity(context, validatedName, callback)
3170
+ }
3171
+
3153
3172
  /**
3154
3173
  * Runs run with timezone.
3155
3174
  * @param {string} timeZone - IANA timezone identifier.
@@ -91,6 +91,14 @@
91
91
  * @property {boolean} willRetry - Whether another outer transaction attempt will run.
92
92
  */
93
93
 
94
+ /**
95
+ * TestProfileQueryAttempt type.
96
+ * @typedef {object} TestProfileQueryAttempt
97
+ * @property {import("../../testing/test-profiler.js").TestProfileAsyncContext} context - Captured async attribution.
98
+ * @property {{sqlFingerprint: string, sqlOperation: string}} diagnostic - Redacted statement diagnostic.
99
+ * @property {number} startedAtMs - Physical attempt start time.
100
+ */
101
+
94
102
  /**
95
103
  * ActiveQueryDebugSnapshot type.
96
104
  * @typedef {object} ActiveQueryDebugSnapshot
@@ -164,6 +172,7 @@ import TableForeignKey from "../table-data/table-foreign-key.js"
164
172
  import wait from "awaitery/build/wait.js"
165
173
  import { optionalPositiveInteger } from "typanic"
166
174
  import { coordinateSharedTransactionConnection } from "../../testing/shared-transaction-connection-coordinator.js"
175
+ import { currentTestProfileContext } from "../../testing/test-profile-context.js"
167
176
  import sha256Hex from "../../utils/sha256-hex.js"
168
177
 
169
178
  /** Maximum characters inspected when building the debug SQL preview. */
@@ -1740,7 +1749,9 @@ export default class VelociousDatabaseDriversBase {
1740
1749
  return
1741
1750
  }
1742
1751
 
1743
- await this._startTransactionAction(options)
1752
+ await this._runProfiledTransactionAction("start", async () => {
1753
+ await this._startTransactionAction(options)
1754
+ })
1744
1755
  this._transactionsCount++
1745
1756
 
1746
1757
  if (this._transactionsCount === 1) {
@@ -1772,7 +1783,9 @@ export default class VelociousDatabaseDriversBase {
1772
1783
  */
1773
1784
  async commitTransaction(options = {}) {
1774
1785
  await this._transactionsActionsMutex.sync(async () => {
1775
- await this._commitTransactionAction(options)
1786
+ await this._runProfiledTransactionAction("commit", async () => {
1787
+ await this._commitTransactionAction(options)
1788
+ })
1776
1789
  this._transactionsCount--
1777
1790
  this._resolveCompletedTransaction()
1778
1791
  })
@@ -1797,6 +1810,68 @@ export default class VelociousDatabaseDriversBase {
1797
1810
  await this.query("COMMIT", options)
1798
1811
  }
1799
1812
 
1813
+ /**
1814
+ * Times a physical transaction action only when test profiling is active.
1815
+ * @template T
1816
+ * @param {"start" | "commit" | "rollback"} action - Transaction action.
1817
+ * @param {() => Promise<T>} callback - Physical action callback.
1818
+ * @returns {Promise<T>} - Callback result.
1819
+ */
1820
+ async _runProfiledTransactionAction(action, callback) {
1821
+ const profileContext = currentTestProfileContext(this.configuration)
1822
+
1823
+ if (!profileContext) return await callback()
1824
+
1825
+ const startedAtMs = nowMs()
1826
+ let failed = true
1827
+
1828
+ try {
1829
+ const result = await callback()
1830
+
1831
+ failed = false
1832
+ return result
1833
+ } finally {
1834
+ profileContext.profiler.recordDatabaseTransaction(profileContext, {
1835
+ action,
1836
+ durationMs: nowMs() - startedAtMs,
1837
+ failed
1838
+ })
1839
+ }
1840
+ }
1841
+
1842
+ /**
1843
+ * Starts an optional physical-query profile attempt without retaining SQL.
1844
+ * @param {string} sql - Original SQL used only to derive its redacted diagnostic.
1845
+ * @returns {TestProfileQueryAttempt | undefined} - Active profile handle.
1846
+ */
1847
+ _startProfiledQueryAttempt(sql) {
1848
+ const context = currentTestProfileContext(this.configuration)
1849
+
1850
+ if (!context) return undefined
1851
+
1852
+ return {
1853
+ context,
1854
+ diagnostic: sqlDiagnostic(sql),
1855
+ startedAtMs: nowMs()
1856
+ }
1857
+ }
1858
+
1859
+ /**
1860
+ * Completes an optional physical-query profile attempt.
1861
+ * @param {TestProfileQueryAttempt | undefined} attempt - Profile handle.
1862
+ * @param {boolean} failed - Whether the physical driver call failed.
1863
+ * @returns {void}
1864
+ */
1865
+ _finishProfiledQueryAttempt(attempt, failed) {
1866
+ if (!attempt) return
1867
+
1868
+ attempt.context.profiler.recordDatabaseQuery(attempt.context, {
1869
+ durationMs: nowMs() - attempt.startedAtMs,
1870
+ failed,
1871
+ ...attempt.diagnostic
1872
+ })
1873
+ }
1874
+
1800
1875
  /**
1801
1876
  * Runs every guard registered to the transaction frame.
1802
1877
  * @param {TransactionCallbackFrame} callbackFrame - Frame whose guards are completing.
@@ -1916,7 +1991,17 @@ export default class VelociousDatabaseDriversBase {
1916
1991
  await this.beforeQuery(sql, options)
1917
1992
 
1918
1993
  try {
1919
- return await this._affectedRowsActual(sql)
1994
+ const profileAttempt = this._startProfiledQueryAttempt(sql)
1995
+ let failed = true
1996
+
1997
+ try {
1998
+ const affectedRows = await this._affectedRowsActual(sql)
1999
+
2000
+ failed = false
2001
+ return affectedRows
2002
+ } finally {
2003
+ this._finishProfiledQueryAttempt(profileAttempt, failed)
2004
+ }
1920
2005
  } finally {
1921
2006
  await this.afterQuery(sql, options)
1922
2007
  }
@@ -1945,7 +2030,7 @@ export default class VelociousDatabaseDriversBase {
1945
2030
  let result
1946
2031
 
1947
2032
  try {
1948
- const runQueryActualWithHooks = async () => await this._queryActualWithHooks(querySql, options)
2033
+ const runQueryActualWithHooks = async () => await this._queryActualWithHooks(querySql, options, originalSql)
1949
2034
 
1950
2035
  if (requestTiming && tries === 1) {
1951
2036
  result = await requestTiming.measureDbQuery(runQueryActualWithHooks)
@@ -1980,14 +2065,25 @@ export default class VelociousDatabaseDriversBase {
1980
2065
  * Runs query actual with before/after hooks.
1981
2066
  * @param {string} sql - SQL string.
1982
2067
  * @param {QueryOptions} options - Query options.
2068
+ * @param {string} originalSql - SQL before process-list comments.
1983
2069
  * @returns {Promise<QueryResultType>} - Resolves with the query.
1984
2070
  */
1985
- async _queryActualWithHooks(sql, options) {
2071
+ async _queryActualWithHooks(sql, options, originalSql) {
1986
2072
  return await coordinateSharedTransactionConnection(this, async () => {
1987
2073
  await this.beforeQuery(sql, options)
1988
2074
 
1989
2075
  try {
1990
- return await this._queryActual(sql, options)
2076
+ const profileAttempt = this._startProfiledQueryAttempt(originalSql)
2077
+ let failed = true
2078
+
2079
+ try {
2080
+ const result = await this._queryActual(sql, options)
2081
+
2082
+ failed = false
2083
+ return result
2084
+ } finally {
2085
+ this._finishProfiledQueryAttempt(profileAttempt, failed)
2086
+ }
1991
2087
  } finally {
1992
2088
  await this.afterQuery(sql, options)
1993
2089
  }
@@ -2403,7 +2499,9 @@ export default class VelociousDatabaseDriversBase {
2403
2499
  async rollbackTransaction(options = {}) {
2404
2500
  await this._transactionsActionsMutex.sync(async () => {
2405
2501
  try {
2406
- await this._rollbackTransactionAction(options)
2502
+ await this._runProfiledTransactionAction("rollback", async () => {
2503
+ await this._rollbackTransactionAction(options)
2504
+ })
2407
2505
  } finally {
2408
2506
  this._transactionsCount--
2409
2507
  this._resolveCompletedTransaction()
@@ -518,7 +518,15 @@ export default class VelociousDatabaseDriversMysql extends Base{
518
518
  if (!this.pool) await this.connect()
519
519
  if (!this.pool) throw new Error("MySQL pool failed to initialize")
520
520
 
521
- yield* streamQuery(this.pool, sql)
521
+ const profileAttempt = this._startProfiledQueryAttempt(sql)
522
+ let failed = true
523
+
524
+ try {
525
+ yield* streamQuery(this.pool, sql)
526
+ failed = false
527
+ } finally {
528
+ this._finishProfiledQueryAttempt(profileAttempt, failed)
529
+ }
522
530
  }
523
531
 
524
532
  /**
@@ -558,13 +566,20 @@ export default class VelociousDatabaseDriversMysql extends Base{
558
566
  if (!this.pool) throw new Error("MySQL pool failed to initialize")
559
567
 
560
568
  const pool = this.pool
569
+ const profileAttempt = this._startProfiledQueryAttempt(structureSql)
570
+ let failed = true
561
571
 
562
- await new Promise((resolve, reject) => {
563
- pool.query(structureSql, (error) => {
564
- if (error) reject(error)
565
- else resolve(undefined)
572
+ try {
573
+ await new Promise((resolve, reject) => {
574
+ pool.query(structureSql, (error) => {
575
+ if (error) reject(error)
576
+ else resolve(undefined)
577
+ })
566
578
  })
567
- })
579
+ failed = false
580
+ } finally {
581
+ this._finishProfiledQueryAttempt(profileAttempt, failed)
582
+ }
568
583
 
569
584
  return true
570
585
  }
@@ -1,7 +1,8 @@
1
1
  // @ts-check
2
2
 
3
- import {AsyncLocalStorage} from "async_hooks"
4
- import BasePool, {POOL_CONFIGURATION_KEY} from "./base.js"
3
+ import { AsyncLocalStorage } from "async_hooks"
4
+ import BasePool, { POOL_CONFIGURATION_KEY } from "./base.js"
5
+ import { currentTestProfileContext } from "../../testing/test-profile-context.js"
5
6
 
6
7
  /**
7
8
  * PendingCheckout type.
@@ -15,6 +16,7 @@ import BasePool, {POOL_CONFIGURATION_KEY} from "./base.js"
15
16
  * @property {number | null} timeoutAt - Timestamp when the checkout will time out, or null when disabled.
16
17
  * @property {number | null} timeoutMillis - Milliseconds to wait before rejecting, or null when disabled.
17
18
  * @property {ReturnType<typeof setTimeout> | undefined} timeoutTimer - Timer that rejects the pending checkout.
19
+ * @property {import("../../testing/test-profiler.js").TestProfileAsyncContext | undefined} [testProfileContext] - Async-safe profile attribution captured at enqueue.
18
20
  */
19
21
  export const CLOSED_CONNECTION = Symbol("velociousClosedConnection")
20
22
  const IDLE_CONNECTION_CHECKED_IN_AT = Symbol("velociousIdleConnectionCheckedInAt")
@@ -115,10 +117,20 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
115
117
 
116
118
  /** Cumulative low-cardinality pool telemetry. */
117
119
  telemetry = {
120
+ connectionCreationCount: 0,
121
+ connectionCreationFailureCount: 0,
122
+ connectionCreationMaxMs: 0,
123
+ connectionCreationTotalMs: 0,
124
+ checkoutTimeoutCount: 0,
118
125
  checkoutWaitCount: 0,
119
126
  checkoutWaitMaxMs: 0,
120
127
  checkoutWaitTotalMs: 0,
121
- idleReapDisposalCount: 0
128
+ idleReapCount: 0,
129
+ idleReapDisposalCount: 0,
130
+ idleReapFailureCount: 0,
131
+ idleReapMaxMs: 0,
132
+ idleReapTotalMs: 0,
133
+ peakLiveConnections: 0
122
134
  }
123
135
 
124
136
  idSeq = 0
@@ -139,6 +151,58 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
139
151
  this._withoutCurrentConnectionContext = withoutCurrentConnectionContext
140
152
  }
141
153
 
154
+ /**
155
+ * Returns the pool telemetry clock.
156
+ * @returns {number} - Current time in milliseconds.
157
+ */
158
+ nowMs() { return Date.now() }
159
+
160
+ /**
161
+ * Records a pool metric in the active async-safe test profile context.
162
+ * @param {import("../../testing/test-profiler.js").TestProfileAsyncContext | undefined} context - Captured profile context.
163
+ * @param {"connectionCreation" | "checkoutWait" | "checkoutTimeout" | "idleReap" | "idleReapDisposal" | "peakLiveConnections"} metric - Metric name.
164
+ * @param {{durationMs?: number, failed?: boolean, value?: number}} [values] - Aggregate values.
165
+ * @returns {void}
166
+ */
167
+ recordTestProfilePoolMetric(context, metric, values = {}) {
168
+ if (!context) return
169
+
170
+ context.profiler.recordPoolMetric(context, this.identifier, metric, values)
171
+ }
172
+
173
+ /**
174
+ * Spawns and times a physical connection without retaining its configuration.
175
+ * @param {import("../../configuration-types.js").DatabaseConfigurationType} config - Resolved database configuration.
176
+ * @returns {Promise<import("../drivers/base.js").default>} - Connected driver.
177
+ */
178
+ async spawnConnectionWithConfiguration(config) {
179
+ const startedAt = this.nowMs()
180
+ const profileContext = currentTestProfileContext(this.configuration)
181
+ let failed = true
182
+
183
+ try {
184
+ const connection = await super.spawnConnectionWithConfiguration(config)
185
+
186
+ failed = false
187
+ const liveConnectionCount = this.liveConnectionCount() - this.connectionsBeingSpawned + 1
188
+
189
+ if (liveConnectionCount > this.telemetry.peakLiveConnections) {
190
+ this.telemetry.peakLiveConnections = liveConnectionCount
191
+ this.recordTestProfilePoolMetric(profileContext, "peakLiveConnections", {value: liveConnectionCount})
192
+ }
193
+
194
+ return connection
195
+ } finally {
196
+ const durationMs = Math.max(0, this.nowMs() - startedAt)
197
+
198
+ this.telemetry.connectionCreationCount++
199
+ if (failed) this.telemetry.connectionCreationFailureCount++
200
+ this.telemetry.connectionCreationTotalMs += durationMs
201
+ this.telemetry.connectionCreationMaxMs = Math.max(this.telemetry.connectionCreationMaxMs, durationMs)
202
+ this.recordTestProfilePoolMetric(profileContext, "connectionCreation", {durationMs, failed})
203
+ }
204
+ }
205
+
142
206
  /**
143
207
  * Runs checkin.
144
208
  * @param {import("../drivers/base.js").default} connection - Database connection instance.
@@ -258,7 +322,11 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
258
322
  // The post-reap configuration is fresh for the current caller, and its reuse key is
259
323
  // derived from this exact captured object so the connection cannot open one tenant while
260
324
  // being stamped for another. The queued path retains the same captured pair.
261
- connection = await this.spawnConnectionForCheckout(databaseConfig, reuseKey)
325
+ connection = await this.spawnConnectionForCheckout(
326
+ databaseConfig,
327
+ reuseKey,
328
+ currentTestProfileContext(this.configuration)
329
+ )
262
330
 
263
331
  return await this.activateConnection(connection, options)
264
332
  }
@@ -291,7 +359,11 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
291
359
  if (connection) return await this.activateConnection(connection, options)
292
360
 
293
361
  if (this.canSpawnConnection(databaseConfig)) {
294
- connection = await this.spawnConnectionForCheckout(databaseConfig, reuseKey)
362
+ connection = await this.spawnConnectionForCheckout(
363
+ databaseConfig,
364
+ reuseKey,
365
+ currentTestProfileContext(this.configuration)
366
+ )
295
367
 
296
368
  return await this.activateConnection(connection, options)
297
369
  }
@@ -436,13 +508,17 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
436
508
  * Runs spawn connection for checkout.
437
509
  * @param {import("../../configuration-types.js").DatabaseConfigurationType} databaseConfig - Resolved database config for the checkout.
438
510
  * @param {string} reuseKey - Database configuration reuse key for the checkout.
511
+ * @param {import("../../testing/test-profiler.js").TestProfileAsyncContext | undefined} profileContext - Profile context captured when checkout began.
439
512
  * @returns {Promise<import("../drivers/base.js").default>} - Spawned connection.
440
513
  */
441
- async spawnConnectionForCheckout(databaseConfig, reuseKey) {
514
+ async spawnConnectionForCheckout(databaseConfig, reuseKey, profileContext) {
442
515
  this.connectionsBeingSpawned++
443
516
 
444
517
  try {
445
- const connection = await this.spawnConnectionWithConfiguration(databaseConfig)
518
+ const environmentHandler = this.configuration.getEnvironmentHandler()
519
+ const connection = await environmentHandler.runWithTestProfileContext(profileContext, async () => {
520
+ return await this.spawnConnectionWithConfiguration(databaseConfig)
521
+ })
446
522
 
447
523
  this.stampConnectionForConfigurationReuseKey(connection, reuseKey)
448
524
 
@@ -473,7 +549,8 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
473
549
  reuseKey,
474
550
  timeoutAt: timeoutMillis === null ? null : enqueuedAt + timeoutMillis,
475
551
  timeoutMillis,
476
- timeoutTimer: undefined
552
+ timeoutTimer: undefined,
553
+ testProfileContext: currentTestProfileContext(this.configuration)
477
554
  }
478
555
 
479
556
  checkout.timeoutTimer = this.startPendingCheckoutTimeout(checkout)
@@ -573,11 +650,12 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
573
650
  * @returns {void}
574
651
  */
575
652
  recordCheckoutWait(checkout) {
576
- const waitedForMs = Math.max(0, Date.now() - checkout.enqueuedAt)
653
+ const waitedForMs = Math.max(0, this.nowMs() - checkout.enqueuedAt)
577
654
 
578
655
  this.telemetry.checkoutWaitCount++
579
656
  this.telemetry.checkoutWaitTotalMs += waitedForMs
580
657
  this.telemetry.checkoutWaitMaxMs = Math.max(this.telemetry.checkoutWaitMaxMs, waitedForMs)
658
+ this.recordTestProfilePoolMetric(checkout.testProfileContext, "checkoutWait", {durationMs: waitedForMs})
581
659
  }
582
660
 
583
661
  /**
@@ -606,6 +684,8 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
606
684
  if (index === -1) return
607
685
 
608
686
  this.removePendingCheckoutAt(index)
687
+ this.telemetry.checkoutTimeoutCount++
688
+ this.recordTestProfilePoolMetric(checkout.testProfileContext, "checkoutTimeout")
609
689
  checkout.reject(this.pendingCheckoutTimeoutError(checkout))
610
690
  }
611
691
 
@@ -739,16 +819,24 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
739
819
  * @returns {Promise<void>} - Resolves when the checkout has been handled.
740
820
  */
741
821
  async spawnAndResolvePendingCheckout(checkout) {
742
- let connection
822
+ const environmentHandler = this.configuration.getEnvironmentHandler()
743
823
 
744
- try {
745
- connection = await this.spawnConnectionForCheckout(checkout.databaseConfig, checkout.reuseKey)
746
- } catch (error) {
747
- checkout.reject(error instanceof Error ? error : new Error("Failed to spawn database connection.", {cause: error}))
748
- return
749
- }
824
+ return await environmentHandler.runWithTestProfileContext(checkout.testProfileContext, async () => {
825
+ let connection
826
+
827
+ try {
828
+ connection = await this.spawnConnectionForCheckout(
829
+ checkout.databaseConfig,
830
+ checkout.reuseKey,
831
+ checkout.testProfileContext
832
+ )
833
+ } catch (error) {
834
+ checkout.reject(error instanceof Error ? error : new Error("Failed to spawn database connection.", {cause: error}))
835
+ return
836
+ }
750
837
 
751
- await this.resolvePendingCheckout(checkout, connection)
838
+ await this.resolvePendingCheckout(checkout, connection)
839
+ })
752
840
  }
753
841
 
754
842
  /**
@@ -758,11 +846,15 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
758
846
  * @returns {Promise<void>} - Resolves when the checkout has been handled.
759
847
  */
760
848
  async resolvePendingCheckout(checkout, connection) {
761
- try {
762
- checkout.resolve(await this.activateConnection(connection, checkout.options))
763
- } catch (error) {
764
- checkout.reject(error instanceof Error ? error : new Error("Failed to activate database connection.", {cause: error}))
765
- }
849
+ const environmentHandler = this.configuration.getEnvironmentHandler()
850
+
851
+ return await environmentHandler.runWithTestProfileContext(checkout.testProfileContext, async () => {
852
+ try {
853
+ checkout.resolve(await this.activateConnection(connection, checkout.options))
854
+ } catch (error) {
855
+ checkout.reject(error instanceof Error ? error : new Error("Failed to activate database connection.", {cause: error}))
856
+ }
857
+ })
766
858
  }
767
859
 
768
860
  /**
@@ -1308,24 +1400,40 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
1308
1400
  const idleTimeoutMillis = this.idleTimeoutMillis()
1309
1401
 
1310
1402
  if (idleTimeoutMillis === null) return
1403
+ const startedAt = this.nowMs()
1404
+ const profileContext = currentTestProfileContext(this.configuration)
1405
+ let failed = true
1406
+
1407
+ try {
1408
+ const {expiredConnections, keptConnections} = this.classifyIdleConnectionsForReaping({idleTimeoutMillis, now: this.nowMs()})
1311
1409
 
1312
- const {expiredConnections, keptConnections} = this.classifyIdleConnectionsForReaping({idleTimeoutMillis, now: Date.now()})
1410
+ this.connections = keptConnections
1411
+ await this.closeExpiredIdleConnections(expiredConnections, profileContext)
1412
+ await this.awaitInflightConnectionCloses()
1413
+ if (this.connections.length > 0) this.scheduleIdleConnectionReaper()
1414
+ failed = false
1415
+ } finally {
1416
+ const durationMs = Math.max(0, this.nowMs() - startedAt)
1313
1417
 
1314
- this.connections = keptConnections
1315
- await this.closeExpiredIdleConnections(expiredConnections)
1316
- await this.awaitInflightConnectionCloses()
1317
- if (this.connections.length > 0) this.scheduleIdleConnectionReaper()
1418
+ this.telemetry.idleReapCount++
1419
+ if (failed) this.telemetry.idleReapFailureCount++
1420
+ this.telemetry.idleReapTotalMs += durationMs
1421
+ this.telemetry.idleReapMaxMs = Math.max(this.telemetry.idleReapMaxMs, durationMs)
1422
+ this.recordTestProfilePoolMetric(profileContext, "idleReap", {durationMs, failed})
1423
+ }
1318
1424
  }
1319
1425
 
1320
1426
  /**
1321
1427
  * Runs close expired idle connections.
1322
1428
  * @param {import("../drivers/base.js").default[]} expiredConnections - Connections to close.
1429
+ * @param {import("../../testing/test-profiler.js").TestProfileAsyncContext | undefined} [profileContext] - Reaper profile context.
1323
1430
  * @returns {Promise<void>} - Resolves when closed.
1324
1431
  */
1325
- async closeExpiredIdleConnections(expiredConnections) {
1432
+ async closeExpiredIdleConnections(expiredConnections, profileContext) {
1326
1433
  for (const connection of expiredConnections) {
1327
1434
  await this.closeConnection(connection)
1328
1435
  this.telemetry.idleReapDisposalCount++
1436
+ this.recordTestProfilePoolMetric(profileContext, "idleReapDisposal")
1329
1437
  }
1330
1438
  }
1331
1439
 
@@ -45,7 +45,7 @@ import sha256Hex from "../../utils/sha256-hex.js"
45
45
  * @property {Array<DatabasePoolPendingCheckoutDebugSnapshot>} [pendingCheckouts] - Waiting checkout snapshots.
46
46
  * @property {number} pendingCheckoutCount - Number of queued checkout requests.
47
47
  * @property {string} poolClass - Pool class name.
48
- * @property {{checkoutWaitCount: number, checkoutWaitMaxMs: number, checkoutWaitTotalMs: number, idleReapDisposalCount: number}} [telemetry] - Cumulative pool lifecycle telemetry.
48
+ * @property {{connectionCreationCount: number, connectionCreationFailureCount: number, connectionCreationMaxMs: number, connectionCreationTotalMs: number, checkoutTimeoutCount: number, checkoutWaitCount: number, checkoutWaitMaxMs: number, checkoutWaitTotalMs: number, idleReapCount: number, idleReapDisposalCount: number, idleReapFailureCount: number, idleReapMaxMs: number, idleReapTotalMs: number, peakLiveConnections: number}} [telemetry] - Cumulative pool lifecycle telemetry.
49
49
  */
50
50
  export const POOL_CONFIGURATION_KEY = Symbol("velociousPoolConfigurationKey")
51
51
 
@@ -54,6 +54,22 @@ export default class VelociousEnvironmentHandlerBase {
54
54
  */
55
55
  runWithSharedTransactionCoordinatorOwner(_connection, _owner, callback) { return callback() }
56
56
 
57
+ /**
58
+ * Runs work with test-profile attribution. Runtimes without async-context
59
+ * storage execute the callback without installing ambient attribution.
60
+ * @template T
61
+ * @param {import("../testing/test-profiler.js").TestProfileAsyncContext | undefined} _context - Captured profile context, or an explicit absence of attribution.
62
+ * @param {() => T} callback - Profiled work.
63
+ * @returns {T} - Callback result.
64
+ */
65
+ runWithTestProfileContext(_context, callback) { return callback() }
66
+
67
+ /**
68
+ * Gets the current test-profile attribution context.
69
+ * @returns {import("../testing/test-profiler.js").TestProfileAsyncContext | undefined} - Active context.
70
+ */
71
+ getCurrentTestProfileContext() { return undefined }
72
+
57
73
  /**
58
74
  * Mutable ambient tenant used by runtimes without async-context storage.
59
75
  * @type {ReturnType<typeof JSON.parse> | undefined}