proteum 2.1.2 → 2.1.6
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/AGENTS.md +22 -14
- package/README.md +112 -17
- package/agents/project/AGENTS.md +188 -25
- package/agents/project/CODING_STYLE.md +1 -0
- package/agents/project/client/AGENTS.md +13 -8
- package/agents/project/client/pages/AGENTS.md +17 -9
- package/agents/project/diagnostics.md +52 -0
- package/agents/project/optimizations.md +48 -0
- package/agents/project/server/routes/AGENTS.md +9 -6
- package/agents/project/server/services/AGENTS.md +10 -6
- package/agents/project/tests/AGENTS.md +11 -5
- package/cli/app/config.ts +13 -14
- package/cli/app/index.ts +58 -0
- package/cli/commands/command.ts +8 -0
- package/cli/commands/connect.ts +45 -0
- package/cli/commands/dev.ts +26 -11
- package/cli/commands/diagnose.ts +286 -0
- package/cli/commands/doctor.ts +18 -5
- package/cli/commands/explain.ts +25 -0
- package/cli/commands/perf.ts +243 -0
- package/cli/commands/session.ts +254 -0
- package/cli/commands/sessionLocalRunner.js +188 -0
- package/cli/commands/trace.ts +17 -1
- package/cli/commands/verify.ts +281 -0
- package/cli/compiler/artifacts/connectedProjects.ts +453 -0
- package/cli/compiler/artifacts/controllers.ts +198 -49
- package/cli/compiler/artifacts/discovery.ts +0 -34
- package/cli/compiler/artifacts/manifest.ts +90 -6
- package/cli/compiler/artifacts/routing.ts +2 -2
- package/cli/compiler/artifacts/services.ts +277 -130
- package/cli/compiler/client/index.ts +3 -0
- package/cli/compiler/common/files/style.ts +52 -0
- package/cli/compiler/common/generatedRouteModules.ts +34 -5
- package/cli/compiler/common/scripts.ts +11 -5
- package/cli/compiler/index.ts +2 -1
- package/cli/compiler/server/index.ts +3 -0
- package/cli/presentation/commands.ts +136 -7
- package/cli/presentation/devSession.ts +32 -7
- package/cli/runtime/commands.ts +193 -6
- package/cli/scaffold/index.ts +14 -25
- package/cli/scaffold/templates.ts +41 -27
- package/cli/utils/agents.ts +4 -2
- package/cli/utils/keyboard.ts +8 -0
- package/client/dev/profiler/ApexChart.tsx +66 -0
- package/client/dev/profiler/index.tsx +2798 -417
- package/client/dev/profiler/runtime.noop.ts +12 -0
- package/client/dev/profiler/runtime.ts +195 -4
- package/client/services/router/request/api.ts +6 -1
- package/common/applicationConfig.ts +173 -0
- package/common/applicationConfigLoader.ts +102 -0
- package/common/connectedProjects.ts +113 -0
- package/common/dev/connect.ts +267 -0
- package/common/dev/console.ts +31 -0
- package/common/dev/contractsDoctor.ts +128 -0
- package/common/dev/diagnostics.ts +59 -15
- package/common/dev/inspection.ts +491 -0
- package/common/dev/performance.ts +809 -0
- package/common/dev/profiler.ts +3 -0
- package/common/dev/proteumManifest.ts +31 -6
- package/common/dev/requestTrace.ts +56 -1
- package/common/dev/session.ts +24 -0
- package/common/env/proteumEnv.ts +176 -50
- package/common/router/index.ts +1 -0
- package/common/router/request/api.ts +2 -0
- package/config.ts +5 -0
- package/docs/dev-commands.md +5 -1
- package/docs/dev-sessions.md +90 -0
- package/docs/diagnostics.md +74 -11
- package/docs/request-tracing.md +50 -3
- package/package.json +1 -1
- package/server/app/container/config.ts +16 -87
- package/server/app/container/console/index.ts +42 -8
- package/server/app/container/index.ts +3 -1
- package/server/app/container/trace/index.ts +153 -0
- package/server/app/devDiagnostics.ts +138 -0
- package/server/app/index.ts +18 -8
- package/server/app/service/container.ts +0 -12
- package/server/app/service/index.ts +0 -2
- package/server/services/prisma/index.ts +121 -4
- package/server/services/router/http/index.ts +352 -0
- package/server/services/router/index.ts +50 -47
- package/server/services/router/request/api.ts +160 -19
- package/server/services/router/request/index.ts +8 -0
- package/server/services/router/response/index.ts +24 -1
- package/server/services/router/response/page/document.tsx +5 -0
- package/server/services/router/response/page/index.tsx +10 -0
- package/agents/framework/AGENTS.md +0 -177
- package/server/services/auth/router/service.json +0 -6
- package/server/services/auth/service.json +0 -6
- package/server/services/cron/service.json +0 -6
- package/server/services/disks/drivers/local/service.json +0 -6
- package/server/services/disks/drivers/s3/service.json +0 -6
- package/server/services/disks/service.json +0 -6
- package/server/services/fetch/service.json +0 -7
- package/server/services/prisma/service.json +0 -6
- package/server/services/router/service.json +0 -6
- package/server/services/schema/router/service.json +0 -6
- package/server/services/schema/service.json +0 -6
- package/server/services/security/encrypt/aes/service.json +0 -6
|
@@ -0,0 +1,809 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TRequestTrace,
|
|
3
|
+
TRequestTracePerformance,
|
|
4
|
+
TTraceCall,
|
|
5
|
+
TTraceEvent,
|
|
6
|
+
TTraceMemorySnapshot,
|
|
7
|
+
TTraceSqlQuery,
|
|
8
|
+
TTraceSummaryValue,
|
|
9
|
+
} from './requestTrace';
|
|
10
|
+
|
|
11
|
+
export const perfGroupByValues = ['path', 'route', 'controller'] as const;
|
|
12
|
+
export const perfWindowPresets = ['1h', '6h', '24h', 'today', 'yesterday'] as const;
|
|
13
|
+
export const perfStageIds = ['auth', 'routing', 'controller', 'page-data', 'render', 'response'] as const;
|
|
14
|
+
|
|
15
|
+
export type TPerfGroupBy = (typeof perfGroupByValues)[number];
|
|
16
|
+
export type TPerfWindowPreset = (typeof perfWindowPresets)[number];
|
|
17
|
+
export type TPerfStageId = (typeof perfStageIds)[number];
|
|
18
|
+
|
|
19
|
+
export type TPerfStage = {
|
|
20
|
+
durationMs: number;
|
|
21
|
+
endOffsetMs: number;
|
|
22
|
+
id: TPerfStageId;
|
|
23
|
+
label: string;
|
|
24
|
+
startOffsetMs: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type TRequestPerfCall = {
|
|
28
|
+
durationMs?: number;
|
|
29
|
+
errorMessage?: string;
|
|
30
|
+
id: string;
|
|
31
|
+
label: string;
|
|
32
|
+
method: string;
|
|
33
|
+
origin: TTraceCall['origin'];
|
|
34
|
+
path: string;
|
|
35
|
+
statusCode?: number;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type TRequestPerfSql = {
|
|
39
|
+
callerLabel: string;
|
|
40
|
+
durationMs: number;
|
|
41
|
+
id: string;
|
|
42
|
+
kind: TTraceSqlQuery['kind'];
|
|
43
|
+
model?: string;
|
|
44
|
+
operation: string;
|
|
45
|
+
query: string;
|
|
46
|
+
target?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type TRequestPerformance = {
|
|
50
|
+
avgCallDurationMs?: number;
|
|
51
|
+
avgSqlDurationMs?: number;
|
|
52
|
+
callCount: number;
|
|
53
|
+
callDurationMs: number;
|
|
54
|
+
capture: TRequestTrace['capture'];
|
|
55
|
+
controllerLabel: string;
|
|
56
|
+
cpuSystemMs?: number;
|
|
57
|
+
cpuTotalMs?: number;
|
|
58
|
+
cpuUserMs?: number;
|
|
59
|
+
documentBytes?: number;
|
|
60
|
+
errorMessage?: string;
|
|
61
|
+
externalAfterBytes?: number;
|
|
62
|
+
externalBeforeBytes?: number;
|
|
63
|
+
externalDeltaBytes?: number;
|
|
64
|
+
finishedAt?: string;
|
|
65
|
+
heapAfterBytes?: number;
|
|
66
|
+
heapBeforeBytes?: number;
|
|
67
|
+
heapDeltaBytes?: number;
|
|
68
|
+
hottestCalls: TRequestPerfCall[];
|
|
69
|
+
hottestSqlQueries: TRequestPerfSql[];
|
|
70
|
+
htmlBytes?: number;
|
|
71
|
+
method: string;
|
|
72
|
+
path: string;
|
|
73
|
+
renderDurationMs?: number;
|
|
74
|
+
requestId: string;
|
|
75
|
+
responseDurationMs?: number;
|
|
76
|
+
routeLabel: string;
|
|
77
|
+
rssAfterBytes?: number;
|
|
78
|
+
rssBeforeBytes?: number;
|
|
79
|
+
rssDeltaBytes?: number;
|
|
80
|
+
selfDurationMs?: number;
|
|
81
|
+
sqlCount: number;
|
|
82
|
+
sqlDurationMs: number;
|
|
83
|
+
ssrPayloadBytes?: number;
|
|
84
|
+
stages: TPerfStage[];
|
|
85
|
+
startedAt: string;
|
|
86
|
+
statusCode?: number;
|
|
87
|
+
totalDurationMs?: number;
|
|
88
|
+
user?: string;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export type TPerfWindow = {
|
|
92
|
+
availableRequestCount: number;
|
|
93
|
+
finishedAt: string;
|
|
94
|
+
label: string;
|
|
95
|
+
requestCount: number;
|
|
96
|
+
startedAt: string;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export type TPerfTopRow = {
|
|
100
|
+
avgCallDurationMs: number;
|
|
101
|
+
avgCpuMs: number;
|
|
102
|
+
avgDurationMs: number;
|
|
103
|
+
avgHeapDeltaBytes: number;
|
|
104
|
+
avgRenderDurationMs: number;
|
|
105
|
+
avgRssDeltaBytes: number;
|
|
106
|
+
avgSelfDurationMs: number;
|
|
107
|
+
avgSqlDurationMs: number;
|
|
108
|
+
avgSsrPayloadBytes: number;
|
|
109
|
+
errorCount: number;
|
|
110
|
+
groupBy: TPerfGroupBy;
|
|
111
|
+
key: string;
|
|
112
|
+
label: string;
|
|
113
|
+
latestRequestId?: string;
|
|
114
|
+
maxDurationMs: number;
|
|
115
|
+
maxHeapDeltaBytes: number;
|
|
116
|
+
maxRssDeltaBytes: number;
|
|
117
|
+
maxSsrPayloadBytes: number;
|
|
118
|
+
p95DurationMs: number;
|
|
119
|
+
requestCount: number;
|
|
120
|
+
slowestRequestId?: string;
|
|
121
|
+
totalCallDurationMs: number;
|
|
122
|
+
totalCpuMs: number;
|
|
123
|
+
totalDurationMs: number;
|
|
124
|
+
totalRenderDurationMs: number;
|
|
125
|
+
totalSqlDurationMs: number;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export type TPerfTopSummary = Omit<TPerfTopRow, 'groupBy' | 'key' | 'label' | 'latestRequestId' | 'slowestRequestId'>;
|
|
129
|
+
|
|
130
|
+
export type TPerfTopResponse = {
|
|
131
|
+
groupBy: TPerfGroupBy;
|
|
132
|
+
limit: number;
|
|
133
|
+
rows: TPerfTopRow[];
|
|
134
|
+
summary: TPerfTopSummary;
|
|
135
|
+
window: TPerfWindow;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export type TPerfMetricDelta = {
|
|
139
|
+
baseline: number;
|
|
140
|
+
delta: number;
|
|
141
|
+
deltaPercent?: number;
|
|
142
|
+
target: number;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export type TPerfCompareChange = 'changed' | 'improved' | 'new' | 'regressed' | 'removed';
|
|
146
|
+
|
|
147
|
+
export type TPerfCompareRow = {
|
|
148
|
+
avgCpuMs: TPerfMetricDelta;
|
|
149
|
+
avgDurationMs: TPerfMetricDelta;
|
|
150
|
+
avgHeapDeltaBytes: TPerfMetricDelta;
|
|
151
|
+
avgRenderDurationMs: TPerfMetricDelta;
|
|
152
|
+
avgSqlDurationMs: TPerfMetricDelta;
|
|
153
|
+
change: TPerfCompareChange;
|
|
154
|
+
groupBy: TPerfGroupBy;
|
|
155
|
+
key: string;
|
|
156
|
+
label: string;
|
|
157
|
+
p95DurationMs: TPerfMetricDelta;
|
|
158
|
+
requestCount: TPerfMetricDelta;
|
|
159
|
+
score: number;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export type TPerfCompareResponse = {
|
|
163
|
+
baseline: TPerfWindow;
|
|
164
|
+
groupBy: TPerfGroupBy;
|
|
165
|
+
limit: number;
|
|
166
|
+
rows: TPerfCompareRow[];
|
|
167
|
+
target: TPerfWindow;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export type TPerfMemoryTrend = 'mixed' | 'rising' | 'stable';
|
|
171
|
+
|
|
172
|
+
export type TPerfMemoryRow = {
|
|
173
|
+
avgHeapDeltaBytes: number;
|
|
174
|
+
avgRssDeltaBytes: number;
|
|
175
|
+
groupBy: TPerfGroupBy;
|
|
176
|
+
key: string;
|
|
177
|
+
label: string;
|
|
178
|
+
maxHeapDeltaBytes: number;
|
|
179
|
+
maxRssDeltaBytes: number;
|
|
180
|
+
positiveHeapDriftCount: number;
|
|
181
|
+
positiveHeapDriftRatio: number;
|
|
182
|
+
positiveRssDriftCount: number;
|
|
183
|
+
positiveRssDriftRatio: number;
|
|
184
|
+
requestCount: number;
|
|
185
|
+
trend: TPerfMemoryTrend;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export type TPerfMemoryResponse = {
|
|
189
|
+
groupBy: TPerfGroupBy;
|
|
190
|
+
limit: number;
|
|
191
|
+
rows: TPerfMemoryRow[];
|
|
192
|
+
window: TPerfWindow;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export type TPerfRequestResponse = {
|
|
196
|
+
request: TRequestPerformance;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const authEventTypes = [
|
|
200
|
+
'auth.decode',
|
|
201
|
+
'auth.route',
|
|
202
|
+
'auth.check.start',
|
|
203
|
+
'auth.check.rule',
|
|
204
|
+
'auth.check.result',
|
|
205
|
+
'auth.session',
|
|
206
|
+
] as const;
|
|
207
|
+
|
|
208
|
+
const readNumber = (value: TTraceSummaryValue | undefined) => (typeof value === 'number' ? value : undefined);
|
|
209
|
+
const readString = (value: TTraceSummaryValue | undefined) => (typeof value === 'string' ? value : undefined);
|
|
210
|
+
const readDateMs = (value?: string) => {
|
|
211
|
+
if (!value) return undefined;
|
|
212
|
+
const parsed = Date.parse(value);
|
|
213
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const average = (values: number[]) => (values.length === 0 ? 0 : values.reduce((sum, value) => sum + value, 0) / values.length);
|
|
217
|
+
|
|
218
|
+
const percentile = (values: number[], percentileValue: number) => {
|
|
219
|
+
if (values.length === 0) return 0;
|
|
220
|
+
|
|
221
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
222
|
+
const rank = Math.min(sorted.length - 1, Math.max(0, Math.ceil(percentileValue * sorted.length) - 1));
|
|
223
|
+
return sorted[rank];
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const startOfUtcDay = (timestampMs: number) => {
|
|
227
|
+
const date = new Date(timestampMs);
|
|
228
|
+
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const toIso = (timestampMs: number) => new Date(timestampMs).toISOString();
|
|
232
|
+
|
|
233
|
+
const resolvePerfWindow = (rawWindow: string | undefined, requests: TRequestTrace[], nowMs = Date.now()): TPerfWindow => {
|
|
234
|
+
const normalized = (rawWindow || 'today').trim().toLowerCase();
|
|
235
|
+
let startedAtMs: number;
|
|
236
|
+
let finishedAtMs = nowMs;
|
|
237
|
+
|
|
238
|
+
if (normalized === 'today') {
|
|
239
|
+
startedAtMs = startOfUtcDay(nowMs);
|
|
240
|
+
} else if (normalized === 'yesterday') {
|
|
241
|
+
finishedAtMs = startOfUtcDay(nowMs);
|
|
242
|
+
startedAtMs = finishedAtMs - 24 * 60 * 60 * 1000;
|
|
243
|
+
} else {
|
|
244
|
+
const durationMatch = normalized.match(/^(\d+)(m|h|d)$/);
|
|
245
|
+
if (durationMatch) {
|
|
246
|
+
const amount = Number.parseInt(durationMatch[1], 10);
|
|
247
|
+
const unit = durationMatch[2];
|
|
248
|
+
const factor = unit === 'm' ? 60 * 1000 : unit === 'h' ? 60 * 60 * 1000 : 24 * 60 * 60 * 1000;
|
|
249
|
+
startedAtMs = nowMs - amount * factor;
|
|
250
|
+
} else {
|
|
251
|
+
const parsed = Date.parse(rawWindow || '');
|
|
252
|
+
if (Number.isNaN(parsed)) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
`Unsupported perf window "${rawWindow}". Expected one of ${perfWindowPresets.join(', ')} or an ISO timestamp.`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
startedAtMs = parsed;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const requestCount = requests.filter((request) => {
|
|
263
|
+
const requestStartMs = readDateMs(request.startedAt);
|
|
264
|
+
return requestStartMs !== undefined && requestStartMs >= startedAtMs && requestStartMs <= finishedAtMs;
|
|
265
|
+
}).length;
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
availableRequestCount: requests.length,
|
|
269
|
+
finishedAt: toIso(finishedAtMs),
|
|
270
|
+
label: rawWindow || 'today',
|
|
271
|
+
requestCount,
|
|
272
|
+
startedAt: toIso(startedAtMs),
|
|
273
|
+
};
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const selectRequestsInWindow = (requests: TRequestTrace[], rawWindow: string | undefined, nowMs = Date.now()) => {
|
|
277
|
+
const finishedRequests = requests.filter((request) => request.finishedAt && request.durationMs !== undefined);
|
|
278
|
+
const window = resolvePerfWindow(rawWindow, finishedRequests, nowMs);
|
|
279
|
+
const windowStartMs = readDateMs(window.startedAt) || 0;
|
|
280
|
+
const windowEndMs = readDateMs(window.finishedAt) || nowMs;
|
|
281
|
+
const filteredRequests = finishedRequests.filter((request) => {
|
|
282
|
+
const requestStartMs = readDateMs(request.startedAt);
|
|
283
|
+
return requestStartMs !== undefined && requestStartMs >= windowStartMs && requestStartMs <= windowEndMs;
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
requests: filteredRequests,
|
|
288
|
+
window: { ...window, availableRequestCount: finishedRequests.length, requestCount: filteredRequests.length },
|
|
289
|
+
};
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const findFirstEvent = (trace: TRequestTrace, eventTypes: readonly string[]) => trace.events.find((event) => eventTypes.includes(event.type));
|
|
293
|
+
|
|
294
|
+
const findLastEvent = (trace: TRequestTrace, eventTypes: readonly string[]) => {
|
|
295
|
+
for (let index = trace.events.length - 1; index >= 0; index -= 1) {
|
|
296
|
+
const event = trace.events[index];
|
|
297
|
+
if (eventTypes.includes(event.type)) return event;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return undefined;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const buildStage = (input: { id: TPerfStageId; label: string; startOffsetMs?: number; endOffsetMs?: number }) => {
|
|
304
|
+
if (input.startOffsetMs === undefined || input.endOffsetMs === undefined) return undefined;
|
|
305
|
+
if (input.endOffsetMs <= input.startOffsetMs) return undefined;
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
durationMs: Math.max(0, input.endOffsetMs - input.startOffsetMs),
|
|
309
|
+
endOffsetMs: input.endOffsetMs,
|
|
310
|
+
id: input.id,
|
|
311
|
+
label: input.label,
|
|
312
|
+
startOffsetMs: input.startOffsetMs,
|
|
313
|
+
} satisfies TPerfStage;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const readStageOffset = (event: TTraceEvent | undefined) => event?.elapsedMs;
|
|
317
|
+
|
|
318
|
+
const formatCallLabel = (call: TTraceCall) => {
|
|
319
|
+
if (call.connectedProjectNamespace && call.connectedControllerAccessor) {
|
|
320
|
+
return `${call.connectedProjectNamespace}.${call.connectedControllerAccessor}`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const reference = `${call.method} ${call.path}`.trim();
|
|
324
|
+
if (call.label && reference) return `${call.label} (${reference})`;
|
|
325
|
+
return call.label || reference || call.origin;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const formatSqlCallerLabel = (query: TTraceSqlQuery) => {
|
|
329
|
+
const reference = `${query.callerMethod} ${query.callerPath}`.trim();
|
|
330
|
+
if (query.callerLabel && reference) return `${query.callerLabel} (${reference})`;
|
|
331
|
+
return query.callerLabel || reference || query.operation;
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const readRequestPerformanceMetric = (performance: TRequestTracePerformance | undefined, selector: (snapshot: TTraceMemorySnapshot) => number) => {
|
|
335
|
+
if (!performance) return { after: undefined, before: undefined, delta: undefined };
|
|
336
|
+
|
|
337
|
+
const before = selector(performance.memory.before);
|
|
338
|
+
const after = selector(performance.memory.after);
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
after,
|
|
342
|
+
before,
|
|
343
|
+
delta: after - before,
|
|
344
|
+
};
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const findRouteLabel = (trace: TRequestTrace) => {
|
|
348
|
+
const routeMatch = findFirstEvent(trace, ['resolve.route-match']);
|
|
349
|
+
const controllerRoute = findFirstEvent(trace, ['resolve.controller-route']);
|
|
350
|
+
return (
|
|
351
|
+
readString(routeMatch?.details.routeId) ||
|
|
352
|
+
readString(routeMatch?.details.routePath) ||
|
|
353
|
+
readString(controllerRoute?.details.path) ||
|
|
354
|
+
trace.path
|
|
355
|
+
);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const findControllerLabel = (trace: TRequestTrace) => {
|
|
359
|
+
const controllerStart = findFirstEvent(trace, ['controller.start']);
|
|
360
|
+
return readString(controllerStart?.details.target) || readString(controllerStart?.details.filepath) || findRouteLabel(trace);
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const buildStages = (trace: TRequestTrace) => {
|
|
364
|
+
const requestFinish = findLastEvent(trace, ['request.finish']);
|
|
365
|
+
const controllerResult = findFirstEvent(trace, ['controller.result']);
|
|
366
|
+
const pageData = findFirstEvent(trace, ['page.data']);
|
|
367
|
+
const renderStart = findFirstEvent(trace, ['render.start']);
|
|
368
|
+
const renderEnd = findFirstEvent(trace, ['render.end']);
|
|
369
|
+
const responseSend = findFirstEvent(trace, ['response.send']);
|
|
370
|
+
const resolveStart = findFirstEvent(trace, ['resolve.start']);
|
|
371
|
+
const routeResolved = findFirstEvent(trace, ['resolve.controller-route', 'resolve.route-match', 'resolve.not-found']);
|
|
372
|
+
|
|
373
|
+
return [
|
|
374
|
+
buildStage({
|
|
375
|
+
endOffsetMs: readStageOffset(findLastEvent(trace, authEventTypes)),
|
|
376
|
+
id: 'auth',
|
|
377
|
+
label: 'Auth',
|
|
378
|
+
startOffsetMs: readStageOffset(findFirstEvent(trace, authEventTypes)),
|
|
379
|
+
}),
|
|
380
|
+
buildStage({
|
|
381
|
+
endOffsetMs: readStageOffset(routeResolved),
|
|
382
|
+
id: 'routing',
|
|
383
|
+
label: 'Routing',
|
|
384
|
+
startOffsetMs: readStageOffset(resolveStart),
|
|
385
|
+
}),
|
|
386
|
+
buildStage({
|
|
387
|
+
endOffsetMs: readStageOffset(controllerResult),
|
|
388
|
+
id: 'controller',
|
|
389
|
+
label: 'Controller',
|
|
390
|
+
startOffsetMs: readStageOffset(findFirstEvent(trace, ['controller.start'])),
|
|
391
|
+
}),
|
|
392
|
+
buildStage({
|
|
393
|
+
endOffsetMs: readStageOffset(pageData),
|
|
394
|
+
id: 'page-data',
|
|
395
|
+
label: 'Page Data',
|
|
396
|
+
startOffsetMs: readStageOffset(controllerResult),
|
|
397
|
+
}),
|
|
398
|
+
buildStage({
|
|
399
|
+
endOffsetMs: readStageOffset(renderEnd),
|
|
400
|
+
id: 'render',
|
|
401
|
+
label: 'Render',
|
|
402
|
+
startOffsetMs: readStageOffset(renderStart),
|
|
403
|
+
}),
|
|
404
|
+
buildStage({
|
|
405
|
+
endOffsetMs: readStageOffset(requestFinish),
|
|
406
|
+
id: 'response',
|
|
407
|
+
label: 'Response',
|
|
408
|
+
startOffsetMs: readStageOffset(responseSend),
|
|
409
|
+
}),
|
|
410
|
+
].filter((stage): stage is TPerfStage => stage !== undefined);
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const buildMetricDelta = (baseline: number, target: number): TPerfMetricDelta => ({
|
|
414
|
+
baseline,
|
|
415
|
+
delta: target - baseline,
|
|
416
|
+
deltaPercent: baseline === 0 ? (target === 0 ? 0 : undefined) : ((target - baseline) / baseline) * 100,
|
|
417
|
+
target,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const deriveCompareChange = (row: Omit<TPerfCompareRow, 'change' | 'score'>): TPerfCompareChange => {
|
|
421
|
+
if (row.requestCount.baseline === 0 && row.requestCount.target > 0) return 'new';
|
|
422
|
+
if (row.requestCount.baseline > 0 && row.requestCount.target === 0) return 'removed';
|
|
423
|
+
|
|
424
|
+
const regressionSignals = [
|
|
425
|
+
row.avgDurationMs.delta,
|
|
426
|
+
row.p95DurationMs.delta,
|
|
427
|
+
row.avgCpuMs.delta,
|
|
428
|
+
row.avgHeapDeltaBytes.delta,
|
|
429
|
+
row.avgSqlDurationMs.delta,
|
|
430
|
+
];
|
|
431
|
+
const improvementSignals = regressionSignals.filter((value) => value < 0).length;
|
|
432
|
+
const regressionCount = regressionSignals.filter((value) => value > 0).length;
|
|
433
|
+
|
|
434
|
+
if (regressionCount > improvementSignals) return 'regressed';
|
|
435
|
+
if (improvementSignals > regressionCount) return 'improved';
|
|
436
|
+
return 'changed';
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const summarizeProfiles = (profiles: TRequestPerformance[]): TPerfTopSummary => {
|
|
440
|
+
const durations = profiles.map((profile) => profile.totalDurationMs || 0);
|
|
441
|
+
const cpu = profiles.map((profile) => profile.cpuTotalMs || 0);
|
|
442
|
+
const heapDeltas = profiles.map((profile) => profile.heapDeltaBytes || 0);
|
|
443
|
+
const rssDeltas = profiles.map((profile) => profile.rssDeltaBytes || 0);
|
|
444
|
+
const renderDurations = profiles.map((profile) => profile.renderDurationMs || 0);
|
|
445
|
+
const sqlDurations = profiles.map((profile) => profile.sqlDurationMs);
|
|
446
|
+
const callDurations = profiles.map((profile) => profile.callDurationMs);
|
|
447
|
+
const selfDurations = profiles.map((profile) => profile.selfDurationMs || 0);
|
|
448
|
+
const ssrPayloads = profiles.map((profile) => profile.ssrPayloadBytes || 0);
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
avgCallDurationMs: average(callDurations),
|
|
452
|
+
avgCpuMs: average(cpu),
|
|
453
|
+
avgDurationMs: average(durations),
|
|
454
|
+
avgHeapDeltaBytes: average(heapDeltas),
|
|
455
|
+
avgRenderDurationMs: average(renderDurations),
|
|
456
|
+
avgRssDeltaBytes: average(rssDeltas),
|
|
457
|
+
avgSelfDurationMs: average(selfDurations),
|
|
458
|
+
avgSqlDurationMs: average(sqlDurations),
|
|
459
|
+
avgSsrPayloadBytes: average(ssrPayloads),
|
|
460
|
+
errorCount: profiles.filter((profile) => profile.errorMessage || (profile.statusCode !== undefined && profile.statusCode >= 400)).length,
|
|
461
|
+
maxDurationMs: durations.reduce((value, durationMs) => Math.max(value, durationMs), 0),
|
|
462
|
+
maxHeapDeltaBytes: heapDeltas.reduce((value, delta) => Math.max(value, delta), 0),
|
|
463
|
+
maxRssDeltaBytes: rssDeltas.reduce((value, delta) => Math.max(value, delta), 0),
|
|
464
|
+
maxSsrPayloadBytes: ssrPayloads.reduce((value, bytes) => Math.max(value, bytes), 0),
|
|
465
|
+
p95DurationMs: percentile(durations, 0.95),
|
|
466
|
+
requestCount: profiles.length,
|
|
467
|
+
totalCallDurationMs: callDurations.reduce((count, durationMs) => count + durationMs, 0),
|
|
468
|
+
totalCpuMs: cpu.reduce((count, durationMs) => count + durationMs, 0),
|
|
469
|
+
totalDurationMs: durations.reduce((count, durationMs) => count + durationMs, 0),
|
|
470
|
+
totalRenderDurationMs: renderDurations.reduce((count, durationMs) => count + durationMs, 0),
|
|
471
|
+
totalSqlDurationMs: sqlDurations.reduce((count, durationMs) => count + durationMs, 0),
|
|
472
|
+
};
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
export const buildRequestPerformance = (trace: TRequestTrace): TRequestPerformance => {
|
|
476
|
+
const performance = trace.performance;
|
|
477
|
+
const cpuUserMs = performance ? performance.cpu.userMicros / 1000 : undefined;
|
|
478
|
+
const cpuSystemMs = performance ? performance.cpu.systemMicros / 1000 : undefined;
|
|
479
|
+
const cpuTotalMs = cpuUserMs !== undefined && cpuSystemMs !== undefined ? cpuUserMs + cpuSystemMs : undefined;
|
|
480
|
+
const heapMetrics = readRequestPerformanceMetric(performance, (snapshot) => snapshot.heapUsed);
|
|
481
|
+
const rssMetrics = readRequestPerformanceMetric(performance, (snapshot) => snapshot.rss);
|
|
482
|
+
const externalMetrics = readRequestPerformanceMetric(performance, (snapshot) => snapshot.external);
|
|
483
|
+
const renderEnd = findFirstEvent(trace, ['render.end']);
|
|
484
|
+
const renderStart = findFirstEvent(trace, ['render.start']);
|
|
485
|
+
const ssrPayload = findFirstEvent(trace, ['ssr.payload']);
|
|
486
|
+
const stages = buildStages(trace);
|
|
487
|
+
const renderDurationMs =
|
|
488
|
+
renderStart && renderEnd && renderEnd.elapsedMs >= renderStart.elapsedMs
|
|
489
|
+
? renderEnd.elapsedMs - renderStart.elapsedMs
|
|
490
|
+
: undefined;
|
|
491
|
+
const responseStage = stages.find((stage) => stage.id === 'response');
|
|
492
|
+
const sqlDurationMs = trace.sqlQueries.reduce((count, query) => count + query.durationMs, 0);
|
|
493
|
+
const callDurations = trace.calls.map((call) => call.durationMs || 0);
|
|
494
|
+
const callDurationMs = callDurations.reduce((count, durationMs) => count + durationMs, 0);
|
|
495
|
+
const totalDurationMs = trace.durationMs;
|
|
496
|
+
const selfDurationMs =
|
|
497
|
+
totalDurationMs !== undefined ? Math.max(0, totalDurationMs - sqlDurationMs - callDurationMs - (renderDurationMs || 0)) : undefined;
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
avgCallDurationMs: trace.calls.length > 0 ? callDurationMs / trace.calls.length : undefined,
|
|
501
|
+
avgSqlDurationMs: trace.sqlQueries.length > 0 ? sqlDurationMs / trace.sqlQueries.length : undefined,
|
|
502
|
+
callCount: trace.calls.length,
|
|
503
|
+
callDurationMs,
|
|
504
|
+
capture: trace.capture,
|
|
505
|
+
controllerLabel: findControllerLabel(trace),
|
|
506
|
+
cpuSystemMs,
|
|
507
|
+
cpuTotalMs,
|
|
508
|
+
cpuUserMs,
|
|
509
|
+
documentBytes: readNumber(renderEnd?.details.documentLength),
|
|
510
|
+
errorMessage: trace.errorMessage,
|
|
511
|
+
externalAfterBytes: externalMetrics.after,
|
|
512
|
+
externalBeforeBytes: externalMetrics.before,
|
|
513
|
+
externalDeltaBytes: externalMetrics.delta,
|
|
514
|
+
finishedAt: trace.finishedAt,
|
|
515
|
+
heapAfterBytes: heapMetrics.after,
|
|
516
|
+
heapBeforeBytes: heapMetrics.before,
|
|
517
|
+
heapDeltaBytes: heapMetrics.delta,
|
|
518
|
+
hottestCalls: [...trace.calls]
|
|
519
|
+
.sort((left, right) => (right.durationMs || 0) - (left.durationMs || 0))
|
|
520
|
+
.slice(0, 5)
|
|
521
|
+
.map((call) => ({
|
|
522
|
+
durationMs: call.durationMs,
|
|
523
|
+
errorMessage: call.errorMessage,
|
|
524
|
+
id: call.id,
|
|
525
|
+
label: formatCallLabel(call),
|
|
526
|
+
method: call.method,
|
|
527
|
+
origin: call.origin,
|
|
528
|
+
path: call.path,
|
|
529
|
+
statusCode: call.statusCode,
|
|
530
|
+
})),
|
|
531
|
+
hottestSqlQueries: [...trace.sqlQueries]
|
|
532
|
+
.sort((left, right) => right.durationMs - left.durationMs)
|
|
533
|
+
.slice(0, 5)
|
|
534
|
+
.map((query) => ({
|
|
535
|
+
callerLabel: formatSqlCallerLabel(query),
|
|
536
|
+
durationMs: query.durationMs,
|
|
537
|
+
id: query.id,
|
|
538
|
+
kind: query.kind,
|
|
539
|
+
model: query.model,
|
|
540
|
+
operation: query.operation,
|
|
541
|
+
query: query.query,
|
|
542
|
+
target: query.target,
|
|
543
|
+
})),
|
|
544
|
+
htmlBytes: readNumber(renderEnd?.details.htmlLength),
|
|
545
|
+
method: trace.method,
|
|
546
|
+
path: trace.path,
|
|
547
|
+
renderDurationMs,
|
|
548
|
+
requestId: trace.id,
|
|
549
|
+
responseDurationMs: responseStage?.durationMs,
|
|
550
|
+
routeLabel: findRouteLabel(trace),
|
|
551
|
+
rssAfterBytes: rssMetrics.after,
|
|
552
|
+
rssBeforeBytes: rssMetrics.before,
|
|
553
|
+
rssDeltaBytes: rssMetrics.delta,
|
|
554
|
+
selfDurationMs,
|
|
555
|
+
sqlCount: trace.sqlQueries.length,
|
|
556
|
+
sqlDurationMs,
|
|
557
|
+
ssrPayloadBytes: readNumber(ssrPayload?.details.serializedBytes),
|
|
558
|
+
stages,
|
|
559
|
+
startedAt: trace.startedAt,
|
|
560
|
+
statusCode: trace.statusCode,
|
|
561
|
+
totalDurationMs,
|
|
562
|
+
user: trace.user,
|
|
563
|
+
};
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const readGroupValue = (groupBy: TPerfGroupBy, request: TRequestPerformance) => {
|
|
567
|
+
if (groupBy === 'controller') return request.controllerLabel;
|
|
568
|
+
if (groupBy === 'route') return request.routeLabel;
|
|
569
|
+
return request.path;
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
export const buildPerfTopResponse = ({
|
|
573
|
+
groupBy = 'path',
|
|
574
|
+
limit = 12,
|
|
575
|
+
requests,
|
|
576
|
+
since = 'today',
|
|
577
|
+
}: {
|
|
578
|
+
groupBy?: TPerfGroupBy;
|
|
579
|
+
limit?: number;
|
|
580
|
+
requests: TRequestTrace[];
|
|
581
|
+
since?: string;
|
|
582
|
+
}): TPerfTopResponse => {
|
|
583
|
+
const selectedGroupBy = perfGroupByValues.includes(groupBy) ? groupBy : 'path';
|
|
584
|
+
const { requests: filteredRequests, window } = selectRequestsInWindow(requests, since);
|
|
585
|
+
const profiles = filteredRequests.map(buildRequestPerformance);
|
|
586
|
+
const groups = new Map<string, TRequestPerformance[]>();
|
|
587
|
+
|
|
588
|
+
for (const profile of profiles) {
|
|
589
|
+
const key = readGroupValue(selectedGroupBy, profile) || profile.path || 'request';
|
|
590
|
+
const existing = groups.get(key);
|
|
591
|
+
if (existing) existing.push(profile);
|
|
592
|
+
else groups.set(key, [profile]);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const rows = [...groups.entries()]
|
|
596
|
+
.map(([key, groupProfiles]) => {
|
|
597
|
+
const durations = groupProfiles.map((profile) => profile.totalDurationMs || 0);
|
|
598
|
+
const cpu = groupProfiles.map((profile) => profile.cpuTotalMs || 0);
|
|
599
|
+
const heapDeltas = groupProfiles.map((profile) => profile.heapDeltaBytes || 0);
|
|
600
|
+
const rssDeltas = groupProfiles.map((profile) => profile.rssDeltaBytes || 0);
|
|
601
|
+
const renderDurations = groupProfiles.map((profile) => profile.renderDurationMs || 0);
|
|
602
|
+
const sqlDurations = groupProfiles.map((profile) => profile.sqlDurationMs);
|
|
603
|
+
const callDurations = groupProfiles.map((profile) => profile.callDurationMs);
|
|
604
|
+
const selfDurations = groupProfiles.map((profile) => profile.selfDurationMs || 0);
|
|
605
|
+
const ssrPayloads = groupProfiles.map((profile) => profile.ssrPayloadBytes || 0);
|
|
606
|
+
const latestProfile = [...groupProfiles].sort(
|
|
607
|
+
(left, right) => (readDateMs(right.startedAt) || 0) - (readDateMs(left.startedAt) || 0),
|
|
608
|
+
)[0];
|
|
609
|
+
const slowestProfile = [...groupProfiles].sort((left, right) => (right.totalDurationMs || 0) - (left.totalDurationMs || 0))[0];
|
|
610
|
+
|
|
611
|
+
return {
|
|
612
|
+
avgCallDurationMs: average(callDurations),
|
|
613
|
+
avgCpuMs: average(cpu),
|
|
614
|
+
avgDurationMs: average(durations),
|
|
615
|
+
avgHeapDeltaBytes: average(heapDeltas),
|
|
616
|
+
avgRenderDurationMs: average(renderDurations),
|
|
617
|
+
avgRssDeltaBytes: average(rssDeltas),
|
|
618
|
+
avgSelfDurationMs: average(selfDurations),
|
|
619
|
+
avgSqlDurationMs: average(sqlDurations),
|
|
620
|
+
avgSsrPayloadBytes: average(ssrPayloads),
|
|
621
|
+
errorCount: groupProfiles.filter(
|
|
622
|
+
(profile) => profile.errorMessage || (profile.statusCode !== undefined && profile.statusCode >= 400),
|
|
623
|
+
).length,
|
|
624
|
+
groupBy: selectedGroupBy,
|
|
625
|
+
key,
|
|
626
|
+
label: key,
|
|
627
|
+
latestRequestId: latestProfile?.requestId,
|
|
628
|
+
maxDurationMs: durations.reduce((value, durationMs) => Math.max(value, durationMs), 0),
|
|
629
|
+
maxHeapDeltaBytes: heapDeltas.reduce((value, durationMs) => Math.max(value, durationMs), 0),
|
|
630
|
+
maxRssDeltaBytes: rssDeltas.reduce((value, durationMs) => Math.max(value, durationMs), 0),
|
|
631
|
+
maxSsrPayloadBytes: ssrPayloads.reduce((value, durationMs) => Math.max(value, durationMs), 0),
|
|
632
|
+
p95DurationMs: percentile(durations, 0.95),
|
|
633
|
+
requestCount: groupProfiles.length,
|
|
634
|
+
slowestRequestId: slowestProfile?.requestId,
|
|
635
|
+
totalCallDurationMs: callDurations.reduce((count, durationMs) => count + durationMs, 0),
|
|
636
|
+
totalCpuMs: cpu.reduce((count, durationMs) => count + durationMs, 0),
|
|
637
|
+
totalDurationMs: durations.reduce((count, durationMs) => count + durationMs, 0),
|
|
638
|
+
totalRenderDurationMs: renderDurations.reduce((count, durationMs) => count + durationMs, 0),
|
|
639
|
+
totalSqlDurationMs: sqlDurations.reduce((count, durationMs) => count + durationMs, 0),
|
|
640
|
+
} satisfies TPerfTopRow;
|
|
641
|
+
})
|
|
642
|
+
.sort(
|
|
643
|
+
(left, right) =>
|
|
644
|
+
right.totalDurationMs - left.totalDurationMs ||
|
|
645
|
+
right.p95DurationMs - left.p95DurationMs ||
|
|
646
|
+
right.totalCpuMs - left.totalCpuMs ||
|
|
647
|
+
left.label.localeCompare(right.label),
|
|
648
|
+
)
|
|
649
|
+
.slice(0, Math.max(1, limit));
|
|
650
|
+
|
|
651
|
+
return {
|
|
652
|
+
groupBy: selectedGroupBy,
|
|
653
|
+
limit: Math.max(1, limit),
|
|
654
|
+
rows,
|
|
655
|
+
summary: summarizeProfiles(profiles),
|
|
656
|
+
window,
|
|
657
|
+
};
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
export const buildPerfCompareResponse = ({
|
|
661
|
+
baseline = 'yesterday',
|
|
662
|
+
groupBy = 'path',
|
|
663
|
+
limit = 12,
|
|
664
|
+
requests,
|
|
665
|
+
target = 'today',
|
|
666
|
+
}: {
|
|
667
|
+
baseline?: string;
|
|
668
|
+
groupBy?: TPerfGroupBy;
|
|
669
|
+
limit?: number;
|
|
670
|
+
requests: TRequestTrace[];
|
|
671
|
+
target?: string;
|
|
672
|
+
}): TPerfCompareResponse => {
|
|
673
|
+
const selectedGroupBy = perfGroupByValues.includes(groupBy) ? groupBy : 'path';
|
|
674
|
+
const baselineTop = buildPerfTopResponse({ groupBy: selectedGroupBy, limit: Number.MAX_SAFE_INTEGER, requests, since: baseline });
|
|
675
|
+
const targetTop = buildPerfTopResponse({ groupBy: selectedGroupBy, limit: Number.MAX_SAFE_INTEGER, requests, since: target });
|
|
676
|
+
const keys = [...new Set([...baselineTop.rows.map((row) => row.key), ...targetTop.rows.map((row) => row.key)])];
|
|
677
|
+
const baselineByKey = new Map(baselineTop.rows.map((row) => [row.key, row]));
|
|
678
|
+
const targetByKey = new Map(targetTop.rows.map((row) => [row.key, row]));
|
|
679
|
+
|
|
680
|
+
const rows = keys
|
|
681
|
+
.map((key) => {
|
|
682
|
+
const baselineRow = baselineByKey.get(key);
|
|
683
|
+
const targetRow = targetByKey.get(key);
|
|
684
|
+
const row = {
|
|
685
|
+
avgCpuMs: buildMetricDelta(baselineRow?.avgCpuMs || 0, targetRow?.avgCpuMs || 0),
|
|
686
|
+
avgDurationMs: buildMetricDelta(baselineRow?.avgDurationMs || 0, targetRow?.avgDurationMs || 0),
|
|
687
|
+
avgHeapDeltaBytes: buildMetricDelta(baselineRow?.avgHeapDeltaBytes || 0, targetRow?.avgHeapDeltaBytes || 0),
|
|
688
|
+
avgRenderDurationMs: buildMetricDelta(
|
|
689
|
+
baselineRow?.avgRenderDurationMs || 0,
|
|
690
|
+
targetRow?.avgRenderDurationMs || 0,
|
|
691
|
+
),
|
|
692
|
+
avgSqlDurationMs: buildMetricDelta(baselineRow?.avgSqlDurationMs || 0, targetRow?.avgSqlDurationMs || 0),
|
|
693
|
+
groupBy: selectedGroupBy,
|
|
694
|
+
key,
|
|
695
|
+
label: targetRow?.label || baselineRow?.label || key,
|
|
696
|
+
p95DurationMs: buildMetricDelta(baselineRow?.p95DurationMs || 0, targetRow?.p95DurationMs || 0),
|
|
697
|
+
requestCount: buildMetricDelta(baselineRow?.requestCount || 0, targetRow?.requestCount || 0),
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const deltaSignals = [
|
|
701
|
+
row.p95DurationMs.deltaPercent,
|
|
702
|
+
row.avgDurationMs.deltaPercent,
|
|
703
|
+
row.avgCpuMs.deltaPercent,
|
|
704
|
+
row.avgHeapDeltaBytes.deltaPercent,
|
|
705
|
+
]
|
|
706
|
+
.filter((value): value is number => value !== undefined)
|
|
707
|
+
.map((value) => Math.abs(value));
|
|
708
|
+
|
|
709
|
+
return {
|
|
710
|
+
...row,
|
|
711
|
+
change: deriveCompareChange(row),
|
|
712
|
+
score: deltaSignals.length > 0 ? Math.max(...deltaSignals) : Math.abs(row.avgDurationMs.delta),
|
|
713
|
+
} satisfies TPerfCompareRow;
|
|
714
|
+
})
|
|
715
|
+
.filter((row) => row.requestCount.baseline > 0 || row.requestCount.target > 0)
|
|
716
|
+
.sort((left, right) => right.score - left.score || left.label.localeCompare(right.label))
|
|
717
|
+
.slice(0, Math.max(1, limit));
|
|
718
|
+
|
|
719
|
+
return {
|
|
720
|
+
baseline: baselineTop.window,
|
|
721
|
+
groupBy: selectedGroupBy,
|
|
722
|
+
limit: Math.max(1, limit),
|
|
723
|
+
rows,
|
|
724
|
+
target: targetTop.window,
|
|
725
|
+
};
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
export const buildPerfMemoryResponse = ({
|
|
729
|
+
groupBy = 'path',
|
|
730
|
+
limit = 12,
|
|
731
|
+
requests,
|
|
732
|
+
since = 'today',
|
|
733
|
+
}: {
|
|
734
|
+
groupBy?: TPerfGroupBy;
|
|
735
|
+
limit?: number;
|
|
736
|
+
requests: TRequestTrace[];
|
|
737
|
+
since?: string;
|
|
738
|
+
}): TPerfMemoryResponse => {
|
|
739
|
+
const selectedGroupBy = perfGroupByValues.includes(groupBy) ? groupBy : 'path';
|
|
740
|
+
const { requests: filteredRequests, window } = selectRequestsInWindow(requests, since);
|
|
741
|
+
const profiles = filteredRequests.map(buildRequestPerformance);
|
|
742
|
+
const groups = new Map<string, TRequestPerformance[]>();
|
|
743
|
+
|
|
744
|
+
for (const profile of profiles) {
|
|
745
|
+
const key = readGroupValue(selectedGroupBy, profile) || profile.path || 'request';
|
|
746
|
+
const existing = groups.get(key);
|
|
747
|
+
if (existing) existing.push(profile);
|
|
748
|
+
else groups.set(key, [profile]);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const rows = [...groups.entries()]
|
|
752
|
+
.map(([key, groupProfiles]) => {
|
|
753
|
+
const heapDeltas = groupProfiles.map((profile) => profile.heapDeltaBytes || 0);
|
|
754
|
+
const rssDeltas = groupProfiles.map((profile) => profile.rssDeltaBytes || 0);
|
|
755
|
+
const positiveHeapDriftCount = heapDeltas.filter((value) => value > 0).length;
|
|
756
|
+
const positiveRssDriftCount = rssDeltas.filter((value) => value > 0).length;
|
|
757
|
+
const positiveHeapDriftRatio = groupProfiles.length > 0 ? positiveHeapDriftCount / groupProfiles.length : 0;
|
|
758
|
+
const positiveRssDriftRatio = groupProfiles.length > 0 ? positiveRssDriftCount / groupProfiles.length : 0;
|
|
759
|
+
|
|
760
|
+
let trend: TPerfMemoryTrend = 'stable';
|
|
761
|
+
if (positiveHeapDriftRatio >= 0.7 || positiveRssDriftRatio >= 0.7) trend = 'rising';
|
|
762
|
+
else if (positiveHeapDriftRatio >= 0.35 || positiveRssDriftRatio >= 0.35) trend = 'mixed';
|
|
763
|
+
|
|
764
|
+
return {
|
|
765
|
+
avgHeapDeltaBytes: average(heapDeltas),
|
|
766
|
+
avgRssDeltaBytes: average(rssDeltas),
|
|
767
|
+
groupBy: selectedGroupBy,
|
|
768
|
+
key,
|
|
769
|
+
label: key,
|
|
770
|
+
maxHeapDeltaBytes: heapDeltas.reduce((value, delta) => Math.max(value, delta), 0),
|
|
771
|
+
maxRssDeltaBytes: rssDeltas.reduce((value, delta) => Math.max(value, delta), 0),
|
|
772
|
+
positiveHeapDriftCount,
|
|
773
|
+
positiveHeapDriftRatio,
|
|
774
|
+
positiveRssDriftCount,
|
|
775
|
+
positiveRssDriftRatio,
|
|
776
|
+
requestCount: groupProfiles.length,
|
|
777
|
+
trend,
|
|
778
|
+
} satisfies TPerfMemoryRow;
|
|
779
|
+
})
|
|
780
|
+
.sort(
|
|
781
|
+
(left, right) =>
|
|
782
|
+
right.avgHeapDeltaBytes - left.avgHeapDeltaBytes ||
|
|
783
|
+
right.maxHeapDeltaBytes - left.maxHeapDeltaBytes ||
|
|
784
|
+
left.label.localeCompare(right.label),
|
|
785
|
+
)
|
|
786
|
+
.slice(0, Math.max(1, limit));
|
|
787
|
+
|
|
788
|
+
return {
|
|
789
|
+
groupBy: selectedGroupBy,
|
|
790
|
+
limit: Math.max(1, limit),
|
|
791
|
+
rows,
|
|
792
|
+
window,
|
|
793
|
+
};
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
export const resolvePerfRequest = (requests: TRequestTrace[], requestOrPath: string) => {
|
|
797
|
+
const normalized = requestOrPath.trim();
|
|
798
|
+
if (!normalized) throw new Error('Perf request id or path is required.');
|
|
799
|
+
|
|
800
|
+
const request =
|
|
801
|
+
requests.find((candidate) => candidate.id === normalized) ||
|
|
802
|
+
[...requests].reverse().find((candidate) => candidate.path === normalized);
|
|
803
|
+
|
|
804
|
+
if (!request) {
|
|
805
|
+
throw new Error(`Could not find a traced request for "${requestOrPath}".`);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
return buildRequestPerformance(request);
|
|
809
|
+
};
|