velocious 1.0.666 → 1.0.668
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 +40 -11
- package/build/background-jobs/adapter-client.js +22 -0
- package/build/background-jobs/adapter.js +15 -0
- package/build/background-jobs/client.js +147 -1
- package/build/background-jobs/job-semantics.js +30 -0
- package/build/background-jobs/job.js +26 -1
- package/build/background-jobs/local-adapter.js +36 -8
- package/build/background-jobs/local-store.js +410 -39
- package/build/background-jobs/main.js +69 -2
- package/build/background-jobs/platform-job.js +26 -1
- package/build/background-jobs/runtime.js +58 -0
- package/build/background-jobs/store.js +374 -6
- package/build/background-jobs/types.js +27 -2
- package/build/background-jobs/web/controller.js +1 -0
- package/build/configuration.js +30 -6
- package/build/database/drivers/mssql/column.js +2 -2
- package/build/database/drivers/mssql/table.js +19 -5
- package/build/database/pool/async-tracked-multi-connection.js +2 -1
- package/build/database/query/alter-table-base.js +8 -1
- package/build/database/record/attachments/store.js +2 -2
- package/build/environment-handlers/node/cli/commands/test/timing-manifest/merge.js +2 -48
- package/build/environment-handlers/node/cli/commands/test.js +17 -69
- package/build/frontend-model-controller.js +22 -2
- package/build/frontend-models/websocket-channel.js +18 -0
- package/build/http-server/client/websocket-session.js +41 -4
- package/build/http-server/websocket-channel.js +14 -0
- package/build/http-server/websocket-event-log-store.js +85 -12
- package/build/http-server/websocket-events-host.js +17 -7
- package/build/src/background-jobs/adapter-client.d.ts +17 -0
- package/build/src/background-jobs/adapter-client.d.ts.map +1 -1
- package/build/src/background-jobs/adapter-client.js +19 -1
- package/build/src/background-jobs/adapter.d.ts +15 -0
- package/build/src/background-jobs/adapter.d.ts.map +1 -1
- package/build/src/background-jobs/adapter.js +14 -1
- package/build/src/background-jobs/client.d.ts +17 -0
- package/build/src/background-jobs/client.d.ts.map +1 -1
- package/build/src/background-jobs/client.js +129 -1
- package/build/src/background-jobs/job-semantics.d.ts +18 -0
- package/build/src/background-jobs/job-semantics.d.ts.map +1 -1
- package/build/src/background-jobs/job-semantics.js +28 -1
- package/build/src/background-jobs/job.d.ts +15 -0
- package/build/src/background-jobs/job.d.ts.map +1 -1
- package/build/src/background-jobs/job.js +21 -2
- package/build/src/background-jobs/local-adapter.d.ts +23 -8
- package/build/src/background-jobs/local-adapter.d.ts.map +1 -1
- package/build/src/background-jobs/local-adapter.js +35 -9
- package/build/src/background-jobs/local-store.d.ts +120 -5
- package/build/src/background-jobs/local-store.d.ts.map +1 -1
- package/build/src/background-jobs/local-store.js +366 -40
- package/build/src/background-jobs/main.d.ts +24 -2
- package/build/src/background-jobs/main.d.ts.map +1 -1
- package/build/src/background-jobs/main.js +68 -3
- package/build/src/background-jobs/platform-job.d.ts +15 -0
- package/build/src/background-jobs/platform-job.d.ts.map +1 -1
- package/build/src/background-jobs/platform-job.js +19 -2
- package/build/src/background-jobs/runtime.d.ts +39 -0
- package/build/src/background-jobs/runtime.d.ts.map +1 -1
- package/build/src/background-jobs/runtime.js +49 -1
- package/build/src/background-jobs/store.d.ts +100 -1
- package/build/src/background-jobs/store.d.ts.map +1 -1
- package/build/src/background-jobs/store.js +334 -8
- package/build/src/background-jobs/types.d.ts +97 -4
- package/build/src/background-jobs/types.d.ts.map +1 -1
- package/build/src/background-jobs/types.js +28 -3
- package/build/src/background-jobs/web/controller.d.ts.map +1 -1
- package/build/src/background-jobs/web/controller.js +2 -1
- package/build/src/configuration.d.ts +18 -1
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +28 -6
- package/build/src/database/drivers/mssql/column.d.ts.map +1 -1
- package/build/src/database/drivers/mssql/column.js +2 -2
- package/build/src/database/drivers/mssql/table.d.ts.map +1 -1
- package/build/src/database/drivers/mssql/table.js +19 -5
- package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
- package/build/src/database/pool/async-tracked-multi-connection.js +3 -2
- package/build/src/database/query/alter-table-base.d.ts.map +1 -1
- package/build/src/database/query/alter-table-base.js +9 -2
- package/build/src/database/record/attachments/store.js +3 -3
- package/build/src/environment-handlers/node/cli/commands/test/timing-manifest/merge.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/test/timing-manifest/merge.js +3 -47
- package/build/src/environment-handlers/node/cli/commands/test.d.ts +2 -28
- package/build/src/environment-handlers/node/cli/commands/test.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/test.js +10 -61
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +18 -3
- package/build/src/frontend-models/websocket-channel.d.ts +9 -0
- package/build/src/frontend-models/websocket-channel.d.ts.map +1 -1
- package/build/src/frontend-models/websocket-channel.js +16 -1
- package/build/src/http-server/client/websocket-session.d.ts +25 -2
- package/build/src/http-server/client/websocket-session.d.ts.map +1 -1
- package/build/src/http-server/client/websocket-session.js +40 -5
- package/build/src/http-server/websocket-channel.d.ts +11 -0
- package/build/src/http-server/websocket-channel.d.ts.map +1 -1
- package/build/src/http-server/websocket-channel.js +14 -1
- package/build/src/http-server/websocket-event-log-store.d.ts +60 -41
- package/build/src/http-server/websocket-event-log-store.d.ts.map +1 -1
- package/build/src/http-server/websocket-event-log-store.js +79 -12
- package/build/src/http-server/websocket-events-host.d.ts +10 -3
- package/build/src/http-server/websocket-events-host.d.ts.map +1 -1
- package/build/src/http-server/websocket-events-host.js +17 -8
- package/build/src/testing/test-files-finder.d.ts +17 -117
- package/build/src/testing/test-files-finder.d.ts.map +1 -1
- package/build/src/testing/test-files-finder.js +29 -294
- package/build/src/testing/test-filter-parser.d.ts +32 -22
- package/build/src/testing/test-filter-parser.d.ts.map +1 -1
- package/build/src/testing/test-filter-parser.js +25 -217
- package/build/src/testing/test-profile-activity.d.ts +1 -6
- package/build/src/testing/test-profile-activity.d.ts.map +1 -1
- package/build/src/testing/test-profile-activity.js +2 -12
- package/build/src/testing/test-profile-output.d.ts +1 -40
- package/build/src/testing/test-profile-output.d.ts.map +1 -1
- package/build/src/testing/test-profile-output.js +2 -217
- package/build/src/testing/test-profiler.d.ts +15 -659
- package/build/src/testing/test-profiler.d.ts.map +1 -1
- package/build/src/testing/test-profiler.js +35 -833
- package/build/src/testing/test-runner.d.ts +10 -1
- package/build/src/testing/test-runner.d.ts.map +1 -1
- package/build/src/testing/test-runner.js +26 -4
- package/build/src/testing/test-suite-splitter.d.ts +1 -128
- package/build/src/testing/test-suite-splitter.d.ts.map +1 -1
- package/build/src/testing/test-suite-splitter.js +2 -259
- package/build/src/testing/timing-manifest.d.ts +5 -93
- package/build/src/testing/timing-manifest.d.ts.map +1 -1
- package/build/src/testing/timing-manifest.js +4 -283
- package/build/src/testing/velocious-attempt-executor.d.ts.map +1 -1
- package/build/src/testing/velocious-attempt-executor.js +8 -2
- package/build/testing/test-files-finder.js +32 -343
- package/build/testing/test-filter-parser.js +29 -248
- package/build/testing/test-profile-activity.js +1 -12
- package/build/testing/test-profile-output.js +8 -253
- package/build/testing/test-profiler.js +38 -895
- package/build/testing/test-runner.js +33 -9
- package/build/testing/test-suite-splitter.js +1 -301
- package/build/testing/timing-manifest.js +10 -344
- package/build/testing/velocious-attempt-executor.js +7 -1
- package/package.json +3 -3
- package/scripts/test-browser.js +30 -6
- package/scripts/verify-docker-dev-environment.js +13 -1
- package/src/background-jobs/adapter-client.js +22 -0
- package/src/background-jobs/adapter.js +15 -0
- package/src/background-jobs/client.js +147 -1
- package/src/background-jobs/job-semantics.js +30 -0
- package/src/background-jobs/job.js +26 -1
- package/src/background-jobs/local-adapter.js +36 -8
- package/src/background-jobs/local-store.js +410 -39
- package/src/background-jobs/main.js +69 -2
- package/src/background-jobs/platform-job.js +26 -1
- package/src/background-jobs/runtime.js +58 -0
- package/src/background-jobs/store.js +374 -6
- package/src/background-jobs/types.js +27 -2
- package/src/background-jobs/web/controller.js +1 -0
- package/src/configuration.js +30 -6
- package/src/database/drivers/mssql/column.js +2 -2
- package/src/database/drivers/mssql/table.js +19 -5
- package/src/database/pool/async-tracked-multi-connection.js +2 -1
- package/src/database/query/alter-table-base.js +8 -1
- package/src/database/record/attachments/store.js +2 -2
- package/src/environment-handlers/node/cli/commands/test/timing-manifest/merge.js +2 -48
- package/src/environment-handlers/node/cli/commands/test.js +17 -69
- package/src/frontend-model-controller.js +22 -2
- package/src/frontend-models/websocket-channel.js +18 -0
- package/src/http-server/client/websocket-session.js +41 -4
- package/src/http-server/websocket-channel.js +14 -0
- package/src/http-server/websocket-event-log-store.js +85 -12
- package/src/http-server/websocket-events-host.js +17 -7
- package/src/testing/test-files-finder.js +32 -343
- package/src/testing/test-filter-parser.js +29 -248
- package/src/testing/test-profile-activity.js +1 -12
- package/src/testing/test-profile-output.js +8 -253
- package/src/testing/test-profiler.js +38 -895
- package/src/testing/test-runner.js +33 -9
- package/src/testing/test-suite-splitter.js +1 -301
- package/src/testing/timing-manifest.js +10 -344
- package/src/testing/velocious-attempt-executor.js +7 -1
|
@@ -1,918 +1,61 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import { compareTimingManifestPaths } from "./timing-manifest.js"
|
|
8
|
-
|
|
9
|
-
/** @typedef {"passed" | "failed" | "interrupted" | "timed-out"} TestProfileAttemptStatus */
|
|
10
|
-
/** @typedef {{user: number, system: number}} ProcessCpuUsage */
|
|
11
|
-
/** @typedef {{queryCount: number, failedQueryCount: number, totalMs: number, maxMs: number}} ProfileDatabaseAggregate */
|
|
12
|
-
/** @typedef {{count: number, failedCount: number, totalMs: number, maxMs: number}} ProfileActionAggregate */
|
|
13
|
-
/** @typedef {{identifier: string, connectionCreation: ProfileActionAggregate, checkoutWait: {count: number, totalMs: number, maxMs: number}, checkoutTimeoutCount: number, idleReap: ProfileActionAggregate & {disposalCount: number}, peakLiveConnections: number}} ProfilePoolAggregate */
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @typedef {object} TestProfileAsyncContext
|
|
17
|
-
* @property {TestProfiler} profiler - Owning profiler.
|
|
18
|
-
* @property {TestProfileAttemptRecord | undefined} attempt - Active test attempt.
|
|
19
|
-
* @property {boolean} active - Whether attribution is still open.
|
|
20
|
-
* @property {Set<TestProfileAsyncContext> | undefined} [attemptContexts] - Contexts sharing one attempt lifetime.
|
|
21
|
-
* @property {string | undefined} filePath - Project-relative owning test file.
|
|
22
|
-
* @property {TestProfileSpan | undefined} [span] - Innermost active profile span.
|
|
23
|
-
* @property {number} [spanStartedAt] - Span monotonic start time.
|
|
24
|
-
* @property {ProcessCpuUsage} [spanCpuStartedAt] - Span CPU start time.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* @typedef {object} TestProfileSpan
|
|
29
|
-
* @property {string} phase - Profile phase.
|
|
30
|
-
* @property {number} executionOrder - Monotonic span start order.
|
|
31
|
-
* @property {number} durationMs - Real duration.
|
|
32
|
-
* @property {{user: number, system: number, total: number}} cpuMs - Process CPU duration.
|
|
33
|
-
* @property {string} [activity] - Validated custom activity name.
|
|
34
|
-
* @property {number} [declarationIndex] - Hook index within its declaration scope.
|
|
35
|
-
* @property {string} [declarationScopeId] - Opaque declaration scope identifier.
|
|
36
|
-
* @property {string} [file] - Project-relative source path.
|
|
37
|
-
* @property {ProfileDatabaseAggregate} [database] - Span database aggregate.
|
|
38
|
-
* @property {ProfilePoolAggregate[]} [pools] - Span pool aggregates.
|
|
39
|
-
*/
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* @typedef {object} TestProfileAttemptRecord
|
|
43
|
-
* @property {number} number - One-indexed attempt number.
|
|
44
|
-
* @property {TestProfileAttemptStatus | "running"} status - Attempt result.
|
|
45
|
-
* @property {number} durationMs - Real duration.
|
|
46
|
-
* @property {{user: number, system: number, total: number}} cpuMs - Process CPU duration.
|
|
47
|
-
* @property {TestProfileSpan[]} spans - Attempt-owned spans.
|
|
48
|
-
*/
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* @typedef {object} TestProfileAttemptHandle
|
|
52
|
-
* @property {TestProfileAsyncContext} context - Async attribution context.
|
|
53
|
-
* @property {TestProfileAttemptRecord} attempt - Output attempt record.
|
|
54
|
-
* @property {number} startedAt - Real start time.
|
|
55
|
-
* @property {ProcessCpuUsage} cpuStartedAt - CPU start time.
|
|
56
|
-
*/
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* @typedef {object} InternalPhaseAggregate
|
|
60
|
-
* @property {number} count - Invocation count.
|
|
61
|
-
* @property {number} totalMs - Total real duration.
|
|
62
|
-
* @property {number} maxMs - Maximum real duration.
|
|
63
|
-
* @property {number} cpuUserMs - Total user CPU time.
|
|
64
|
-
* @property {number} cpuSystemMs - Total system CPU time.
|
|
65
|
-
*/
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* @typedef {object} InternalFileAggregate
|
|
69
|
-
* @property {string} path - Project-relative path.
|
|
70
|
-
* @property {number} importMs - Import duration.
|
|
71
|
-
* @property {number} hooksMs - Hook duration.
|
|
72
|
-
* @property {number} testsMs - Test body duration.
|
|
73
|
-
* @property {number} attemptsMs - Complete test-attempt duration.
|
|
74
|
-
* @property {number} totalMs - Total file weight duration.
|
|
75
|
-
*/
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* @typedef {object} InternalTestRecord
|
|
79
|
-
* @property {string} id - Opaque stable test identifier.
|
|
80
|
-
* @property {string} file - Project-relative source path.
|
|
81
|
-
* @property {number | undefined} line - Declaration line.
|
|
82
|
-
* @property {TestProfileAttemptRecord[]} attempts - Attempt records.
|
|
83
|
-
*/
|
|
84
|
-
|
|
85
|
-
const BUILT_IN_PHASES = [
|
|
86
|
-
"discovery",
|
|
87
|
-
"imports",
|
|
88
|
-
"testing config/global setup",
|
|
89
|
-
"beforeAll",
|
|
90
|
-
"beforeEach",
|
|
91
|
-
"test body",
|
|
92
|
-
"afterEach",
|
|
93
|
-
"afterAll"
|
|
94
|
-
]
|
|
95
|
-
const MAX_CUSTOM_ACTIVITY_NAMES = 20
|
|
96
|
-
const SAFE_SQL_OPERATIONS = new Set([
|
|
97
|
-
"ALTER",
|
|
98
|
-
"ANALYZE",
|
|
99
|
-
"BEGIN",
|
|
100
|
-
"CALL",
|
|
101
|
-
"COMMIT",
|
|
102
|
-
"COPY",
|
|
103
|
-
"CREATE",
|
|
104
|
-
"DELETE",
|
|
105
|
-
"DESCRIBE",
|
|
106
|
-
"DROP",
|
|
107
|
-
"EXEC",
|
|
108
|
-
"EXECUTE",
|
|
109
|
-
"EXPLAIN",
|
|
110
|
-
"GRANT",
|
|
111
|
-
"INSERT",
|
|
112
|
-
"MERGE",
|
|
113
|
-
"PRAGMA",
|
|
114
|
-
"RELEASE",
|
|
115
|
-
"REPLACE",
|
|
116
|
-
"REVOKE",
|
|
117
|
-
"ROLLBACK",
|
|
118
|
-
"SAVEPOINT",
|
|
119
|
-
"SELECT",
|
|
120
|
-
"SET",
|
|
121
|
-
"SHOW",
|
|
122
|
-
"TRUNCATE",
|
|
123
|
-
"UPDATE",
|
|
124
|
-
"VACUUM",
|
|
125
|
-
"VALUES",
|
|
126
|
-
"WITH"
|
|
127
|
-
])
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Rounds and bounds duration values for public output.
|
|
131
|
-
* @param {number} durationMs - Raw duration.
|
|
132
|
-
* @returns {number} - Safe duration.
|
|
133
|
-
*/
|
|
134
|
-
export function roundProfileDuration(durationMs) {
|
|
135
|
-
if (!Number.isFinite(durationMs) || durationMs < 0) return 0
|
|
136
|
-
|
|
137
|
-
return Math.round(durationMs * 1000) / 1000
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Collects opt-in test-run timing without retaining application payloads.
|
|
142
|
-
*/
|
|
143
|
-
export default class TestProfiler {
|
|
144
|
-
/**
|
|
145
|
-
* Creates an opt-in test profile collector.
|
|
146
|
-
* @param {object} args - Profiler options.
|
|
147
|
-
* @param {import("../configuration.js").default} args.configuration - Test configuration.
|
|
148
|
-
* @param {string} args.projectDirectory - Project root used for portable paths.
|
|
149
|
-
* @param {Record<string, ReturnType<typeof JSON.parse>>} [args.selection] - Sanitized selection metadata.
|
|
150
|
-
*/
|
|
151
|
-
constructor({configuration, projectDirectory, selection = {}}) {
|
|
152
|
-
this._configuration = configuration
|
|
153
|
-
this._projectDirectory = path.resolve(projectDirectory)
|
|
154
|
-
this._selection = selection
|
|
155
|
-
this._startedAt = this.now()
|
|
156
|
-
this._cpuStartedAt = process.cpuUsage()
|
|
157
|
-
this._executionOrder = 0
|
|
158
|
-
this._unattributedLateEventCount = 0
|
|
159
|
-
/** @type {Map<string, InternalPhaseAggregate>} */
|
|
160
|
-
this._phases = new Map()
|
|
161
|
-
/** @type {Map<string, InternalFileAggregate>} */
|
|
162
|
-
this._files = new Map()
|
|
163
|
-
/** @type {InternalTestRecord[]} */
|
|
164
|
-
this._tests = []
|
|
165
|
-
/** @type {Map<string, InternalTestRecord>} */
|
|
166
|
-
this._testsById = new Map()
|
|
167
|
-
/** @type {Set<TestProfileAttemptHandle>} */
|
|
168
|
-
this._activeAttempts = new Set()
|
|
169
|
-
/** @type {Set<TestProfileAsyncContext>} */
|
|
170
|
-
this._activeSpanContexts = new Set()
|
|
171
|
-
/** @type {TestProfileSpan[]} */
|
|
172
|
-
this._spans = []
|
|
173
|
-
/** @type {Array<{id: string, parentId: string | undefined, file: string, line: number | undefined}>} */
|
|
174
|
-
this._scopes = []
|
|
175
|
-
/** @type {WeakMap<import("./test-runner.js").TestsArgument | import("@velocious/testing/runner").SuiteDeclaration, string>} */
|
|
176
|
-
this._scopeIds = new WeakMap()
|
|
177
|
-
/** @type {Set<string>} */
|
|
178
|
-
this._customActivityNames = new Set()
|
|
179
|
-
this._database = this.emptyDatabaseAggregate()
|
|
180
|
-
/** @type {Map<string, {hash: string, operation: string, count: number, failedCount: number, totalMs: number, maxMs: number}>} */
|
|
181
|
-
this._queryFingerprints = new Map()
|
|
182
|
-
this._transactions = {
|
|
183
|
-
start: this.phaseAggregateShape(),
|
|
184
|
-
commit: this.phaseAggregateShape(),
|
|
185
|
-
rollback: this.phaseAggregateShape()
|
|
186
|
-
}
|
|
187
|
-
/** @type {Map<string, ProfilePoolAggregate>} */
|
|
188
|
-
this._pools = new Map()
|
|
189
|
-
this._finishedProfile = undefined
|
|
190
|
-
|
|
191
|
-
registerTestProfileContextReader(configuration, () => {
|
|
192
|
-
return configuration.getEnvironmentHandler().getCurrentTestProfileContext()
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
for (const phase of BUILT_IN_PHASES) this.phaseAggregate(phase)
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Returns the profiler's monotonic clock.
|
|
200
|
-
* @returns {number} - Monotonic milliseconds.
|
|
201
|
-
*/
|
|
202
|
-
now() {
|
|
203
|
-
return globalThis.performance.now()
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Converts a source path to a project-relative path or an opaque hash.
|
|
208
|
-
* @param {string | undefined} filePath - Source path.
|
|
209
|
-
* @returns {string} - Safe source identifier.
|
|
210
|
-
*/
|
|
211
|
-
safeSourcePath(filePath) {
|
|
212
|
-
if (!filePath) return "sha256:unknown"
|
|
213
|
-
|
|
214
|
-
const absolutePath = path.resolve(filePath)
|
|
215
|
-
const relativePath = path.relative(this._projectDirectory, absolutePath)
|
|
216
|
-
|
|
217
|
-
if (relativePath && !relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
|
|
218
|
-
return relativePath.replaceAll(path.sep, "/")
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (!relativePath) return path.basename(absolutePath)
|
|
222
|
-
|
|
223
|
-
return this.hash(`external-source:${absolutePath}`)
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Returns a bounded opaque SHA-256 identifier.
|
|
228
|
-
* @param {string} value - Value to hash.
|
|
229
|
-
* @returns {string} - Opaque hash.
|
|
230
|
-
*/
|
|
231
|
-
hash(value) {
|
|
232
|
-
return `sha256:${createHash("sha256").update(value).digest("hex").slice(0, 24)}`
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Adds aggregate-only selection metadata as discovery progresses.
|
|
237
|
-
* @param {Record<string, ReturnType<typeof JSON.parse>>} selection - Safe selection metadata.
|
|
238
|
-
* @returns {void}
|
|
239
|
-
*/
|
|
240
|
-
setSelection(selection) {
|
|
241
|
-
Object.assign(this._selection, selection)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Registers and returns a deterministic scope identifier.
|
|
246
|
-
* @param {import("./test-runner.js").TestsArgument | import("@velocious/testing/runner").SuiteDeclaration} scope - Scope object.
|
|
247
|
-
* @param {object} args - Scope metadata.
|
|
248
|
-
* @param {string[]} args.descriptions - Scope description path.
|
|
249
|
-
* @param {string | undefined} args.filePath - Scope source path.
|
|
250
|
-
* @param {number | undefined} [args.line] - Scope source line.
|
|
251
|
-
* @param {string | undefined} [args.parentId] - Parent scope identifier.
|
|
252
|
-
* @returns {string} - Opaque scope identifier.
|
|
253
|
-
*/
|
|
254
|
-
scopeId(scope, {descriptions, filePath, line, parentId}) {
|
|
255
|
-
const existing = this._scopeIds.get(scope)
|
|
256
|
-
if (existing) return existing
|
|
257
|
-
|
|
258
|
-
const safeFilePath = this.safeSourcePath(filePath)
|
|
259
|
-
const id = this.hash(`scope:${safeFilePath}:${line ?? 0}:${descriptions.join("\u0000")}`)
|
|
260
|
-
|
|
261
|
-
this._scopeIds.set(scope, id)
|
|
262
|
-
this._scopes.push({id, parentId, file: safeFilePath, line})
|
|
263
|
-
return id
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Starts an attempt and its async attribution context.
|
|
268
|
-
* @param {object} args - Attempt metadata.
|
|
269
|
-
* @param {string[]} args.descriptions - Parent descriptions.
|
|
270
|
-
* @param {number} args.attemptNumber - Attempt number.
|
|
271
|
-
* @param {import("./test-runner.js").TestData} args.testData - Test declaration.
|
|
272
|
-
* @param {string} args.testDescription - Test description.
|
|
273
|
-
* @returns {TestProfileAttemptHandle} - Active attempt handle.
|
|
274
|
-
*/
|
|
275
|
-
startAttempt({descriptions, attemptNumber, testData, testDescription}) {
|
|
276
|
-
const file = this.safeSourcePath(testData.ownerFilePath ?? testData.filePath)
|
|
277
|
-
const id = this.hash(`test:${file}:${testData.line ?? 0}:${[...descriptions, testDescription].join("\u0000")}`)
|
|
278
|
-
let testRecord = this._testsById.get(id)
|
|
279
|
-
|
|
280
|
-
if (!testRecord) {
|
|
281
|
-
testRecord = {id, file, line: testData.line, attempts: []}
|
|
282
|
-
this._testsById.set(id, testRecord)
|
|
283
|
-
this._tests.push(testRecord)
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/** @type {TestProfileAttemptRecord} */
|
|
287
|
-
const attempt = {
|
|
288
|
-
number: attemptNumber,
|
|
289
|
-
status: "running",
|
|
290
|
-
durationMs: 0,
|
|
291
|
-
cpuMs: {user: 0, system: 0, total: 0},
|
|
292
|
-
spans: []
|
|
293
|
-
}
|
|
294
|
-
/** @type {Set<TestProfileAsyncContext>} */
|
|
295
|
-
const attemptContexts = new Set()
|
|
296
|
-
const context = {profiler: this, attempt, active: true, attemptContexts, filePath: file, span: undefined}
|
|
297
|
-
|
|
298
|
-
attemptContexts.add(context)
|
|
299
|
-
|
|
300
|
-
const handle = {context, attempt, startedAt: this.now(), cpuStartedAt: process.cpuUsage()}
|
|
301
|
-
|
|
302
|
-
testRecord.attempts.push(attempt)
|
|
303
|
-
this._activeAttempts.add(handle)
|
|
304
|
-
|
|
305
|
-
return handle
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Runs work inside an attempt's async context.
|
|
310
|
-
* @template T
|
|
311
|
-
* @param {TestProfileAttemptHandle} handle - Attempt handle.
|
|
312
|
-
* @param {() => Promise<T>} callback - Attempt lifecycle.
|
|
313
|
-
* @returns {Promise<T>} - Callback result.
|
|
314
|
-
*/
|
|
315
|
-
async runAttempt(handle, callback) {
|
|
316
|
-
return await this._configuration
|
|
317
|
-
.getEnvironmentHandler()
|
|
318
|
-
.runWithTestProfileContext(handle.context, callback)
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Completes an attempt and prevents descendant late work from retaining attribution.
|
|
323
|
-
* @param {TestProfileAttemptHandle} handle - Attempt handle.
|
|
324
|
-
* @param {TestProfileAttemptStatus} status - Attempt result.
|
|
325
|
-
* @returns {void}
|
|
326
|
-
*/
|
|
327
|
-
finishAttempt(handle, status) {
|
|
328
|
-
if (handle.attempt.status !== "running") return
|
|
329
|
-
|
|
330
|
-
for (const context of handle.context.attemptContexts || []) {
|
|
331
|
-
if (context.span) this.finishSpanContext(context)
|
|
332
|
-
context.active = false
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
handle.attempt.status = status
|
|
336
|
-
handle.attempt.durationMs = roundProfileDuration(this.now() - handle.startedAt)
|
|
337
|
-
handle.attempt.cpuMs = this.cpuDuration(handle.cpuStartedAt)
|
|
338
|
-
this.addFileAttemptDuration(handle.context.filePath, handle.attempt.durationMs)
|
|
339
|
-
this._activeAttempts.delete(handle)
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Closes every active attempt and nested span at the current interruption boundary.
|
|
344
|
-
* @returns {void}
|
|
345
|
-
*/
|
|
346
|
-
interrupt() {
|
|
347
|
-
for (const handle of [...this._activeAttempts]) {
|
|
348
|
-
this.finishAttempt(handle, "interrupted")
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
for (const context of [...this._activeSpanContexts]) {
|
|
352
|
-
this.finishSpanContext(context)
|
|
353
|
-
}
|
|
354
|
-
}
|
|
3
|
+
import {
|
|
4
|
+
TestProfiler as PackageTestProfiler,
|
|
5
|
+
roundProfileDuration
|
|
6
|
+
} from "@velocious/testing/node"
|
|
355
7
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
* @template T
|
|
359
|
-
* @param {object} args - Span metadata.
|
|
360
|
-
* @param {string} args.phase - Phase name.
|
|
361
|
-
* @param {string} [args.activity] - Custom activity label.
|
|
362
|
-
* @param {number} [args.declarationIndex] - Hook declaration index.
|
|
363
|
-
* @param {string} [args.declarationScopeId] - Hook declaration scope.
|
|
364
|
-
* @param {string} [args.filePath] - Source or owning test file.
|
|
365
|
-
* @param {() => (T | Promise<T>)} callback - Timed work.
|
|
366
|
-
* @returns {Promise<T>} - Callback result.
|
|
367
|
-
*/
|
|
368
|
-
async runSpan({phase, activity, declarationIndex, declarationScopeId, filePath}, callback) {
|
|
369
|
-
const currentContext = this._configuration.getEnvironmentHandler().getCurrentTestProfileContext()
|
|
370
|
-
|
|
371
|
-
if (currentContext && !this.contextIsActive(currentContext)) {
|
|
372
|
-
this._unattributedLateEventCount++
|
|
373
|
-
return await callback()
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const safeFilePath = currentContext?.filePath ?? (filePath ? this.safeSourcePath(filePath) : undefined)
|
|
377
|
-
const startedAt = this.now()
|
|
378
|
-
const cpuStartedAt = process.cpuUsage()
|
|
379
|
-
/** @type {TestProfileSpan} */
|
|
380
|
-
const span = {
|
|
381
|
-
phase,
|
|
382
|
-
executionOrder: ++this._executionOrder,
|
|
383
|
-
durationMs: 0,
|
|
384
|
-
cpuMs: {user: 0, system: 0, total: 0}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
if (activity) span.activity = activity
|
|
388
|
-
if (declarationIndex !== undefined) span.declarationIndex = declarationIndex
|
|
389
|
-
if (declarationScopeId) span.declarationScopeId = declarationScopeId
|
|
390
|
-
if (safeFilePath) span.file = safeFilePath
|
|
391
|
-
|
|
392
|
-
if (currentContext?.attempt) {
|
|
393
|
-
currentContext.attempt.spans.push(span)
|
|
394
|
-
} else {
|
|
395
|
-
this._spans.push(span)
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const spanContext = {
|
|
399
|
-
profiler: this,
|
|
400
|
-
attempt: currentContext?.attempt,
|
|
401
|
-
active: true,
|
|
402
|
-
attemptContexts: currentContext?.attemptContexts,
|
|
403
|
-
filePath: safeFilePath,
|
|
404
|
-
span,
|
|
405
|
-
spanStartedAt: startedAt,
|
|
406
|
-
spanCpuStartedAt: cpuStartedAt
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
spanContext.attemptContexts?.add(spanContext)
|
|
410
|
-
this._activeSpanContexts.add(spanContext)
|
|
411
|
-
|
|
412
|
-
try {
|
|
413
|
-
return await this._configuration
|
|
414
|
-
.getEnvironmentHandler()
|
|
415
|
-
.runWithTestProfileContext(spanContext, callback)
|
|
416
|
-
} finally {
|
|
417
|
-
if (spanContext.active) {
|
|
418
|
-
this.finishSpanContext(spanContext)
|
|
419
|
-
} else if (spanContext.attempt) {
|
|
420
|
-
this._unattributedLateEventCount++
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
8
|
+
import restArgsError from "../utils/rest-args-error.js"
|
|
9
|
+
import { registerTestProfileContextReader } from "./test-profile-context.js"
|
|
424
10
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
* @returns {void}
|
|
429
|
-
*/
|
|
430
|
-
finishSpanContext(context) {
|
|
431
|
-
if (!context.active || !context.span || context.spanStartedAt === undefined || !context.spanCpuStartedAt) return
|
|
11
|
+
/** @typedef {import("@velocious/testing/node").TestProfileAsyncContext} TestProfileAsyncContext */
|
|
12
|
+
/** @typedef {import("@velocious/testing/node").TestProfileAttemptHandle} TestProfileAttemptHandle */
|
|
13
|
+
/** @typedef {import("@velocious/testing/node").TestProfileAttemptStatus} TestProfileAttemptStatus */
|
|
432
14
|
|
|
433
|
-
|
|
434
|
-
context.span.durationMs = roundProfileDuration(this.now() - context.spanStartedAt)
|
|
435
|
-
context.span.cpuMs = this.cpuDuration(context.spanCpuStartedAt)
|
|
436
|
-
this.addPhaseDuration(context.span.phase, context.span.durationMs, context.span.cpuMs)
|
|
437
|
-
this.addFileDuration(context.filePath, context.span.phase, context.span.durationMs, Boolean(context.attempt))
|
|
438
|
-
this._activeSpanContexts.delete(context)
|
|
439
|
-
}
|
|
15
|
+
export { roundProfileDuration }
|
|
440
16
|
|
|
17
|
+
/** Connects the package profiler to Velocious's environment-owned async context. */
|
|
18
|
+
class VelociousProfileContextAdapter {
|
|
441
19
|
/**
|
|
442
|
-
*
|
|
443
|
-
* @param {
|
|
444
|
-
* @returns {boolean} - Whether events may still be attributed.
|
|
20
|
+
* Creates the environment context adapter.
|
|
21
|
+
* @param {import("../environment-handlers/base.js").default} environmentHandler - Active environment handler.
|
|
445
22
|
*/
|
|
446
|
-
|
|
447
|
-
|
|
23
|
+
constructor(environmentHandler) {
|
|
24
|
+
this.environmentHandler = environmentHandler
|
|
448
25
|
}
|
|
449
26
|
|
|
450
27
|
/**
|
|
451
|
-
*
|
|
452
|
-
* @
|
|
453
|
-
* @param {TestProfileAsyncContext} context - Captured async context.
|
|
454
|
-
* @param {string} name - Validated activity name.
|
|
455
|
-
* @param {() => (T | Promise<T>)} callback - Activity callback.
|
|
456
|
-
* @returns {Promise<T>} - Callback result.
|
|
28
|
+
* Gets the active framework profile context.
|
|
29
|
+
* @returns {TestProfileAsyncContext | undefined} - Current profile attribution.
|
|
457
30
|
*/
|
|
458
|
-
|
|
459
|
-
if (!this.contextIsActive(context)) {
|
|
460
|
-
this._unattributedLateEventCount++
|
|
461
|
-
return await callback()
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
const validatedName = validateTestActivityName(name)
|
|
465
|
-
let outputName = validatedName
|
|
466
|
-
|
|
467
|
-
if (!this._customActivityNames.has(validatedName)) {
|
|
468
|
-
if (this._customActivityNames.size >= MAX_CUSTOM_ACTIVITY_NAMES) {
|
|
469
|
-
outputName = "other"
|
|
470
|
-
} else {
|
|
471
|
-
this._customActivityNames.add(validatedName)
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
return await this.runSpan({phase: "custom", activity: outputName}, callback)
|
|
476
|
-
}
|
|
31
|
+
current() { return this.environmentHandler.getCurrentTestProfileContext() }
|
|
477
32
|
|
|
478
33
|
/**
|
|
479
|
-
*
|
|
34
|
+
* Runs work with framework profile attribution.
|
|
480
35
|
* @template T
|
|
481
|
-
* @param {
|
|
482
|
-
* @param {() =>
|
|
483
|
-
* @
|
|
484
|
-
* @returns {Promise<T>} - Callback result.
|
|
485
|
-
*/
|
|
486
|
-
async measurePhase(phase, callback, metadata = {}) {
|
|
487
|
-
return await this.runSpan({phase, filePath: metadata.filePath}, callback)
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Records one successful or failed physical database query attempt.
|
|
492
|
-
* @param {TestProfileAsyncContext} context - Context captured when the query began.
|
|
493
|
-
* @param {{durationMs: number, failed: boolean, sqlFingerprint: string, sqlOperation: string}} args - Query aggregate values.
|
|
494
|
-
* @returns {void}
|
|
36
|
+
* @param {TestProfileAsyncContext} context - Profile attribution to install.
|
|
37
|
+
* @param {() => T} callback - Work to run.
|
|
38
|
+
* @returns {T} - Callback result.
|
|
495
39
|
*/
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
this._unattributedLateEventCount++
|
|
499
|
-
return
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
const safeDurationMs = roundProfileDuration(durationMs)
|
|
503
|
-
|
|
504
|
-
this.addDatabaseQueryAggregate(this._database, {durationMs: safeDurationMs, failed})
|
|
505
|
-
if (context.span) {
|
|
506
|
-
context.span.database ||= this.emptyDatabaseAggregate()
|
|
507
|
-
this.addDatabaseQueryAggregate(context.span.database, {durationMs: safeDurationMs, failed})
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
const safeOperation = SAFE_SQL_OPERATIONS.has(sqlOperation) ? sqlOperation : "UNKNOWN"
|
|
511
|
-
const fingerprintKey = `${safeOperation}:${sqlFingerprint}`
|
|
512
|
-
let fingerprint = this._queryFingerprints.get(fingerprintKey)
|
|
513
|
-
|
|
514
|
-
if (!fingerprint && this._queryFingerprints.size < 50) {
|
|
515
|
-
fingerprint = {
|
|
516
|
-
hash: sqlFingerprint,
|
|
517
|
-
operation: safeOperation,
|
|
518
|
-
count: 0,
|
|
519
|
-
failedCount: 0,
|
|
520
|
-
totalMs: 0,
|
|
521
|
-
maxMs: 0
|
|
522
|
-
}
|
|
523
|
-
this._queryFingerprints.set(fingerprintKey, fingerprint)
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
if (fingerprint) {
|
|
527
|
-
fingerprint.count++
|
|
528
|
-
if (failed) fingerprint.failedCount++
|
|
529
|
-
fingerprint.totalMs += safeDurationMs
|
|
530
|
-
fingerprint.maxMs = Math.max(fingerprint.maxMs, safeDurationMs)
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
/**
|
|
535
|
-
* Records a physical transaction action.
|
|
536
|
-
* @param {TestProfileAsyncContext} context - Context captured when the action began.
|
|
537
|
-
* @param {{action: "start" | "commit" | "rollback", durationMs: number, failed: boolean}} args - Transaction aggregate values.
|
|
538
|
-
* @returns {void}
|
|
539
|
-
*/
|
|
540
|
-
recordDatabaseTransaction(context, {action, durationMs, failed}) {
|
|
541
|
-
if (!this.contextIsActive(context)) {
|
|
542
|
-
this._unattributedLateEventCount++
|
|
543
|
-
return
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
const aggregate = this._transactions[action]
|
|
547
|
-
const safeDurationMs = roundProfileDuration(durationMs)
|
|
548
|
-
|
|
549
|
-
aggregate.count++
|
|
550
|
-
if (failed) aggregate.failedCount++
|
|
551
|
-
aggregate.totalMs += safeDurationMs
|
|
552
|
-
aggregate.maxMs = Math.max(aggregate.maxMs, safeDurationMs)
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
/**
|
|
556
|
-
* Records one low-cardinality pool lifecycle delta.
|
|
557
|
-
* @param {TestProfileAsyncContext} context - Context captured when the operation began.
|
|
558
|
-
* @param {string} identifier - Logical pool identifier.
|
|
559
|
-
* @param {"connectionCreation" | "checkoutWait" | "checkoutTimeout" | "idleReap" | "idleReapDisposal" | "peakLiveConnections"} metric - Metric name.
|
|
560
|
-
* @param {{durationMs?: number, failed?: boolean, value?: number}} [values] - Aggregate values.
|
|
561
|
-
* @returns {void}
|
|
562
|
-
*/
|
|
563
|
-
recordPoolMetric(context, identifier, metric, values = {}) {
|
|
564
|
-
if (!this.contextIsActive(context)) {
|
|
565
|
-
this._unattributedLateEventCount++
|
|
566
|
-
return
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
const aggregate = this.poolAggregate(this._pools, identifier)
|
|
570
|
-
|
|
571
|
-
this.addPoolMetric(aggregate, metric, values)
|
|
572
|
-
|
|
573
|
-
if (context.span) {
|
|
574
|
-
context.span.pools ||= []
|
|
575
|
-
const spanPools = new Map(context.span.pools.map((pool) => [pool.identifier, pool]))
|
|
576
|
-
const spanAggregate = this.poolAggregate(spanPools, identifier)
|
|
577
|
-
|
|
578
|
-
if (!context.span.pools.includes(spanAggregate)) context.span.pools.push(spanAggregate)
|
|
579
|
-
this.addPoolMetric(spanAggregate, metric, values)
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
/**
|
|
584
|
-
* Builds the final privacy-safe profile document.
|
|
585
|
-
* @param {object} args - Run result.
|
|
586
|
-
* @param {{discovered: number, executed: number, failed: number, passed: number}} args.counts - Run counts.
|
|
587
|
-
* @param {boolean} args.focused - Whether focused tests were selected.
|
|
588
|
-
* @param {string} args.status - Run status.
|
|
589
|
-
* @returns {ReturnType<typeof JSON.parse>} - Rich profile document.
|
|
590
|
-
*/
|
|
591
|
-
finish({counts, focused, status}) {
|
|
592
|
-
if (this._finishedProfile) return this._finishedProfile
|
|
593
|
-
|
|
594
|
-
const totalDurationMs = roundProfileDuration(this.now() - this._startedAt)
|
|
595
|
-
const totalCpuMs = this.cpuDuration(this._cpuStartedAt)
|
|
596
|
-
const exclusivePhaseNames = BUILT_IN_PHASES
|
|
597
|
-
const measuredDurationMs = exclusivePhaseNames.reduce((sum, phase) => sum + this.phaseAggregate(phase).totalMs, 0)
|
|
598
|
-
const measuredCpuUserMs = exclusivePhaseNames.reduce((sum, phase) => sum + this.phaseAggregate(phase).cpuUserMs, 0)
|
|
599
|
-
const measuredCpuSystemMs = exclusivePhaseNames.reduce((sum, phase) => sum + this.phaseAggregate(phase).cpuSystemMs, 0)
|
|
600
|
-
|
|
601
|
-
this._phases.set("runner overhead", {
|
|
602
|
-
count: 1,
|
|
603
|
-
totalMs: Math.max(0, totalDurationMs - measuredDurationMs),
|
|
604
|
-
maxMs: Math.max(0, totalDurationMs - measuredDurationMs),
|
|
605
|
-
cpuUserMs: Math.max(0, totalCpuMs.user - measuredCpuUserMs),
|
|
606
|
-
cpuSystemMs: Math.max(0, totalCpuMs.system - measuredCpuSystemMs)
|
|
607
|
-
})
|
|
608
|
-
this._phases.set("total", {
|
|
609
|
-
count: 1,
|
|
610
|
-
totalMs: totalDurationMs,
|
|
611
|
-
maxMs: totalDurationMs,
|
|
612
|
-
cpuUserMs: totalCpuMs.user,
|
|
613
|
-
cpuSystemMs: totalCpuMs.system
|
|
614
|
-
})
|
|
615
|
-
|
|
616
|
-
const phases = Object.fromEntries([...this._phases.entries()].map(([phase, aggregate]) => [
|
|
617
|
-
phase,
|
|
618
|
-
this.publicAggregate(aggregate)
|
|
619
|
-
]))
|
|
620
|
-
const files = [...this._files.values()]
|
|
621
|
-
.map((file) => ({
|
|
622
|
-
path: file.path,
|
|
623
|
-
importMs: roundProfileDuration(file.importMs),
|
|
624
|
-
hooksMs: roundProfileDuration(file.hooksMs),
|
|
625
|
-
testsMs: roundProfileDuration(file.testsMs),
|
|
626
|
-
attemptsMs: roundProfileDuration(file.attemptsMs),
|
|
627
|
-
totalMs: roundProfileDuration(file.totalMs)
|
|
628
|
-
}))
|
|
629
|
-
.sort((fileA, fileB) => compareTimingManifestPaths(fileA.path, fileB.path))
|
|
630
|
-
const timingManifest = Object.fromEntries(files.map((file) => [file.path, file.totalMs]))
|
|
631
|
-
const attempts = this._tests.reduce((sum, test) => sum + test.attempts.length, 0)
|
|
632
|
-
|
|
633
|
-
this._finishedProfile = {
|
|
634
|
-
schema: "velocious.test-profile",
|
|
635
|
-
schemaVersion: 1,
|
|
636
|
-
status,
|
|
637
|
-
selection: {...this._selection, focused},
|
|
638
|
-
counts: {...counts, attempts},
|
|
639
|
-
durationMs: totalDurationMs,
|
|
640
|
-
cpuMs: totalCpuMs,
|
|
641
|
-
phases,
|
|
642
|
-
files,
|
|
643
|
-
scopes: [...this._scopes].sort((scopeA, scopeB) => scopeA.id.localeCompare(scopeB.id)),
|
|
644
|
-
tests: this._tests.map((test) => ({
|
|
645
|
-
id: test.id,
|
|
646
|
-
file: test.file,
|
|
647
|
-
line: test.line,
|
|
648
|
-
durationMs: roundProfileDuration(test.attempts.reduce((sum, attempt) => sum + attempt.durationMs, 0)),
|
|
649
|
-
attempts: test.attempts
|
|
650
|
-
})),
|
|
651
|
-
spans: this._spans,
|
|
652
|
-
database: {
|
|
653
|
-
queryCount: this._database.queryCount,
|
|
654
|
-
failedQueryCount: this._database.failedQueryCount,
|
|
655
|
-
totalMs: roundProfileDuration(this._database.totalMs),
|
|
656
|
-
maxMs: roundProfileDuration(this._database.maxMs),
|
|
657
|
-
fingerprints: [...this._queryFingerprints.values()]
|
|
658
|
-
.map((fingerprint) => ({
|
|
659
|
-
...fingerprint,
|
|
660
|
-
totalMs: roundProfileDuration(fingerprint.totalMs),
|
|
661
|
-
maxMs: roundProfileDuration(fingerprint.maxMs)
|
|
662
|
-
}))
|
|
663
|
-
.sort((fingerprintA, fingerprintB) => {
|
|
664
|
-
return fingerprintA.operation.localeCompare(fingerprintB.operation) || fingerprintA.hash.localeCompare(fingerprintB.hash)
|
|
665
|
-
}),
|
|
666
|
-
transactions: {
|
|
667
|
-
start: this.publicActionAggregate(this._transactions.start),
|
|
668
|
-
commit: this.publicActionAggregate(this._transactions.commit),
|
|
669
|
-
rollback: this.publicActionAggregate(this._transactions.rollback)
|
|
670
|
-
}
|
|
671
|
-
},
|
|
672
|
-
pools: [...this._pools.values()]
|
|
673
|
-
.map((pool) => this.publicPoolAggregate(pool))
|
|
674
|
-
.sort((poolA, poolB) => poolA.identifier.localeCompare(poolB.identifier)),
|
|
675
|
-
unattributedLateEventCount: this._unattributedLateEventCount,
|
|
676
|
-
timingManifest
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
return this._finishedProfile
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
/**
|
|
683
|
-
* Returns an internal phase aggregate.
|
|
684
|
-
* @param {string} phase - Phase name.
|
|
685
|
-
* @returns {InternalPhaseAggregate} - Aggregate.
|
|
686
|
-
*/
|
|
687
|
-
phaseAggregate(phase) {
|
|
688
|
-
let aggregate = this._phases.get(phase)
|
|
689
|
-
|
|
690
|
-
if (!aggregate) {
|
|
691
|
-
aggregate = {count: 0, totalMs: 0, maxMs: 0, cpuUserMs: 0, cpuSystemMs: 0}
|
|
692
|
-
this._phases.set(phase, aggregate)
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
return aggregate
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
/**
|
|
699
|
-
* Adds a completed span to its aggregate phase.
|
|
700
|
-
* @param {string} phase - Phase name.
|
|
701
|
-
* @param {number} durationMs - Real duration.
|
|
702
|
-
* @param {{user: number, system: number, total: number}} cpuMs - CPU duration.
|
|
703
|
-
* @returns {void}
|
|
704
|
-
*/
|
|
705
|
-
addPhaseDuration(phase, durationMs, cpuMs) {
|
|
706
|
-
const aggregate = this.phaseAggregate(phase)
|
|
707
|
-
|
|
708
|
-
aggregate.count++
|
|
709
|
-
aggregate.totalMs += durationMs
|
|
710
|
-
aggregate.maxMs = Math.max(aggregate.maxMs, durationMs)
|
|
711
|
-
aggregate.cpuUserMs += cpuMs.user
|
|
712
|
-
aggregate.cpuSystemMs += cpuMs.system
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
/**
|
|
716
|
-
* Adds an exclusive file-owned phase to timing-manifest weight.
|
|
717
|
-
* @param {string | undefined} filePath - Safe project-relative path.
|
|
718
|
-
* @param {string} phase - Phase name.
|
|
719
|
-
* @param {number} durationMs - Real duration.
|
|
720
|
-
* @param {boolean} [insideAttempt] - Whether the phase is already covered by complete attempt time.
|
|
721
|
-
* @returns {void}
|
|
722
|
-
*/
|
|
723
|
-
addFileDuration(filePath, phase, durationMs, insideAttempt = false) {
|
|
724
|
-
if (!filePath) return
|
|
725
|
-
if (!["imports", "beforeAll", "beforeEach", "test body", "afterEach", "afterAll"].includes(phase)) return
|
|
726
|
-
|
|
727
|
-
let file = this._files.get(filePath)
|
|
728
|
-
|
|
729
|
-
if (!file) {
|
|
730
|
-
file = {path: filePath, importMs: 0, hooksMs: 0, testsMs: 0, attemptsMs: 0, totalMs: 0}
|
|
731
|
-
this._files.set(filePath, file)
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
if (phase === "imports") file.importMs += durationMs
|
|
735
|
-
if (phase === "test body") file.testsMs += durationMs
|
|
736
|
-
if (["beforeAll", "beforeEach", "afterEach", "afterAll"].includes(phase)) file.hooksMs += durationMs
|
|
737
|
-
if (!insideAttempt) file.totalMs += durationMs
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
/**
|
|
741
|
-
* Adds the complete cost of an attempt to its owning file weight.
|
|
742
|
-
* @param {string | undefined} filePath - Safe project-relative path.
|
|
743
|
-
* @param {number} durationMs - Attempt duration.
|
|
744
|
-
* @returns {void}
|
|
745
|
-
*/
|
|
746
|
-
addFileAttemptDuration(filePath, durationMs) {
|
|
747
|
-
if (!filePath) return
|
|
748
|
-
|
|
749
|
-
let file = this._files.get(filePath)
|
|
750
|
-
|
|
751
|
-
if (!file) {
|
|
752
|
-
file = {path: filePath, importMs: 0, hooksMs: 0, testsMs: 0, attemptsMs: 0, totalMs: 0}
|
|
753
|
-
this._files.set(filePath, file)
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
file.attemptsMs += durationMs
|
|
757
|
-
file.totalMs += durationMs
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
/**
|
|
761
|
-
* Calculates process CPU time since a start sample.
|
|
762
|
-
* @param {ProcessCpuUsage} start - CPU start sample.
|
|
763
|
-
* @returns {{user: number, system: number, total: number}} - Millisecond CPU duration.
|
|
764
|
-
*/
|
|
765
|
-
cpuDuration(start) {
|
|
766
|
-
const cpu = process.cpuUsage(start)
|
|
767
|
-
const user = roundProfileDuration(cpu.user / 1000)
|
|
768
|
-
const system = roundProfileDuration(cpu.system / 1000)
|
|
769
|
-
|
|
770
|
-
return {user, system, total: roundProfileDuration(user + system)}
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
/**
|
|
774
|
-
* Converts an internal aggregate to public rounded values.
|
|
775
|
-
* @param {InternalPhaseAggregate} [aggregate] - Internal aggregate.
|
|
776
|
-
* @returns {{count: number, totalMs: number, maxMs: number, cpuMs: {user: number, system: number, total: number}}} - Public aggregate.
|
|
777
|
-
*/
|
|
778
|
-
publicAggregate(aggregate = {count: 0, totalMs: 0, maxMs: 0, cpuUserMs: 0, cpuSystemMs: 0}) {
|
|
779
|
-
const user = roundProfileDuration(aggregate.cpuUserMs)
|
|
780
|
-
const system = roundProfileDuration(aggregate.cpuSystemMs)
|
|
781
|
-
|
|
782
|
-
return {
|
|
783
|
-
count: aggregate.count,
|
|
784
|
-
totalMs: roundProfileDuration(aggregate.totalMs),
|
|
785
|
-
maxMs: roundProfileDuration(aggregate.maxMs),
|
|
786
|
-
cpuMs: {user, system, total: roundProfileDuration(user + system)}
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
/**
|
|
791
|
-
* Returns an empty query aggregate.
|
|
792
|
-
* @returns {ProfileDatabaseAggregate} - Empty aggregate.
|
|
793
|
-
*/
|
|
794
|
-
emptyDatabaseAggregate() {
|
|
795
|
-
return {queryCount: 0, failedQueryCount: 0, totalMs: 0, maxMs: 0}
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
/**
|
|
799
|
-
* Adds a query attempt to a database aggregate.
|
|
800
|
-
* @param {ProfileDatabaseAggregate} aggregate - Aggregate to update.
|
|
801
|
-
* @param {{durationMs: number, failed: boolean}} args - Attempt values.
|
|
802
|
-
* @returns {void}
|
|
803
|
-
*/
|
|
804
|
-
addDatabaseQueryAggregate(aggregate, {durationMs, failed}) {
|
|
805
|
-
aggregate.queryCount++
|
|
806
|
-
if (failed) aggregate.failedQueryCount++
|
|
807
|
-
aggregate.totalMs += durationMs
|
|
808
|
-
aggregate.maxMs = Math.max(aggregate.maxMs, durationMs)
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
/**
|
|
812
|
-
* Returns an empty action aggregate.
|
|
813
|
-
* @returns {ProfileActionAggregate} - Empty aggregate.
|
|
814
|
-
*/
|
|
815
|
-
phaseAggregateShape() {
|
|
816
|
-
return {count: 0, failedCount: 0, totalMs: 0, maxMs: 0}
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
/**
|
|
820
|
-
* Rounds a transaction or pool action aggregate.
|
|
821
|
-
* @param {ProfileActionAggregate} aggregate - Internal aggregate.
|
|
822
|
-
* @returns {ProfileActionAggregate} - Public aggregate.
|
|
823
|
-
*/
|
|
824
|
-
publicActionAggregate(aggregate) {
|
|
825
|
-
return {
|
|
826
|
-
count: aggregate.count,
|
|
827
|
-
failedCount: aggregate.failedCount,
|
|
828
|
-
totalMs: roundProfileDuration(aggregate.totalMs),
|
|
829
|
-
maxMs: roundProfileDuration(aggregate.maxMs)
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
/**
|
|
834
|
-
* Returns an empty pool aggregate.
|
|
835
|
-
* @param {string} [identifier] - Logical pool identifier.
|
|
836
|
-
* @returns {ProfilePoolAggregate} - Empty aggregate.
|
|
837
|
-
*/
|
|
838
|
-
emptyPoolAggregate(identifier = "") {
|
|
839
|
-
return {
|
|
840
|
-
identifier,
|
|
841
|
-
connectionCreation: this.phaseAggregateShape(),
|
|
842
|
-
checkoutWait: {count: 0, totalMs: 0, maxMs: 0},
|
|
843
|
-
checkoutTimeoutCount: 0,
|
|
844
|
-
idleReap: {...this.phaseAggregateShape(), disposalCount: 0},
|
|
845
|
-
peakLiveConnections: 0
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
/**
|
|
850
|
-
* Gets or creates a pool aggregate in a map.
|
|
851
|
-
* @param {Map<string, ProfilePoolAggregate>} pools - Pool map.
|
|
852
|
-
* @param {string} identifier - Logical pool identifier.
|
|
853
|
-
* @returns {ProfilePoolAggregate} - Pool aggregate.
|
|
854
|
-
*/
|
|
855
|
-
poolAggregate(pools, identifier) {
|
|
856
|
-
let aggregate = pools.get(identifier)
|
|
857
|
-
|
|
858
|
-
if (!aggregate) {
|
|
859
|
-
aggregate = this.emptyPoolAggregate(identifier)
|
|
860
|
-
pools.set(identifier, aggregate)
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
return aggregate
|
|
40
|
+
run(context, callback) {
|
|
41
|
+
return this.environmentHandler.runWithTestProfileContext(context, callback)
|
|
864
42
|
}
|
|
43
|
+
}
|
|
865
44
|
|
|
45
|
+
/** Velocious compatibility facade around the package-owned profiler. */
|
|
46
|
+
export default class TestProfiler extends PackageTestProfiler {
|
|
866
47
|
/**
|
|
867
|
-
*
|
|
868
|
-
* @param {
|
|
869
|
-
* @param {"
|
|
870
|
-
* @param {
|
|
871
|
-
* @
|
|
48
|
+
* Creates the package profiler with Velocious environment context.
|
|
49
|
+
* @param {object} args - Profiler options.
|
|
50
|
+
* @param {import("../configuration.js").default} args.configuration - Test configuration.
|
|
51
|
+
* @param {string} args.projectDirectory - Project root used for portable paths.
|
|
52
|
+
* @param {Partial<import("@velocious/testing/node").TestProfileSelection>} [args.selection] - Selection metadata.
|
|
872
53
|
*/
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
if (metric === "connectionCreation" || metric === "idleReap") {
|
|
877
|
-
const actionAggregate = metric === "connectionCreation" ? aggregate.connectionCreation : aggregate.idleReap
|
|
878
|
-
|
|
879
|
-
actionAggregate.count++
|
|
880
|
-
if (values.failed) actionAggregate.failedCount++
|
|
881
|
-
actionAggregate.totalMs += durationMs
|
|
882
|
-
actionAggregate.maxMs = Math.max(actionAggregate.maxMs, durationMs)
|
|
883
|
-
} else if (metric === "checkoutWait") {
|
|
884
|
-
aggregate.checkoutWait.count++
|
|
885
|
-
aggregate.checkoutWait.totalMs += durationMs
|
|
886
|
-
aggregate.checkoutWait.maxMs = Math.max(aggregate.checkoutWait.maxMs, durationMs)
|
|
887
|
-
} else if (metric === "checkoutTimeout") {
|
|
888
|
-
aggregate.checkoutTimeoutCount++
|
|
889
|
-
} else if (metric === "idleReapDisposal") {
|
|
890
|
-
aggregate.idleReap.disposalCount++
|
|
891
|
-
} else if (metric === "peakLiveConnections") {
|
|
892
|
-
aggregate.peakLiveConnections = Math.max(aggregate.peakLiveConnections, values.value ?? 0)
|
|
893
|
-
}
|
|
894
|
-
}
|
|
54
|
+
constructor({configuration, projectDirectory, selection, ...restArgs}) {
|
|
55
|
+
restArgsError(restArgs)
|
|
56
|
+
const contextAdapter = new VelociousProfileContextAdapter(configuration.getEnvironmentHandler())
|
|
895
57
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
* @param {ProfilePoolAggregate} aggregate - Internal aggregate.
|
|
899
|
-
* @returns {ProfilePoolAggregate} - Public aggregate.
|
|
900
|
-
*/
|
|
901
|
-
publicPoolAggregate(aggregate) {
|
|
902
|
-
return {
|
|
903
|
-
identifier: aggregate.identifier,
|
|
904
|
-
connectionCreation: this.publicActionAggregate(aggregate.connectionCreation),
|
|
905
|
-
checkoutWait: {
|
|
906
|
-
count: aggregate.checkoutWait.count,
|
|
907
|
-
totalMs: roundProfileDuration(aggregate.checkoutWait.totalMs),
|
|
908
|
-
maxMs: roundProfileDuration(aggregate.checkoutWait.maxMs)
|
|
909
|
-
},
|
|
910
|
-
checkoutTimeoutCount: aggregate.checkoutTimeoutCount,
|
|
911
|
-
idleReap: {
|
|
912
|
-
...this.publicActionAggregate(aggregate.idleReap),
|
|
913
|
-
disposalCount: aggregate.idleReap.disposalCount
|
|
914
|
-
},
|
|
915
|
-
peakLiveConnections: aggregate.peakLiveConnections
|
|
916
|
-
}
|
|
58
|
+
super({contextAdapter, projectDirectory, selection})
|
|
59
|
+
registerTestProfileContextReader(configuration, () => contextAdapter.current())
|
|
917
60
|
}
|
|
918
61
|
}
|