proteum 2.1.3-1 → 2.1.7
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 +109 -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/connect.ts +45 -0
- package/cli/commands/dev.ts +37 -13
- 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/trace.ts +9 -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 +95 -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 +110 -7
- package/cli/presentation/devSession.ts +32 -7
- package/cli/runtime/commands.ts +165 -6
- package/cli/scaffold/index.ts +18 -27
- package/cli/scaffold/templates.ts +48 -28
- package/cli/utils/agents.ts +106 -13
- package/cli/utils/keyboard.ts +8 -0
- package/client/dev/profiler/ApexChart.tsx +66 -0
- package/client/dev/profiler/index.tsx +2508 -302
- 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 +52 -1
- 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 +10 -2
- package/server/app/container/trace/index.ts +105 -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 +305 -11
- package/server/services/router/index.ts +116 -57
- 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 +23 -1
- package/server/services/router/response/page/document.tsx +31 -14
- 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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import type { ApexOptions } from 'apexcharts';
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
5
|
buildDoctorBlocks,
|
|
@@ -9,15 +10,23 @@ import {
|
|
|
9
10
|
type THumanTextBlock,
|
|
10
11
|
} from '@common/dev/diagnostics';
|
|
11
12
|
import type { TDevCommandDefinition, TDevCommandExecution } from '@common/dev/commands';
|
|
13
|
+
import { summarizeTraceForDiagnose, type TExplainOwnerMatch } from '@common/dev/inspection';
|
|
14
|
+
import {
|
|
15
|
+
buildRequestPerformance,
|
|
16
|
+
perfGroupByValues,
|
|
17
|
+
perfWindowPresets,
|
|
18
|
+
type TRequestPerformance,
|
|
19
|
+
} from '@common/dev/performance';
|
|
12
20
|
import type {
|
|
13
21
|
TProfilerCronTask,
|
|
14
22
|
TProfilerNavigationSession,
|
|
15
23
|
TProfilerPanel,
|
|
16
24
|
TProfilerSessionTrace,
|
|
17
25
|
} from '@common/dev/profiler';
|
|
18
|
-
import type { TRequestTrace, TTraceCall, TTraceEventType, TTraceSummaryValue } from '@common/dev/requestTrace';
|
|
26
|
+
import type { TRequestTrace, TTraceCall, TTraceEventType, TTraceSqlQuery, TTraceSummaryValue } from '@common/dev/requestTrace';
|
|
19
27
|
|
|
20
28
|
import { profilerRuntime } from './runtime';
|
|
29
|
+
import ApexChart from './ApexChart';
|
|
21
30
|
|
|
22
31
|
const profilerStyles = `
|
|
23
32
|
.proteum-profiler {
|
|
@@ -257,6 +266,10 @@ const profilerStyles = `
|
|
|
257
266
|
background: transparent;
|
|
258
267
|
}
|
|
259
268
|
|
|
269
|
+
.proteum-profiler__panelBody--split {
|
|
270
|
+
overflow: hidden;
|
|
271
|
+
}
|
|
272
|
+
|
|
260
273
|
.proteum-profiler__metrics {
|
|
261
274
|
display: grid;
|
|
262
275
|
gap: 0;
|
|
@@ -508,6 +521,7 @@ const profilerStyles = `
|
|
|
508
521
|
align-items: stretch;
|
|
509
522
|
min-height: 100%;
|
|
510
523
|
height: 100%;
|
|
524
|
+
max-height: 100%;
|
|
511
525
|
}
|
|
512
526
|
|
|
513
527
|
.proteum-profiler__splitView {
|
|
@@ -517,6 +531,7 @@ const profilerStyles = `
|
|
|
517
531
|
align-items: stretch;
|
|
518
532
|
min-height: 100%;
|
|
519
533
|
height: 100%;
|
|
534
|
+
max-height: 100%;
|
|
520
535
|
}
|
|
521
536
|
|
|
522
537
|
.proteum-profiler__splitView--stacked {
|
|
@@ -528,7 +543,12 @@ const profilerStyles = `
|
|
|
528
543
|
display: grid;
|
|
529
544
|
gap: 0;
|
|
530
545
|
min-width: 0;
|
|
546
|
+
min-height: 0;
|
|
547
|
+
height: 100%;
|
|
531
548
|
align-content: start;
|
|
549
|
+
overflow: auto;
|
|
550
|
+
overscroll-behavior: contain;
|
|
551
|
+
scrollbar-width: thin;
|
|
532
552
|
}
|
|
533
553
|
|
|
534
554
|
.proteum-profiler__requestGroups {
|
|
@@ -559,8 +579,6 @@ const profilerStyles = `
|
|
|
559
579
|
}
|
|
560
580
|
|
|
561
581
|
.proteum-profiler__sidebar {
|
|
562
|
-
position: sticky;
|
|
563
|
-
top: 0;
|
|
564
582
|
display: flex;
|
|
565
583
|
align-self: stretch;
|
|
566
584
|
height: 100%;
|
|
@@ -571,6 +589,7 @@ const profilerStyles = `
|
|
|
571
589
|
border-radius: 0;
|
|
572
590
|
background: transparent;
|
|
573
591
|
box-shadow: none;
|
|
592
|
+
overflow: hidden;
|
|
574
593
|
}
|
|
575
594
|
|
|
576
595
|
.proteum-profiler__sidebarScroller {
|
|
@@ -660,6 +679,44 @@ const profilerStyles = `
|
|
|
660
679
|
background: transparent !important;
|
|
661
680
|
}
|
|
662
681
|
|
|
682
|
+
.proteum-profiler__chartGrid {
|
|
683
|
+
display: grid;
|
|
684
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
685
|
+
gap: 0;
|
|
686
|
+
border-top: 1px solid var(--profiler-line);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
.proteum-profiler__chartCard {
|
|
690
|
+
display: grid;
|
|
691
|
+
align-content: start;
|
|
692
|
+
min-width: 0;
|
|
693
|
+
border-top: 1px solid var(--profiler-line);
|
|
694
|
+
border-left: 1px solid var(--profiler-line);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.proteum-profiler__chartCard:nth-child(2n + 1) {
|
|
698
|
+
border-left: none;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
.proteum-profiler__chartHeader {
|
|
702
|
+
display: grid;
|
|
703
|
+
gap: 4px;
|
|
704
|
+
padding: 8px 10px;
|
|
705
|
+
background: var(--profiler-title-row-bg);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
.proteum-profiler__chartSubtitle {
|
|
709
|
+
color: var(--profiler-muted);
|
|
710
|
+
font-size: 11px;
|
|
711
|
+
line-height: 1.5;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
.proteum-profiler__chartMount {
|
|
715
|
+
min-width: 0;
|
|
716
|
+
padding: 8px 8px 2px;
|
|
717
|
+
background: transparent;
|
|
718
|
+
}
|
|
719
|
+
|
|
663
720
|
.proteum-profiler__traceEventRow {
|
|
664
721
|
--profiler-trace-depth: 0;
|
|
665
722
|
--profiler-trace-guide-opacity: 0;
|
|
@@ -710,6 +767,18 @@ const profilerStyles = `
|
|
|
710
767
|
gap: 10px;
|
|
711
768
|
}
|
|
712
769
|
|
|
770
|
+
.proteum-profiler__panelBody--split {
|
|
771
|
+
overflow: auto;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
.proteum-profiler__chartGrid {
|
|
775
|
+
grid-template-columns: 1fr;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
.proteum-profiler__chartCard {
|
|
779
|
+
border-left: none;
|
|
780
|
+
}
|
|
781
|
+
|
|
713
782
|
.proteum-profiler__metricRow {
|
|
714
783
|
grid-template-columns: minmax(90px, 110px) 1fr;
|
|
715
784
|
}
|
|
@@ -722,12 +791,19 @@ const profilerStyles = `
|
|
|
722
791
|
grid-template-columns: 1fr;
|
|
723
792
|
min-height: 0;
|
|
724
793
|
height: auto;
|
|
794
|
+
max-height: none;
|
|
725
795
|
}
|
|
726
796
|
|
|
727
797
|
.proteum-profiler__splitView {
|
|
728
798
|
grid-template-columns: 1fr;
|
|
729
799
|
min-height: 0;
|
|
730
800
|
height: auto;
|
|
801
|
+
max-height: none;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
.proteum-profiler__splitColumn {
|
|
805
|
+
height: auto;
|
|
806
|
+
overflow: visible;
|
|
731
807
|
}
|
|
732
808
|
|
|
733
809
|
.proteum-profiler__sidebar {
|
|
@@ -754,6 +830,7 @@ type TSessionSummary = {
|
|
|
754
830
|
primaryTrace?: TProfilerSessionTrace;
|
|
755
831
|
renderMs?: number;
|
|
756
832
|
routeLabel: string;
|
|
833
|
+
sqlCount: number;
|
|
757
834
|
ssrPayloadBytes?: number;
|
|
758
835
|
statusLabel: string;
|
|
759
836
|
totalMs?: number;
|
|
@@ -766,6 +843,7 @@ type TApiRequestItem = {
|
|
|
766
843
|
finishedAt?: string;
|
|
767
844
|
label?: string;
|
|
768
845
|
method: string;
|
|
846
|
+
originLabel?: string;
|
|
769
847
|
path: string;
|
|
770
848
|
requestData?: TTraceSummaryValue;
|
|
771
849
|
requestDataJson?: unknown;
|
|
@@ -776,6 +854,26 @@ type TApiRequestItem = {
|
|
|
776
854
|
statusLabel?: string;
|
|
777
855
|
tags: string[];
|
|
778
856
|
};
|
|
857
|
+
type TSqlQueryItem = {
|
|
858
|
+
callerLabel: string;
|
|
859
|
+
durationMs: number;
|
|
860
|
+
finishedAt: string;
|
|
861
|
+
id: string;
|
|
862
|
+
kind: TTraceSqlQuery['kind'];
|
|
863
|
+
model?: string;
|
|
864
|
+
operation: string;
|
|
865
|
+
paramsJson?: unknown;
|
|
866
|
+
paramsText?: string;
|
|
867
|
+
query: string;
|
|
868
|
+
startedAt: string;
|
|
869
|
+
tags: string[];
|
|
870
|
+
target?: string;
|
|
871
|
+
};
|
|
872
|
+
type TSqlQueryGroup = {
|
|
873
|
+
id: string;
|
|
874
|
+
items: TSqlQueryItem[];
|
|
875
|
+
label: string;
|
|
876
|
+
};
|
|
779
877
|
type TWaterfallChartItem = {
|
|
780
878
|
barLabel: string;
|
|
781
879
|
color: string;
|
|
@@ -791,11 +889,14 @@ type TProfilerState = ReturnType<typeof profilerRuntime.getState>;
|
|
|
791
889
|
const panelLabels: Record<TProfilerPanel, string> = {
|
|
792
890
|
summary: 'Summary',
|
|
793
891
|
timeline: 'Timeline',
|
|
892
|
+
perf: 'Perf',
|
|
794
893
|
routing: 'Routing',
|
|
795
894
|
auth: 'Auth',
|
|
796
895
|
controller: 'Controller',
|
|
797
896
|
ssr: 'SSR',
|
|
798
897
|
api: 'API',
|
|
898
|
+
sql: 'SQL',
|
|
899
|
+
diagnose: 'Diagnose',
|
|
799
900
|
errors: 'Errors',
|
|
800
901
|
explain: 'Explain',
|
|
801
902
|
doctor: 'Doctor',
|
|
@@ -814,11 +915,24 @@ const readNumber = (value: TTraceSummaryValue | undefined) => (typeof value ===
|
|
|
814
915
|
const readString = (value: TTraceSummaryValue | undefined) => (typeof value === 'string' ? value : undefined);
|
|
815
916
|
const formatDuration = (value?: number) => (value === undefined ? 'pending' : `${Math.round(value)} ms`);
|
|
816
917
|
const formatBytes = (value?: number) => (value === undefined ? 'n/a' : `${(value / 1024).toFixed(value >= 1024 ? 1 : 2)} KB`);
|
|
918
|
+
const formatSignedBytes = (value?: number) =>
|
|
919
|
+
value === undefined ? 'n/a' : `${value >= 0 ? '+' : '-'}${formatBytes(Math.abs(value))}`;
|
|
920
|
+
const formatSignedPercent = (value?: number) => (value === undefined ? 'n/a' : `${value >= 0 ? '+' : ''}${value.toFixed(0)}%`);
|
|
817
921
|
const formatTimestamp = (value?: string) => {
|
|
818
922
|
if (!value) return 'never';
|
|
819
923
|
const date = new Date(value);
|
|
820
924
|
return Number.isNaN(date.valueOf()) ? value : date.toLocaleString();
|
|
821
925
|
};
|
|
926
|
+
const formatTimeLabel = (value?: string) => {
|
|
927
|
+
if (!value) return 'n/a';
|
|
928
|
+
const date = new Date(value);
|
|
929
|
+
return Number.isNaN(date.valueOf())
|
|
930
|
+
? value
|
|
931
|
+
: date.toLocaleTimeString([], {
|
|
932
|
+
hour: '2-digit',
|
|
933
|
+
minute: '2-digit',
|
|
934
|
+
});
|
|
935
|
+
};
|
|
822
936
|
const formatCronFrequency = (task: TProfilerCronTask) =>
|
|
823
937
|
task.frequency.kind === 'cron' ? task.frequency.value : `once at ${formatTimestamp(task.frequency.value)}`;
|
|
824
938
|
const formatStructuredValue = (value: unknown) => {
|
|
@@ -828,6 +942,29 @@ const formatStructuredValue = (value: unknown) => {
|
|
|
828
942
|
return String(value);
|
|
829
943
|
}
|
|
830
944
|
};
|
|
945
|
+
const formatOwnerSource = (match: TExplainOwnerMatch) =>
|
|
946
|
+
`${match.source.filepath}${formatManifestLocation(match.source.line, match.source.column)}`;
|
|
947
|
+
const toRoundedNumber = (value?: number, precision = 1) => {
|
|
948
|
+
if (value === undefined || Number.isNaN(value)) return 0;
|
|
949
|
+
return Number(value.toFixed(precision));
|
|
950
|
+
};
|
|
951
|
+
const toKilobytes = (value?: number, precision = 1) => toRoundedNumber((value || 0) / 1024, precision);
|
|
952
|
+
const buildChartHeight = (rowCount: number, options?: { max?: number; min?: number; rowHeight?: number }) =>
|
|
953
|
+
Math.max(options?.min || 240, Math.min(options?.max || 460, 112 + rowCount * (options?.rowHeight || 34)));
|
|
954
|
+
|
|
955
|
+
const profilerChartTheme = {
|
|
956
|
+
amber: '#f59e0b',
|
|
957
|
+
blue: '#175fe6',
|
|
958
|
+
cyan: '#0ea5e9',
|
|
959
|
+
green: '#15803d',
|
|
960
|
+
indigo: '#6366f1',
|
|
961
|
+
line: 'rgba(19, 32, 51, 0.1)',
|
|
962
|
+
muted: '#627186',
|
|
963
|
+
orange: '#ea580c',
|
|
964
|
+
red: '#b91c1c',
|
|
965
|
+
teal: '#0f766e',
|
|
966
|
+
text: '#132033',
|
|
967
|
+
};
|
|
831
968
|
|
|
832
969
|
const renderSummaryValue = (value: TTraceSummaryValue | undefined): string => {
|
|
833
970
|
if (value === undefined) return '';
|
|
@@ -985,6 +1122,11 @@ const formatProfilerRequestReference = ({
|
|
|
985
1122
|
return rawReference || fallbackLabel || 'request';
|
|
986
1123
|
};
|
|
987
1124
|
|
|
1125
|
+
const formatConnectedTraceCallReference = (call: TTraceCall) =>
|
|
1126
|
+
call.connectedProjectNamespace && call.connectedControllerAccessor
|
|
1127
|
+
? `${call.connectedProjectNamespace}.${call.connectedControllerAccessor}`
|
|
1128
|
+
: undefined;
|
|
1129
|
+
|
|
988
1130
|
const getTraceRequestData = (trace: TRequestTrace | undefined) =>
|
|
989
1131
|
trace?.events.find((event) => event.type === 'request.start')?.details.data;
|
|
990
1132
|
|
|
@@ -1060,6 +1202,7 @@ const getSummary = (session: TProfilerNavigationSession): TSessionSummary => {
|
|
|
1060
1202
|
session.steps.filter((step) => step.status === 'error').length +
|
|
1061
1203
|
session.traces.filter((traceItem) => traceItem.status === 'error').length +
|
|
1062
1204
|
syncCalls.filter((call) => call.errorMessage || (call.statusCode !== undefined && call.statusCode >= 400)).length;
|
|
1205
|
+
const sqlCount = session.traces.reduce((count, traceItem) => count + (traceItem.trace?.sqlQueries.length || 0), 0);
|
|
1063
1206
|
const renderStart = trace?.events.find((event) => event.type === 'render.start');
|
|
1064
1207
|
const renderEnd = trace?.events.find((event) => event.type === 'render.end');
|
|
1065
1208
|
const localRender = [...session.steps].reverse().find((step) => step.label === 'Render' && step.durationMs !== undefined);
|
|
@@ -1076,6 +1219,7 @@ const getSummary = (session: TProfilerNavigationSession): TSessionSummary => {
|
|
|
1076
1219
|
? Math.max(0, renderEnd.elapsedMs - renderStart.elapsedMs)
|
|
1077
1220
|
: localRender?.durationMs,
|
|
1078
1221
|
routeLabel,
|
|
1222
|
+
sqlCount,
|
|
1079
1223
|
ssrPayloadBytes: readNumber(ssrPayload?.details.serializedBytes),
|
|
1080
1224
|
statusLabel: session.kind === 'client-navigation' ? 'NAV' : trace ? `${trace.statusCode || 'pending'} ${trace.method}` : 'SSR',
|
|
1081
1225
|
totalMs: session.kind === 'client-navigation' ? session.durationMs : trace?.durationMs ?? session.durationMs,
|
|
@@ -1099,7 +1243,12 @@ const JsonCodeBlock = ({ value }: { value: string }) => (
|
|
|
1099
1243
|
<pre className="proteum-profiler__mono proteum-profiler__pre">{renderHighlightedJson(value)}</pre>
|
|
1100
1244
|
);
|
|
1101
1245
|
|
|
1246
|
+
const PlainCodeBlock = ({ value }: { value: string }) => <pre className="proteum-profiler__mono proteum-profiler__pre">{value}</pre>;
|
|
1247
|
+
|
|
1102
1248
|
const formatTraceCallDisplay = (call: TTraceCall) => {
|
|
1249
|
+
const connectedReference = formatConnectedTraceCallReference(call);
|
|
1250
|
+
if (connectedReference) return connectedReference;
|
|
1251
|
+
|
|
1103
1252
|
if (call.path.startsWith('/api/')) {
|
|
1104
1253
|
return formatProfilerRequestReference({
|
|
1105
1254
|
fallbackLabel: call.label,
|
|
@@ -1127,6 +1276,83 @@ const formatSessionTraceDisplay = (traceItem: TProfilerSessionTrace) => {
|
|
|
1127
1276
|
return traceItem.label || formatProfilerRequestReference({ method: traceItem.method, path: traceItem.path });
|
|
1128
1277
|
};
|
|
1129
1278
|
|
|
1279
|
+
const formatSqlCallerReference = ({
|
|
1280
|
+
callerLabel,
|
|
1281
|
+
callerMethod,
|
|
1282
|
+
callerPath,
|
|
1283
|
+
}: Pick<TTraceSqlQuery, 'callerLabel' | 'callerMethod' | 'callerPath'>) =>
|
|
1284
|
+
formatProfilerRequestReference({
|
|
1285
|
+
fallbackLabel: callerLabel,
|
|
1286
|
+
method: callerMethod,
|
|
1287
|
+
path: callerPath,
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
const formatSqlQueryTitle = (query: string) => truncate(query.replace(/\s+/g, ' ').trim() || 'SQL query', 160);
|
|
1291
|
+
|
|
1292
|
+
const formatSqlParams = (item: TSqlQueryItem) =>
|
|
1293
|
+
item.paramsJson !== undefined ? formatStructuredValue(item.paramsJson) : item.paramsText || '[]';
|
|
1294
|
+
|
|
1295
|
+
const buildSqlQueryWorkspace = (session: TProfilerNavigationSession) => {
|
|
1296
|
+
const groups = new Map<string, TSqlQueryGroup>();
|
|
1297
|
+
const queryItems: TSqlQueryItem[] = [];
|
|
1298
|
+
const sortItems = (left: { id: string; startedAt: string }, right: { id: string; startedAt: string }) =>
|
|
1299
|
+
(readDateMs(left.startedAt) ?? 0) - (readDateMs(right.startedAt) ?? 0) || left.id.localeCompare(right.id);
|
|
1300
|
+
|
|
1301
|
+
for (const traceItem of session.traces) {
|
|
1302
|
+
const trace = traceItem.trace;
|
|
1303
|
+
if (!trace) continue;
|
|
1304
|
+
|
|
1305
|
+
for (const query of trace.sqlQueries || []) {
|
|
1306
|
+
const callerLabel = formatSqlCallerReference(query);
|
|
1307
|
+
const item: TSqlQueryItem = {
|
|
1308
|
+
callerLabel,
|
|
1309
|
+
durationMs: query.durationMs,
|
|
1310
|
+
finishedAt: query.finishedAt,
|
|
1311
|
+
id: query.id,
|
|
1312
|
+
kind: query.kind,
|
|
1313
|
+
model: query.model,
|
|
1314
|
+
operation: query.operation,
|
|
1315
|
+
paramsJson: query.paramsJson,
|
|
1316
|
+
paramsText: query.paramsText,
|
|
1317
|
+
query: query.query,
|
|
1318
|
+
startedAt: query.startedAt,
|
|
1319
|
+
tags: [
|
|
1320
|
+
query.kind,
|
|
1321
|
+
`op:${query.operation}`,
|
|
1322
|
+
...(query.model ? [`model:${query.model}`] : []),
|
|
1323
|
+
...(query.target ? [`target:${query.target}`] : []),
|
|
1324
|
+
...(query.callerOrigin !== 'request' ? [query.callerOrigin] : []),
|
|
1325
|
+
...(query.callerFetcherId ? [`fetcher:${query.callerFetcherId}`] : []),
|
|
1326
|
+
...(query.callerLabel &&
|
|
1327
|
+
query.callerLabel !== query.callerFetcherId &&
|
|
1328
|
+
query.callerLabel !== callerLabel
|
|
1329
|
+
? [`label:${query.callerLabel}`]
|
|
1330
|
+
: []),
|
|
1331
|
+
],
|
|
1332
|
+
target: query.target,
|
|
1333
|
+
};
|
|
1334
|
+
|
|
1335
|
+
queryItems.push(item);
|
|
1336
|
+
|
|
1337
|
+
const groupId = `${traceItem.id}:${query.callerCallId || 'request'}`;
|
|
1338
|
+
const group = groups.get(groupId);
|
|
1339
|
+
if (group) group.items.push(item);
|
|
1340
|
+
else groups.set(groupId, { id: groupId, items: [item], label: callerLabel });
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
return {
|
|
1345
|
+
groups: [...groups.values()]
|
|
1346
|
+
.map((group) => ({ ...group, items: [...group.items].sort(sortItems) }))
|
|
1347
|
+
.sort(
|
|
1348
|
+
(left, right) =>
|
|
1349
|
+
sortItems(left.items[0] || { id: left.id, startedAt: '' }, right.items[0] || { id: right.id, startedAt: '' }) ||
|
|
1350
|
+
left.label.localeCompare(right.label),
|
|
1351
|
+
),
|
|
1352
|
+
queryItems: [...queryItems].sort(sortItems),
|
|
1353
|
+
};
|
|
1354
|
+
};
|
|
1355
|
+
|
|
1130
1356
|
const ApiRequestListEntry = ({
|
|
1131
1357
|
isSelected,
|
|
1132
1358
|
item,
|
|
@@ -1245,6 +1471,106 @@ const ApiRequestSidebar = ({ item }: { item?: TApiRequestItem }) => {
|
|
|
1245
1471
|
);
|
|
1246
1472
|
};
|
|
1247
1473
|
|
|
1474
|
+
const SqlQueryListEntry = ({
|
|
1475
|
+
isSelected,
|
|
1476
|
+
item,
|
|
1477
|
+
onSelect,
|
|
1478
|
+
}: {
|
|
1479
|
+
isSelected: boolean;
|
|
1480
|
+
item: TSqlQueryItem;
|
|
1481
|
+
onSelect: () => void;
|
|
1482
|
+
}) => (
|
|
1483
|
+
<button
|
|
1484
|
+
aria-pressed={isSelected}
|
|
1485
|
+
className={`proteum-profiler__row proteum-profiler__row--interactive ${isSelected ? 'proteum-profiler__row--selected' : ''}`}
|
|
1486
|
+
onClick={onSelect}
|
|
1487
|
+
type="button"
|
|
1488
|
+
>
|
|
1489
|
+
<div className="proteum-profiler__rowHeader">
|
|
1490
|
+
<strong className="proteum-profiler__rowTitle">{formatSqlQueryTitle(item.query)}</strong>
|
|
1491
|
+
<span className="proteum-profiler__rowMeta">
|
|
1492
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">{formatDuration(item.durationMs)}</span>
|
|
1493
|
+
</span>
|
|
1494
|
+
</div>
|
|
1495
|
+
<div className="proteum-profiler__tags">
|
|
1496
|
+
{item.tags.map((tag) => (
|
|
1497
|
+
<span className="proteum-profiler__tag" key={`${item.id}:tag:${tag}`}>
|
|
1498
|
+
{tag}
|
|
1499
|
+
</span>
|
|
1500
|
+
))}
|
|
1501
|
+
</div>
|
|
1502
|
+
</button>
|
|
1503
|
+
);
|
|
1504
|
+
|
|
1505
|
+
const SqlQuerySidebar = ({ item }: { item?: TSqlQueryItem }) => {
|
|
1506
|
+
if (!item) {
|
|
1507
|
+
return (
|
|
1508
|
+
<aside className="proteum-profiler__sidebar">
|
|
1509
|
+
<div className="proteum-profiler__sidebarScroller">
|
|
1510
|
+
<div className="proteum-profiler__sidebarHeader">
|
|
1511
|
+
<div className="proteum-profiler__sidebarEyebrow">SQL details</div>
|
|
1512
|
+
<div className="proteum-profiler__sidebarEmpty">
|
|
1513
|
+
Select a query to inspect its caller, SQL text, bound params, and timing.
|
|
1514
|
+
</div>
|
|
1515
|
+
</div>
|
|
1516
|
+
</div>
|
|
1517
|
+
</aside>
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
return (
|
|
1522
|
+
<aside className="proteum-profiler__sidebar">
|
|
1523
|
+
<div className="proteum-profiler__sidebarScroller">
|
|
1524
|
+
<div className="proteum-profiler__sidebarHeader">
|
|
1525
|
+
<div className="proteum-profiler__sidebarEyebrow">SQL query</div>
|
|
1526
|
+
<div className="proteum-profiler__sidebarTitle">
|
|
1527
|
+
<strong>{formatSqlQueryTitle(item.query)}</strong>
|
|
1528
|
+
</div>
|
|
1529
|
+
<div className="proteum-profiler__mono proteum-profiler__muted">{item.callerLabel}</div>
|
|
1530
|
+
</div>
|
|
1531
|
+
|
|
1532
|
+
<div className="proteum-profiler__metrics">
|
|
1533
|
+
<SummaryRow label="Caller" value={item.callerLabel} />
|
|
1534
|
+
<SummaryRow label="Duration" value={formatDuration(item.durationMs)} />
|
|
1535
|
+
<SummaryRow label="Started" value={formatTimestamp(item.startedAt)} />
|
|
1536
|
+
<SummaryRow label="Finished" value={formatTimestamp(item.finishedAt)} />
|
|
1537
|
+
<SummaryRow label="Kind" value={item.kind} />
|
|
1538
|
+
<SummaryRow label="Operation" value={item.operation} />
|
|
1539
|
+
<SummaryRow label="Model" value={item.model || 'n/a'} />
|
|
1540
|
+
<SummaryRow label="Target" value={item.target || 'n/a'} />
|
|
1541
|
+
</div>
|
|
1542
|
+
|
|
1543
|
+
{item.tags.length > 0 ? (
|
|
1544
|
+
<div className="proteum-profiler__sidebarSection">
|
|
1545
|
+
<div className="proteum-profiler__sidebarSectionTitle">Tags</div>
|
|
1546
|
+
<div className="proteum-profiler__tags">
|
|
1547
|
+
{item.tags.map((tag) => (
|
|
1548
|
+
<span className="proteum-profiler__tag" key={`${item.id}:detail:${tag}`}>
|
|
1549
|
+
{tag}
|
|
1550
|
+
</span>
|
|
1551
|
+
))}
|
|
1552
|
+
</div>
|
|
1553
|
+
</div>
|
|
1554
|
+
) : null}
|
|
1555
|
+
|
|
1556
|
+
<div className="proteum-profiler__sidebarSection">
|
|
1557
|
+
<div className="proteum-profiler__sidebarSectionTitle">SQL</div>
|
|
1558
|
+
<PlainCodeBlock value={item.query} />
|
|
1559
|
+
</div>
|
|
1560
|
+
|
|
1561
|
+
<div className="proteum-profiler__sidebarSection">
|
|
1562
|
+
<div className="proteum-profiler__sidebarSectionTitle">Parameters</div>
|
|
1563
|
+
{item.paramsJson !== undefined ? (
|
|
1564
|
+
<JsonCodeBlock value={formatStructuredValue(item.paramsJson)} />
|
|
1565
|
+
) : (
|
|
1566
|
+
<PlainCodeBlock value={formatSqlParams(item)} />
|
|
1567
|
+
)}
|
|
1568
|
+
</div>
|
|
1569
|
+
</div>
|
|
1570
|
+
</aside>
|
|
1571
|
+
);
|
|
1572
|
+
};
|
|
1573
|
+
|
|
1248
1574
|
const ApiPanel = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
1249
1575
|
const syncItems: TApiRequestItem[] = session.traces
|
|
1250
1576
|
.flatMap((trace) => trace.trace?.calls.filter((call) => call.origin !== 'client-async') || [])
|
|
@@ -1256,6 +1582,7 @@ const ApiPanel = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
|
1256
1582
|
finishedAt: call.finishedAt,
|
|
1257
1583
|
label: call.label,
|
|
1258
1584
|
method: call.method,
|
|
1585
|
+
originLabel: call.origin,
|
|
1259
1586
|
path: call.path,
|
|
1260
1587
|
requestData: call.requestData,
|
|
1261
1588
|
requestDataJson: call.requestDataJson,
|
|
@@ -1265,6 +1592,8 @@ const ApiPanel = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
|
1265
1592
|
statusCode: call.statusCode,
|
|
1266
1593
|
tags: [
|
|
1267
1594
|
call.origin,
|
|
1595
|
+
...(call.connectedProjectNamespace ? [`connected:${call.connectedProjectNamespace}`] : []),
|
|
1596
|
+
...(call.connectedControllerAccessor ? [`target:${call.connectedControllerAccessor}`] : []),
|
|
1268
1597
|
...(call.fetcherId ? [`fetcher:${call.fetcherId}`] : []),
|
|
1269
1598
|
...call.requestDataKeys.map((key) => `arg:${key}`),
|
|
1270
1599
|
...call.resultKeys.map((key) => `res:${key}`),
|
|
@@ -1280,6 +1609,7 @@ const ApiPanel = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
|
1280
1609
|
finishedAt: trace.finishedAt,
|
|
1281
1610
|
label: trace.label,
|
|
1282
1611
|
method: trace.method,
|
|
1612
|
+
originLabel: 'client-async',
|
|
1283
1613
|
path: trace.path,
|
|
1284
1614
|
requestData: getTraceRequestData(trace.trace),
|
|
1285
1615
|
requestDataJson: trace.trace?.requestDataJson,
|
|
@@ -1292,6 +1622,33 @@ const ApiPanel = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
|
1292
1622
|
}));
|
|
1293
1623
|
const requestItems = [...syncItems, ...asyncItems];
|
|
1294
1624
|
const [selectedRequestId, setSelectedRequestId] = React.useState<string | undefined>(() => requestItems[0]?.id);
|
|
1625
|
+
const endpointDurationChart = buildHorizontalBarChartOptions({
|
|
1626
|
+
color: profilerChartTheme.blue,
|
|
1627
|
+
entries: buildDurationEntries(
|
|
1628
|
+
requestItems,
|
|
1629
|
+
(item) => formatApiReference(item.method, item.path, item.requestData, item.label),
|
|
1630
|
+
(item) => item.durationMs,
|
|
1631
|
+
),
|
|
1632
|
+
title: 'Endpoint workload',
|
|
1633
|
+
valueUnit: 'Milliseconds',
|
|
1634
|
+
});
|
|
1635
|
+
const originWorkloadChart = buildColumnChartOptions({
|
|
1636
|
+
colors: [profilerChartTheme.indigo],
|
|
1637
|
+
entries: buildDurationEntries(
|
|
1638
|
+
requestItems,
|
|
1639
|
+
(item) => getApiOriginLabel(item),
|
|
1640
|
+
(item) => item.durationMs,
|
|
1641
|
+
6,
|
|
1642
|
+
),
|
|
1643
|
+
title: 'Origin time share',
|
|
1644
|
+
valueUnit: 'Milliseconds',
|
|
1645
|
+
});
|
|
1646
|
+
const statusCountChart = buildColumnChartOptions({
|
|
1647
|
+
colors: [profilerChartTheme.amber],
|
|
1648
|
+
entries: buildCountEntries(requestItems.map((item) => getRequestStatusGroup(item.statusCode, item.statusLabel))),
|
|
1649
|
+
title: 'Status groups',
|
|
1650
|
+
valueUnit: 'Requests',
|
|
1651
|
+
});
|
|
1295
1652
|
|
|
1296
1653
|
React.useEffect(() => {
|
|
1297
1654
|
if (requestItems.some((item) => item.id === selectedRequestId)) return;
|
|
@@ -1304,6 +1661,27 @@ const ApiPanel = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
|
1304
1661
|
return (
|
|
1305
1662
|
<div className="proteum-profiler__requestWorkspace">
|
|
1306
1663
|
<div className="proteum-profiler__splitColumn">
|
|
1664
|
+
<div className="proteum-profiler__chartGrid">
|
|
1665
|
+
<ChartSection
|
|
1666
|
+
emptyLabel="No API request timings were captured for this session."
|
|
1667
|
+
options={endpointDurationChart}
|
|
1668
|
+
subtitle="Rank the most expensive endpoints across synchronous and async requests."
|
|
1669
|
+
title="Hot Endpoints"
|
|
1670
|
+
/>
|
|
1671
|
+
<ChartSection
|
|
1672
|
+
emptyLabel="No API origin timings were captured for this session."
|
|
1673
|
+
options={originWorkloadChart}
|
|
1674
|
+
subtitle="Compare time spent in SSR fetchers, batch fetchers, and client async calls."
|
|
1675
|
+
title="Origin Mix"
|
|
1676
|
+
/>
|
|
1677
|
+
<ChartSection
|
|
1678
|
+
emptyLabel="No API status information was captured for this session."
|
|
1679
|
+
options={statusCountChart}
|
|
1680
|
+
subtitle="Spot failures or pending requests without scanning every row."
|
|
1681
|
+
title="Status Spread"
|
|
1682
|
+
/>
|
|
1683
|
+
</div>
|
|
1684
|
+
|
|
1307
1685
|
<WaterfallChart
|
|
1308
1686
|
emptyLabel="No API requests were captured for this session."
|
|
1309
1687
|
itemLabel="request"
|
|
@@ -1367,6 +1745,113 @@ const ApiPanel = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
|
1367
1745
|
);
|
|
1368
1746
|
};
|
|
1369
1747
|
|
|
1748
|
+
const SqlPanel = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
1749
|
+
const { groups, queryItems } = buildSqlQueryWorkspace(session);
|
|
1750
|
+
const [selectedQueryId, setSelectedQueryId] = React.useState<string | undefined>(() => queryItems[0]?.id);
|
|
1751
|
+
const callerDurationChart = buildHorizontalBarChartOptions({
|
|
1752
|
+
color: profilerChartTheme.teal,
|
|
1753
|
+
entries: buildDurationEntries(queryItems, (item) => item.callerLabel, (item) => item.durationMs),
|
|
1754
|
+
title: 'Hot SQL callers',
|
|
1755
|
+
valueUnit: 'Milliseconds',
|
|
1756
|
+
});
|
|
1757
|
+
const operationCountChart = buildColumnChartOptions({
|
|
1758
|
+
colors: [profilerChartTheme.amber],
|
|
1759
|
+
entries: buildCountEntries(queryItems.map((item) => `${item.operation}${item.model ? `:${item.model}` : ''}`)),
|
|
1760
|
+
title: 'Operation volume',
|
|
1761
|
+
valueUnit: 'Queries',
|
|
1762
|
+
});
|
|
1763
|
+
const selectedCallers = buildDurationEntries(queryItems, (item) => item.callerLabel, (item) => item.durationMs, 6).map((entry) => entry.label);
|
|
1764
|
+
const selectedOperations = buildCountEntries(
|
|
1765
|
+
queryItems.map((item) => `${item.operation}${item.model ? `:${item.model}` : ''}`),
|
|
1766
|
+
6,
|
|
1767
|
+
).map((entry) => entry.label);
|
|
1768
|
+
const callerOperationHeatmap = buildHeatmapChartOptions({
|
|
1769
|
+
rows: selectedCallers.map((callerLabel) => ({
|
|
1770
|
+
data: selectedOperations.map((operationLabel) => ({
|
|
1771
|
+
x: truncate(operationLabel, 22),
|
|
1772
|
+
y: queryItems.filter(
|
|
1773
|
+
(item) => item.callerLabel === callerLabel && `${item.operation}${item.model ? `:${item.model}` : ''}` === operationLabel,
|
|
1774
|
+
).length,
|
|
1775
|
+
})),
|
|
1776
|
+
name: truncate(callerLabel, 28),
|
|
1777
|
+
})),
|
|
1778
|
+
title: 'Caller x operation density',
|
|
1779
|
+
valueUnit: 'Operation',
|
|
1780
|
+
});
|
|
1781
|
+
|
|
1782
|
+
React.useEffect(() => {
|
|
1783
|
+
if (queryItems.some((item) => item.id === selectedQueryId)) return;
|
|
1784
|
+
setSelectedQueryId(queryItems[0]?.id);
|
|
1785
|
+
}, [queryItems, selectedQueryId]);
|
|
1786
|
+
|
|
1787
|
+
const waterfallItems = buildSqlWaterfallItems(queryItems);
|
|
1788
|
+
const selectedItem = queryItems.find((item) => item.id === selectedQueryId) || queryItems[0];
|
|
1789
|
+
|
|
1790
|
+
return (
|
|
1791
|
+
<div className="proteum-profiler__requestWorkspace">
|
|
1792
|
+
<div className="proteum-profiler__splitColumn">
|
|
1793
|
+
<div className="proteum-profiler__chartGrid">
|
|
1794
|
+
<ChartSection
|
|
1795
|
+
emptyLabel="No SQL timings were captured for this session."
|
|
1796
|
+
options={callerDurationChart}
|
|
1797
|
+
subtitle="Show which callers are driving the most database time."
|
|
1798
|
+
title="Hot Callers"
|
|
1799
|
+
/>
|
|
1800
|
+
<ChartSection
|
|
1801
|
+
emptyLabel="No SQL operation counts were captured for this session."
|
|
1802
|
+
options={operationCountChart}
|
|
1803
|
+
subtitle="Highlight whether reads, writes, or raw queries dominate the request."
|
|
1804
|
+
title="Operation Mix"
|
|
1805
|
+
/>
|
|
1806
|
+
<ChartSection
|
|
1807
|
+
emptyLabel="No caller or operation overlap was captured for this session."
|
|
1808
|
+
options={callerOperationHeatmap}
|
|
1809
|
+
subtitle="Surface dense caller and operation combinations at a glance."
|
|
1810
|
+
title="Caller Heatmap"
|
|
1811
|
+
/>
|
|
1812
|
+
</div>
|
|
1813
|
+
|
|
1814
|
+
<WaterfallChart
|
|
1815
|
+
emptyLabel="No SQL queries were captured for this session."
|
|
1816
|
+
itemLabel="query"
|
|
1817
|
+
items={waterfallItems}
|
|
1818
|
+
onSelect={setSelectedQueryId}
|
|
1819
|
+
/>
|
|
1820
|
+
|
|
1821
|
+
<div className="proteum-profiler__requestGroups">
|
|
1822
|
+
{groups.length === 0 ? (
|
|
1823
|
+
<div className="proteum-profiler__empty">No SQL queries were captured for this session.</div>
|
|
1824
|
+
) : (
|
|
1825
|
+
groups.map((group) => (
|
|
1826
|
+
<div className="proteum-profiler__requestGroup" key={group.id}>
|
|
1827
|
+
<div className="proteum-profiler__requestGroupHeader">
|
|
1828
|
+
<div className="proteum-profiler__sectionTitle">{group.label}</div>
|
|
1829
|
+
<div className="proteum-profiler__requestGroupCount">
|
|
1830
|
+
{group.items.length} item{group.items.length === 1 ? '' : 's'}
|
|
1831
|
+
</div>
|
|
1832
|
+
</div>
|
|
1833
|
+
|
|
1834
|
+
<div className="proteum-profiler__list">
|
|
1835
|
+
{group.items.map((item) => (
|
|
1836
|
+
<SqlQueryListEntry
|
|
1837
|
+
isSelected={item.id === selectedItem?.id}
|
|
1838
|
+
item={item}
|
|
1839
|
+
key={item.id}
|
|
1840
|
+
onSelect={() => setSelectedQueryId(item.id)}
|
|
1841
|
+
/>
|
|
1842
|
+
))}
|
|
1843
|
+
</div>
|
|
1844
|
+
</div>
|
|
1845
|
+
))
|
|
1846
|
+
)}
|
|
1847
|
+
</div>
|
|
1848
|
+
</div>
|
|
1849
|
+
|
|
1850
|
+
<SqlQuerySidebar item={selectedItem} />
|
|
1851
|
+
</div>
|
|
1852
|
+
);
|
|
1853
|
+
};
|
|
1854
|
+
|
|
1370
1855
|
const getTraceEventKey = (traceId: string, event: TRequestTrace['events'][number]) => `${traceId}:${event.index}`;
|
|
1371
1856
|
|
|
1372
1857
|
const TraceEventSidebar = ({
|
|
@@ -1476,6 +1961,12 @@ const TraceRows = ({
|
|
|
1476
1961
|
</div>
|
|
1477
1962
|
<div className="proteum-profiler__tags">
|
|
1478
1963
|
<span className="proteum-profiler__tag">{call.origin}</span>
|
|
1964
|
+
{call.connectedProjectNamespace ? (
|
|
1965
|
+
<span className="proteum-profiler__tag">connected:{call.connectedProjectNamespace}</span>
|
|
1966
|
+
) : null}
|
|
1967
|
+
{call.connectedControllerAccessor ? (
|
|
1968
|
+
<span className="proteum-profiler__tag">target:{call.connectedControllerAccessor}</span>
|
|
1969
|
+
) : null}
|
|
1479
1970
|
{call.fetcherId ? <span className="proteum-profiler__tag">fetcher:{call.fetcherId}</span> : null}
|
|
1480
1971
|
{call.requestDataKeys.map((key) => (
|
|
1481
1972
|
<span className="proteum-profiler__tag" key={`${call.id}:req:${key}`}>
|
|
@@ -1750,6 +2241,79 @@ const buildApiWaterfallItems = (requestItems: TApiRequestItem[]): TWaterfallChar
|
|
|
1750
2241
|
});
|
|
1751
2242
|
};
|
|
1752
2243
|
|
|
2244
|
+
const buildSqlWaterfallItems = (queryItems: TSqlQueryItem[]): TWaterfallChartItem[] => {
|
|
2245
|
+
const rawItems = queryItems.map((item) => {
|
|
2246
|
+
const startMs = readDateMs(item.startedAt) ?? 0;
|
|
2247
|
+
const endMs = buildWaterfallEndMs({
|
|
2248
|
+
durationMs: item.durationMs,
|
|
2249
|
+
finishedAt: item.finishedAt,
|
|
2250
|
+
startMs,
|
|
2251
|
+
});
|
|
2252
|
+
|
|
2253
|
+
return {
|
|
2254
|
+
endMs,
|
|
2255
|
+
item,
|
|
2256
|
+
startMs,
|
|
2257
|
+
title: formatSqlQueryTitle(item.query),
|
|
2258
|
+
};
|
|
2259
|
+
});
|
|
2260
|
+
|
|
2261
|
+
const sortedItems = [...rawItems].sort((left, right) => left.startMs - right.startMs || left.item.id.localeCompare(right.item.id));
|
|
2262
|
+
const chartStartMs = sortedItems.length > 0 ? Math.min(...sortedItems.map((item) => item.startMs)) : 0;
|
|
2263
|
+
|
|
2264
|
+
return sortedItems.map(({ endMs, item, startMs, title }) => {
|
|
2265
|
+
const startOffsetMs = startMs - chartStartMs;
|
|
2266
|
+
const endOffsetMs = endMs - chartStartMs;
|
|
2267
|
+
|
|
2268
|
+
return {
|
|
2269
|
+
barLabel: truncate(title, 84),
|
|
2270
|
+
color: getTimelineDurationColor(item.durationMs),
|
|
2271
|
+
detailLines: [
|
|
2272
|
+
`Caller: ${item.callerLabel}`,
|
|
2273
|
+
`Operation: ${item.operation}${item.model ? ` (${item.model})` : ''}`,
|
|
2274
|
+
`Duration: ${formatDuration(item.durationMs)}`,
|
|
2275
|
+
`Start: +${Math.round(startOffsetMs)} ms`,
|
|
2276
|
+
`End: +${Math.round(endOffsetMs)} ms`,
|
|
2277
|
+
],
|
|
2278
|
+
endOffsetMs,
|
|
2279
|
+
id: item.id,
|
|
2280
|
+
startOffsetMs,
|
|
2281
|
+
subtitle: item.callerLabel,
|
|
2282
|
+
title,
|
|
2283
|
+
};
|
|
2284
|
+
});
|
|
2285
|
+
};
|
|
2286
|
+
|
|
2287
|
+
const getPerfStageColor = (stageId: TRequestPerformance['stages'][number]['id']) => {
|
|
2288
|
+
if (stageId === 'auth') return '#0ea5e9';
|
|
2289
|
+
if (stageId === 'routing') return '#3b82f6';
|
|
2290
|
+
if (stageId === 'controller') return '#6366f1';
|
|
2291
|
+
if (stageId === 'page-data') return '#14b8a6';
|
|
2292
|
+
if (stageId === 'render') return '#f59e0b';
|
|
2293
|
+
return '#22c55e';
|
|
2294
|
+
};
|
|
2295
|
+
|
|
2296
|
+
const buildPerfWaterfallItems = (request: TRequestPerformance): TWaterfallChartItem[] =>
|
|
2297
|
+
request.stages.map((stage) => ({
|
|
2298
|
+
barLabel: `${stage.label} ${formatDuration(stage.durationMs)}`,
|
|
2299
|
+
color: getPerfStageColor(stage.id),
|
|
2300
|
+
detailLines: [
|
|
2301
|
+
`Start: +${Math.round(stage.startOffsetMs)} ms`,
|
|
2302
|
+
`End: +${Math.round(stage.endOffsetMs)} ms`,
|
|
2303
|
+
`Duration: ${formatDuration(stage.durationMs)}`,
|
|
2304
|
+
],
|
|
2305
|
+
endOffsetMs: stage.endOffsetMs,
|
|
2306
|
+
id: stage.id,
|
|
2307
|
+
startOffsetMs: stage.startOffsetMs,
|
|
2308
|
+
title: stage.label,
|
|
2309
|
+
}));
|
|
2310
|
+
|
|
2311
|
+
const pluralizeCountLabel = (label: string, count: number) => {
|
|
2312
|
+
if (count === 1) return label;
|
|
2313
|
+
if (/[bcdfghjklmnpqrstvwxyz]y$/i.test(label)) return `${label.slice(0, -1)}ies`;
|
|
2314
|
+
return `${label}s`;
|
|
2315
|
+
};
|
|
2316
|
+
|
|
1753
2317
|
const WaterfallChart = ({
|
|
1754
2318
|
emptyLabel,
|
|
1755
2319
|
itemLabel,
|
|
@@ -1761,152 +2325,138 @@ const WaterfallChart = ({
|
|
|
1761
2325
|
items: TWaterfallChartItem[];
|
|
1762
2326
|
onSelect?: (itemId: string) => void;
|
|
1763
2327
|
}) => {
|
|
1764
|
-
const [ApexChartComponent, setApexChartComponent] = React.useState<unknown>(null);
|
|
1765
|
-
|
|
1766
|
-
React.useEffect(() => {
|
|
1767
|
-
let isDisposed = false;
|
|
1768
|
-
|
|
1769
|
-
void import('react-apexcharts').then((module) => {
|
|
1770
|
-
if (isDisposed) return;
|
|
1771
|
-
setApexChartComponent(() => module.default);
|
|
1772
|
-
});
|
|
1773
|
-
|
|
1774
|
-
return () => {
|
|
1775
|
-
isDisposed = true;
|
|
1776
|
-
};
|
|
1777
|
-
}, []);
|
|
1778
|
-
|
|
1779
2328
|
const totalDurationMs = Math.max(items.length > 0 ? Math.max(...items.map((item) => item.endOffsetMs)) : 1, 1);
|
|
1780
2329
|
const chartHeight = Math.max(260, items.length * waterfallRowHeight + 24);
|
|
1781
|
-
const
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
2330
|
+
const options: ApexOptions | undefined =
|
|
2331
|
+
items.length === 0
|
|
2332
|
+
? undefined
|
|
2333
|
+
: ({
|
|
2334
|
+
chart: {
|
|
2335
|
+
animations: { enabled: false },
|
|
2336
|
+
background: 'transparent',
|
|
2337
|
+
...(onSelect
|
|
2338
|
+
? {
|
|
2339
|
+
events: {
|
|
2340
|
+
dataPointSelection: (
|
|
2341
|
+
_event: unknown,
|
|
2342
|
+
_chartContext: unknown,
|
|
2343
|
+
config: { dataPointIndex: number },
|
|
2344
|
+
) => {
|
|
2345
|
+
const item = items[config.dataPointIndex];
|
|
2346
|
+
if (item) onSelect(item.id);
|
|
2347
|
+
},
|
|
2348
|
+
},
|
|
2349
|
+
}
|
|
2350
|
+
: {}),
|
|
2351
|
+
foreColor: profilerChartTheme.muted,
|
|
2352
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
|
|
2353
|
+
height: chartHeight,
|
|
2354
|
+
parentHeightOffset: 0,
|
|
2355
|
+
toolbar: { show: false },
|
|
2356
|
+
type: 'rangeBar',
|
|
2357
|
+
zoom: { enabled: false },
|
|
2358
|
+
},
|
|
2359
|
+
dataLabels: {
|
|
2360
|
+
enabled: false,
|
|
2361
|
+
},
|
|
2362
|
+
fill: {
|
|
2363
|
+
opacity: 1,
|
|
2364
|
+
},
|
|
2365
|
+
grid: {
|
|
2366
|
+
borderColor: profilerChartTheme.line,
|
|
2367
|
+
padding: { bottom: 0, left: 0, right: 0, top: 4 },
|
|
2368
|
+
xaxis: { lines: { show: true } },
|
|
2369
|
+
yaxis: { lines: { show: false } },
|
|
2370
|
+
},
|
|
2371
|
+
legend: {
|
|
2372
|
+
show: false,
|
|
2373
|
+
},
|
|
2374
|
+
noData: {
|
|
2375
|
+
style: {
|
|
2376
|
+
color: profilerChartTheme.muted,
|
|
2377
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
|
|
2378
|
+
fontSize: '11px',
|
|
1807
2379
|
},
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
formatter: (value: string | number) => `${Math.round(Number(value))} ms`,
|
|
1875
|
-
style: {
|
|
1876
|
-
colors: '#627186',
|
|
1877
|
-
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
|
|
1878
|
-
fontSize: '10px',
|
|
1879
|
-
},
|
|
1880
|
-
},
|
|
1881
|
-
max: totalDurationMs,
|
|
1882
|
-
min: 0,
|
|
1883
|
-
tickAmount: Math.min(6, Math.max(2, items.length > 0 ? 6 : 2)),
|
|
1884
|
-
type: 'numeric',
|
|
1885
|
-
},
|
|
1886
|
-
yaxis: {
|
|
1887
|
-
show: false,
|
|
1888
|
-
labels: {
|
|
1889
|
-
show: false,
|
|
1890
|
-
},
|
|
1891
|
-
},
|
|
1892
|
-
};
|
|
2380
|
+
text: emptyLabel,
|
|
2381
|
+
},
|
|
2382
|
+
plotOptions: {
|
|
2383
|
+
bar: {
|
|
2384
|
+
barHeight: waterfallBarHeight,
|
|
2385
|
+
borderRadius: 2,
|
|
2386
|
+
horizontal: true,
|
|
2387
|
+
rangeBarGroupRows: false,
|
|
2388
|
+
},
|
|
2389
|
+
},
|
|
2390
|
+
series: [
|
|
2391
|
+
{
|
|
2392
|
+
data: items.map((item) => ({
|
|
2393
|
+
fillColor: item.color,
|
|
2394
|
+
x: item.barLabel,
|
|
2395
|
+
y: [item.startOffsetMs, item.endOffsetMs],
|
|
2396
|
+
})),
|
|
2397
|
+
name: itemLabel,
|
|
2398
|
+
},
|
|
2399
|
+
],
|
|
2400
|
+
stroke: {
|
|
2401
|
+
colors: ['#ffffff'],
|
|
2402
|
+
width: 1,
|
|
2403
|
+
},
|
|
2404
|
+
tooltip: {
|
|
2405
|
+
custom: ({ dataPointIndex }: { dataPointIndex: number }) => {
|
|
2406
|
+
const item = items[dataPointIndex];
|
|
2407
|
+
if (!item) return '';
|
|
2408
|
+
|
|
2409
|
+
return `
|
|
2410
|
+
<div style="padding:8px 10px; color:#132033; font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace; font-size:11px; line-height:1.5;">
|
|
2411
|
+
<div style="font-weight:700;">${escapeHtml(item.title)}</div>
|
|
2412
|
+
${item.subtitle ? `<div style="color:#627186;">${escapeHtml(item.subtitle)}</div>` : ''}
|
|
2413
|
+
${item.detailLines
|
|
2414
|
+
.map(
|
|
2415
|
+
(line, index) =>
|
|
2416
|
+
`<div style="${index === 0 ? 'margin-top:6px;' : ''} color:#627186;">${escapeHtml(line)}</div>`,
|
|
2417
|
+
)
|
|
2418
|
+
.join('')}
|
|
2419
|
+
</div>
|
|
2420
|
+
`;
|
|
2421
|
+
},
|
|
2422
|
+
},
|
|
2423
|
+
xaxis: {
|
|
2424
|
+
axisBorder: { show: false },
|
|
2425
|
+
axisTicks: { show: false },
|
|
2426
|
+
labels: {
|
|
2427
|
+
formatter: (value: string | number) => `${Math.round(Number(value))} ms`,
|
|
2428
|
+
style: {
|
|
2429
|
+
colors: profilerChartTheme.muted,
|
|
2430
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
|
|
2431
|
+
fontSize: '10px',
|
|
2432
|
+
},
|
|
2433
|
+
},
|
|
2434
|
+
max: totalDurationMs,
|
|
2435
|
+
min: 0,
|
|
2436
|
+
tickAmount: Math.min(6, Math.max(2, items.length > 0 ? 6 : 2)),
|
|
2437
|
+
type: 'numeric',
|
|
2438
|
+
},
|
|
2439
|
+
yaxis: {
|
|
2440
|
+
show: false,
|
|
2441
|
+
labels: {
|
|
2442
|
+
show: false,
|
|
2443
|
+
},
|
|
2444
|
+
},
|
|
2445
|
+
}) as ApexOptions;
|
|
1893
2446
|
|
|
1894
2447
|
return (
|
|
1895
2448
|
<div className="proteum-profiler__section">
|
|
1896
2449
|
<div className="proteum-profiler__timelineChart">
|
|
1897
2450
|
<div className="proteum-profiler__timelineChartMeta">
|
|
1898
2451
|
<span className="proteum-profiler__mono proteum-profiler__muted">
|
|
1899
|
-
{items.length} {itemLabel}
|
|
1900
|
-
{items.length === 1 ? '' : 's'}
|
|
2452
|
+
{items.length} {pluralizeCountLabel(itemLabel, items.length)}
|
|
1901
2453
|
</span>
|
|
1902
2454
|
<span className="proteum-profiler__mono proteum-profiler__muted">{formatDuration(totalDurationMs)}</span>
|
|
1903
2455
|
</div>
|
|
1904
2456
|
|
|
1905
2457
|
<div className="proteum-profiler__timelineChartCanvas" style={{ height: `${chartHeight}px` }}>
|
|
1906
|
-
{
|
|
1907
|
-
<
|
|
1908
|
-
) : items.length > 0 ? (
|
|
1909
|
-
<div className="proteum-profiler__empty">Loading waterfall chart...</div>
|
|
2458
|
+
{options ? (
|
|
2459
|
+
<ApexChart emptyLabel={emptyLabel} options={options} />
|
|
1910
2460
|
) : (
|
|
1911
2461
|
<div className="proteum-profiler__empty">{emptyLabel}</div>
|
|
1912
2462
|
)}
|
|
@@ -1916,6 +2466,693 @@ const WaterfallChart = ({
|
|
|
1916
2466
|
);
|
|
1917
2467
|
};
|
|
1918
2468
|
|
|
2469
|
+
const createProfilerBarChartOptions = ({
|
|
2470
|
+
categories,
|
|
2471
|
+
colors,
|
|
2472
|
+
height,
|
|
2473
|
+
series,
|
|
2474
|
+
stacked = false,
|
|
2475
|
+
title,
|
|
2476
|
+
valueUnit,
|
|
2477
|
+
}: {
|
|
2478
|
+
categories: string[];
|
|
2479
|
+
colors: string[];
|
|
2480
|
+
height: number;
|
|
2481
|
+
series: Array<{ data: number[]; name: string }>;
|
|
2482
|
+
stacked?: boolean;
|
|
2483
|
+
title: string;
|
|
2484
|
+
valueUnit: string;
|
|
2485
|
+
}): ApexOptions => ({
|
|
2486
|
+
chart: {
|
|
2487
|
+
animations: { enabled: false },
|
|
2488
|
+
background: 'transparent',
|
|
2489
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
|
|
2490
|
+
foreColor: profilerChartTheme.muted,
|
|
2491
|
+
height,
|
|
2492
|
+
parentHeightOffset: 0,
|
|
2493
|
+
stacked,
|
|
2494
|
+
toolbar: { show: false },
|
|
2495
|
+
type: 'bar',
|
|
2496
|
+
zoom: { enabled: false },
|
|
2497
|
+
},
|
|
2498
|
+
colors,
|
|
2499
|
+
dataLabels: { enabled: false },
|
|
2500
|
+
grid: {
|
|
2501
|
+
borderColor: profilerChartTheme.line,
|
|
2502
|
+
padding: { bottom: 4, left: 8, right: 8, top: 0 },
|
|
2503
|
+
strokeDashArray: 2,
|
|
2504
|
+
},
|
|
2505
|
+
legend: {
|
|
2506
|
+
fontSize: '11px',
|
|
2507
|
+
horizontalAlign: 'left',
|
|
2508
|
+
position: 'top',
|
|
2509
|
+
},
|
|
2510
|
+
noData: { text: 'No chart data.' },
|
|
2511
|
+
plotOptions: {
|
|
2512
|
+
bar: {
|
|
2513
|
+
barHeight: '68%',
|
|
2514
|
+
horizontal: true,
|
|
2515
|
+
},
|
|
2516
|
+
},
|
|
2517
|
+
series,
|
|
2518
|
+
stroke: { width: 1 },
|
|
2519
|
+
title: {
|
|
2520
|
+
style: {
|
|
2521
|
+
color: profilerChartTheme.text,
|
|
2522
|
+
fontSize: '12px',
|
|
2523
|
+
fontWeight: 700,
|
|
2524
|
+
},
|
|
2525
|
+
text: title,
|
|
2526
|
+
},
|
|
2527
|
+
tooltip: { intersect: false, shared: stacked },
|
|
2528
|
+
xaxis: {
|
|
2529
|
+
axisBorder: { show: false },
|
|
2530
|
+
axisTicks: { show: false },
|
|
2531
|
+
categories,
|
|
2532
|
+
labels: { style: { colors: profilerChartTheme.muted, fontSize: '11px' } },
|
|
2533
|
+
title: {
|
|
2534
|
+
style: {
|
|
2535
|
+
color: profilerChartTheme.muted,
|
|
2536
|
+
fontSize: '10px',
|
|
2537
|
+
fontWeight: 600,
|
|
2538
|
+
},
|
|
2539
|
+
text: valueUnit,
|
|
2540
|
+
},
|
|
2541
|
+
},
|
|
2542
|
+
yaxis: {
|
|
2543
|
+
labels: {
|
|
2544
|
+
style: { colors: profilerChartTheme.text, fontSize: '11px' },
|
|
2545
|
+
},
|
|
2546
|
+
},
|
|
2547
|
+
});
|
|
2548
|
+
|
|
2549
|
+
const createProfilerColumnChartOptions = ({
|
|
2550
|
+
categories,
|
|
2551
|
+
colors,
|
|
2552
|
+
height,
|
|
2553
|
+
series,
|
|
2554
|
+
stacked = false,
|
|
2555
|
+
title,
|
|
2556
|
+
valueUnit,
|
|
2557
|
+
}: {
|
|
2558
|
+
categories: string[];
|
|
2559
|
+
colors: string[];
|
|
2560
|
+
height: number;
|
|
2561
|
+
series: Array<{ data: number[]; name: string }>;
|
|
2562
|
+
stacked?: boolean;
|
|
2563
|
+
title: string;
|
|
2564
|
+
valueUnit: string;
|
|
2565
|
+
}): ApexOptions => ({
|
|
2566
|
+
chart: {
|
|
2567
|
+
animations: { enabled: false },
|
|
2568
|
+
background: 'transparent',
|
|
2569
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
|
|
2570
|
+
foreColor: profilerChartTheme.muted,
|
|
2571
|
+
height,
|
|
2572
|
+
parentHeightOffset: 0,
|
|
2573
|
+
stacked,
|
|
2574
|
+
toolbar: { show: false },
|
|
2575
|
+
type: 'bar',
|
|
2576
|
+
zoom: { enabled: false },
|
|
2577
|
+
},
|
|
2578
|
+
colors,
|
|
2579
|
+
dataLabels: { enabled: false },
|
|
2580
|
+
grid: {
|
|
2581
|
+
borderColor: profilerChartTheme.line,
|
|
2582
|
+
padding: { bottom: 4, left: 8, right: 8, top: 0 },
|
|
2583
|
+
strokeDashArray: 2,
|
|
2584
|
+
},
|
|
2585
|
+
legend: {
|
|
2586
|
+
fontSize: '11px',
|
|
2587
|
+
horizontalAlign: 'left',
|
|
2588
|
+
position: 'top',
|
|
2589
|
+
},
|
|
2590
|
+
noData: { text: 'No chart data.' },
|
|
2591
|
+
plotOptions: {
|
|
2592
|
+
bar: {
|
|
2593
|
+
borderRadius: 2,
|
|
2594
|
+
columnWidth: '58%',
|
|
2595
|
+
horizontal: false,
|
|
2596
|
+
},
|
|
2597
|
+
},
|
|
2598
|
+
series,
|
|
2599
|
+
stroke: { width: 1 },
|
|
2600
|
+
title: {
|
|
2601
|
+
style: {
|
|
2602
|
+
color: profilerChartTheme.text,
|
|
2603
|
+
fontSize: '12px',
|
|
2604
|
+
fontWeight: 700,
|
|
2605
|
+
},
|
|
2606
|
+
text: title,
|
|
2607
|
+
},
|
|
2608
|
+
tooltip: { intersect: false, shared: stacked },
|
|
2609
|
+
xaxis: {
|
|
2610
|
+
axisBorder: { show: false },
|
|
2611
|
+
axisTicks: { show: false },
|
|
2612
|
+
categories,
|
|
2613
|
+
labels: {
|
|
2614
|
+
rotate: -28,
|
|
2615
|
+
style: { colors: profilerChartTheme.muted, fontSize: '11px' },
|
|
2616
|
+
trim: true,
|
|
2617
|
+
},
|
|
2618
|
+
},
|
|
2619
|
+
yaxis: {
|
|
2620
|
+
labels: { style: { colors: profilerChartTheme.muted, fontSize: '11px' } },
|
|
2621
|
+
title: {
|
|
2622
|
+
style: {
|
|
2623
|
+
color: profilerChartTheme.muted,
|
|
2624
|
+
fontSize: '10px',
|
|
2625
|
+
fontWeight: 600,
|
|
2626
|
+
},
|
|
2627
|
+
text: valueUnit,
|
|
2628
|
+
},
|
|
2629
|
+
},
|
|
2630
|
+
});
|
|
2631
|
+
|
|
2632
|
+
const createProfilerLineChartOptions = ({
|
|
2633
|
+
categories,
|
|
2634
|
+
colors,
|
|
2635
|
+
height,
|
|
2636
|
+
series,
|
|
2637
|
+
title,
|
|
2638
|
+
valueUnit,
|
|
2639
|
+
}: {
|
|
2640
|
+
categories: string[];
|
|
2641
|
+
colors: string[];
|
|
2642
|
+
height: number;
|
|
2643
|
+
series: Array<{ data: number[]; name: string }>;
|
|
2644
|
+
title: string;
|
|
2645
|
+
valueUnit: string;
|
|
2646
|
+
}): ApexOptions => ({
|
|
2647
|
+
chart: {
|
|
2648
|
+
animations: { enabled: false },
|
|
2649
|
+
background: 'transparent',
|
|
2650
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
|
|
2651
|
+
foreColor: profilerChartTheme.muted,
|
|
2652
|
+
height,
|
|
2653
|
+
parentHeightOffset: 0,
|
|
2654
|
+
toolbar: { show: false },
|
|
2655
|
+
type: 'line',
|
|
2656
|
+
zoom: { enabled: false },
|
|
2657
|
+
},
|
|
2658
|
+
colors,
|
|
2659
|
+
dataLabels: { enabled: false },
|
|
2660
|
+
grid: {
|
|
2661
|
+
borderColor: profilerChartTheme.line,
|
|
2662
|
+
padding: { bottom: 4, left: 8, right: 8, top: 0 },
|
|
2663
|
+
strokeDashArray: 2,
|
|
2664
|
+
},
|
|
2665
|
+
legend: {
|
|
2666
|
+
fontSize: '11px',
|
|
2667
|
+
horizontalAlign: 'left',
|
|
2668
|
+
position: 'top',
|
|
2669
|
+
},
|
|
2670
|
+
markers: { size: 4, strokeWidth: 0 },
|
|
2671
|
+
noData: { text: 'No chart data.' },
|
|
2672
|
+
series,
|
|
2673
|
+
stroke: { curve: 'smooth', width: 2 },
|
|
2674
|
+
title: {
|
|
2675
|
+
style: {
|
|
2676
|
+
color: profilerChartTheme.text,
|
|
2677
|
+
fontSize: '12px',
|
|
2678
|
+
fontWeight: 700,
|
|
2679
|
+
},
|
|
2680
|
+
text: title,
|
|
2681
|
+
},
|
|
2682
|
+
tooltip: { intersect: false, shared: true },
|
|
2683
|
+
xaxis: {
|
|
2684
|
+
axisBorder: { show: false },
|
|
2685
|
+
axisTicks: { show: false },
|
|
2686
|
+
categories,
|
|
2687
|
+
labels: { rotate: -24, style: { colors: profilerChartTheme.muted, fontSize: '11px' }, trim: true },
|
|
2688
|
+
},
|
|
2689
|
+
yaxis: {
|
|
2690
|
+
labels: { style: { colors: profilerChartTheme.muted, fontSize: '11px' } },
|
|
2691
|
+
title: {
|
|
2692
|
+
style: {
|
|
2693
|
+
color: profilerChartTheme.muted,
|
|
2694
|
+
fontSize: '10px',
|
|
2695
|
+
fontWeight: 600,
|
|
2696
|
+
},
|
|
2697
|
+
text: valueUnit,
|
|
2698
|
+
},
|
|
2699
|
+
},
|
|
2700
|
+
});
|
|
2701
|
+
|
|
2702
|
+
const createProfilerScatterChartOptions = ({
|
|
2703
|
+
colors,
|
|
2704
|
+
height,
|
|
2705
|
+
series,
|
|
2706
|
+
title,
|
|
2707
|
+
xaxisTitle,
|
|
2708
|
+
yaxisTitle,
|
|
2709
|
+
}: {
|
|
2710
|
+
colors: string[];
|
|
2711
|
+
height: number;
|
|
2712
|
+
series: Array<{ data: Array<{ x: number; y: number }>; name: string }>;
|
|
2713
|
+
title: string;
|
|
2714
|
+
xaxisTitle: string;
|
|
2715
|
+
yaxisTitle: string;
|
|
2716
|
+
}): ApexOptions => ({
|
|
2717
|
+
chart: {
|
|
2718
|
+
animations: { enabled: false },
|
|
2719
|
+
background: 'transparent',
|
|
2720
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
|
|
2721
|
+
foreColor: profilerChartTheme.muted,
|
|
2722
|
+
height,
|
|
2723
|
+
parentHeightOffset: 0,
|
|
2724
|
+
toolbar: { show: false },
|
|
2725
|
+
type: 'scatter',
|
|
2726
|
+
zoom: { enabled: false },
|
|
2727
|
+
},
|
|
2728
|
+
colors,
|
|
2729
|
+
dataLabels: { enabled: false },
|
|
2730
|
+
grid: {
|
|
2731
|
+
borderColor: profilerChartTheme.line,
|
|
2732
|
+
padding: { bottom: 4, left: 8, right: 8, top: 0 },
|
|
2733
|
+
strokeDashArray: 2,
|
|
2734
|
+
},
|
|
2735
|
+
legend: {
|
|
2736
|
+
fontSize: '11px',
|
|
2737
|
+
horizontalAlign: 'left',
|
|
2738
|
+
position: 'top',
|
|
2739
|
+
},
|
|
2740
|
+
markers: { size: 6, strokeWidth: 0 },
|
|
2741
|
+
noData: { text: 'No chart data.' },
|
|
2742
|
+
series,
|
|
2743
|
+
title: {
|
|
2744
|
+
style: {
|
|
2745
|
+
color: profilerChartTheme.text,
|
|
2746
|
+
fontSize: '12px',
|
|
2747
|
+
fontWeight: 700,
|
|
2748
|
+
},
|
|
2749
|
+
text: title,
|
|
2750
|
+
},
|
|
2751
|
+
tooltip: { intersect: false, shared: false },
|
|
2752
|
+
xaxis: {
|
|
2753
|
+
axisBorder: { show: false },
|
|
2754
|
+
axisTicks: { show: false },
|
|
2755
|
+
labels: { style: { colors: profilerChartTheme.muted, fontSize: '11px' } },
|
|
2756
|
+
title: {
|
|
2757
|
+
style: {
|
|
2758
|
+
color: profilerChartTheme.muted,
|
|
2759
|
+
fontSize: '10px',
|
|
2760
|
+
fontWeight: 600,
|
|
2761
|
+
},
|
|
2762
|
+
text: xaxisTitle,
|
|
2763
|
+
},
|
|
2764
|
+
tickAmount: 6,
|
|
2765
|
+
},
|
|
2766
|
+
yaxis: {
|
|
2767
|
+
labels: { style: { colors: profilerChartTheme.muted, fontSize: '11px' } },
|
|
2768
|
+
title: {
|
|
2769
|
+
style: {
|
|
2770
|
+
color: profilerChartTheme.muted,
|
|
2771
|
+
fontSize: '10px',
|
|
2772
|
+
fontWeight: 600,
|
|
2773
|
+
},
|
|
2774
|
+
text: yaxisTitle,
|
|
2775
|
+
},
|
|
2776
|
+
tickAmount: 6,
|
|
2777
|
+
},
|
|
2778
|
+
});
|
|
2779
|
+
|
|
2780
|
+
const createProfilerHeatmapOptions = ({
|
|
2781
|
+
height,
|
|
2782
|
+
series,
|
|
2783
|
+
title,
|
|
2784
|
+
valueUnit,
|
|
2785
|
+
}: {
|
|
2786
|
+
height: number;
|
|
2787
|
+
series: Array<{ data: Array<{ x: string; y: number }>; name: string }>;
|
|
2788
|
+
title: string;
|
|
2789
|
+
valueUnit: string;
|
|
2790
|
+
}): ApexOptions => ({
|
|
2791
|
+
chart: {
|
|
2792
|
+
animations: { enabled: false },
|
|
2793
|
+
background: 'transparent',
|
|
2794
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
|
|
2795
|
+
foreColor: profilerChartTheme.muted,
|
|
2796
|
+
height,
|
|
2797
|
+
parentHeightOffset: 0,
|
|
2798
|
+
toolbar: { show: false },
|
|
2799
|
+
type: 'heatmap',
|
|
2800
|
+
zoom: { enabled: false },
|
|
2801
|
+
},
|
|
2802
|
+
colors: [profilerChartTheme.blue],
|
|
2803
|
+
dataLabels: { enabled: false },
|
|
2804
|
+
grid: {
|
|
2805
|
+
borderColor: profilerChartTheme.line,
|
|
2806
|
+
padding: { bottom: 4, left: 8, right: 8, top: 0 },
|
|
2807
|
+
strokeDashArray: 2,
|
|
2808
|
+
},
|
|
2809
|
+
legend: { show: false },
|
|
2810
|
+
noData: { text: 'No chart data.' },
|
|
2811
|
+
plotOptions: {
|
|
2812
|
+
heatmap: {
|
|
2813
|
+
colorScale: {
|
|
2814
|
+
ranges: [
|
|
2815
|
+
{ color: '#eef4ff', from: 0, to: 0 },
|
|
2816
|
+
{ color: '#bfdbfe', from: 0.01, to: 5 },
|
|
2817
|
+
{ color: '#60a5fa', from: 5.01, to: 25 },
|
|
2818
|
+
{ color: '#2563eb', from: 25.01, to: 1000000 },
|
|
2819
|
+
],
|
|
2820
|
+
},
|
|
2821
|
+
radius: 0,
|
|
2822
|
+
},
|
|
2823
|
+
},
|
|
2824
|
+
series,
|
|
2825
|
+
stroke: { width: 1 },
|
|
2826
|
+
title: {
|
|
2827
|
+
style: {
|
|
2828
|
+
color: profilerChartTheme.text,
|
|
2829
|
+
fontSize: '12px',
|
|
2830
|
+
fontWeight: 700,
|
|
2831
|
+
},
|
|
2832
|
+
text: title,
|
|
2833
|
+
},
|
|
2834
|
+
tooltip: { intersect: false, shared: false },
|
|
2835
|
+
xaxis: {
|
|
2836
|
+
axisBorder: { show: false },
|
|
2837
|
+
axisTicks: { show: false },
|
|
2838
|
+
labels: { rotate: -24, style: { colors: profilerChartTheme.muted, fontSize: '11px' } },
|
|
2839
|
+
title: {
|
|
2840
|
+
style: {
|
|
2841
|
+
color: profilerChartTheme.muted,
|
|
2842
|
+
fontSize: '10px',
|
|
2843
|
+
fontWeight: 600,
|
|
2844
|
+
},
|
|
2845
|
+
text: valueUnit,
|
|
2846
|
+
},
|
|
2847
|
+
},
|
|
2848
|
+
yaxis: {
|
|
2849
|
+
labels: { style: { colors: profilerChartTheme.text, fontSize: '11px' } },
|
|
2850
|
+
},
|
|
2851
|
+
});
|
|
2852
|
+
|
|
2853
|
+
const buildTopEntries = (entries: Array<{ label: string; value: number }>, limit = 8) =>
|
|
2854
|
+
entries
|
|
2855
|
+
.filter((entry) => entry.label && entry.value > 0)
|
|
2856
|
+
.sort((left, right) => right.value - left.value || left.label.localeCompare(right.label))
|
|
2857
|
+
.slice(0, limit);
|
|
2858
|
+
|
|
2859
|
+
const buildCountEntries = (labels: string[], limit = 8) => {
|
|
2860
|
+
const counts = new Map<string, number>();
|
|
2861
|
+
for (const label of labels) counts.set(label, (counts.get(label) || 0) + 1);
|
|
2862
|
+
return buildTopEntries(
|
|
2863
|
+
[...counts.entries()].map(([label, value]) => ({ label, value })),
|
|
2864
|
+
limit,
|
|
2865
|
+
);
|
|
2866
|
+
};
|
|
2867
|
+
|
|
2868
|
+
const buildDurationEntries = <T,>(items: T[], readLabel: (item: T) => string | undefined, readValue: (item: T) => number | undefined, limit = 8) => {
|
|
2869
|
+
const totals = new Map<string, number>();
|
|
2870
|
+
for (const item of items) {
|
|
2871
|
+
const label = readLabel(item);
|
|
2872
|
+
const value = readValue(item);
|
|
2873
|
+
if (!label || value === undefined) continue;
|
|
2874
|
+
totals.set(label, (totals.get(label) || 0) + value);
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
return buildTopEntries(
|
|
2878
|
+
[...totals.entries()].map(([label, value]) => ({ label, value })),
|
|
2879
|
+
limit,
|
|
2880
|
+
);
|
|
2881
|
+
};
|
|
2882
|
+
|
|
2883
|
+
const buildHorizontalBarChartOptions = ({
|
|
2884
|
+
color,
|
|
2885
|
+
entries,
|
|
2886
|
+
title,
|
|
2887
|
+
valueUnit,
|
|
2888
|
+
}: {
|
|
2889
|
+
color: string;
|
|
2890
|
+
entries: Array<{ label: string; value: number }>;
|
|
2891
|
+
title: string;
|
|
2892
|
+
valueUnit: string;
|
|
2893
|
+
}) =>
|
|
2894
|
+
entries.length === 0
|
|
2895
|
+
? undefined
|
|
2896
|
+
: createProfilerBarChartOptions({
|
|
2897
|
+
categories: entries.map((entry) => truncate(entry.label, 44)),
|
|
2898
|
+
colors: [color],
|
|
2899
|
+
height: buildChartHeight(entries.length),
|
|
2900
|
+
series: [{ data: entries.map((entry) => toRoundedNumber(entry.value)), name: valueUnit }],
|
|
2901
|
+
title,
|
|
2902
|
+
valueUnit,
|
|
2903
|
+
});
|
|
2904
|
+
|
|
2905
|
+
const buildColumnChartOptions = ({
|
|
2906
|
+
colors,
|
|
2907
|
+
entries,
|
|
2908
|
+
title,
|
|
2909
|
+
valueUnit,
|
|
2910
|
+
}: {
|
|
2911
|
+
colors: string[];
|
|
2912
|
+
entries: Array<{ label: string; value: number }>;
|
|
2913
|
+
title: string;
|
|
2914
|
+
valueUnit: string;
|
|
2915
|
+
}) =>
|
|
2916
|
+
entries.length === 0
|
|
2917
|
+
? undefined
|
|
2918
|
+
: createProfilerColumnChartOptions({
|
|
2919
|
+
categories: entries.map((entry) => truncate(entry.label, 28)),
|
|
2920
|
+
colors,
|
|
2921
|
+
height: 280,
|
|
2922
|
+
series: [{ data: entries.map((entry) => toRoundedNumber(entry.value)), name: valueUnit }],
|
|
2923
|
+
title,
|
|
2924
|
+
valueUnit,
|
|
2925
|
+
});
|
|
2926
|
+
|
|
2927
|
+
const buildLineChartOptions = ({
|
|
2928
|
+
color,
|
|
2929
|
+
entries,
|
|
2930
|
+
title,
|
|
2931
|
+
valueUnit,
|
|
2932
|
+
}: {
|
|
2933
|
+
color: string;
|
|
2934
|
+
entries: Array<{ label: string; value: number }>;
|
|
2935
|
+
title: string;
|
|
2936
|
+
valueUnit: string;
|
|
2937
|
+
}) =>
|
|
2938
|
+
entries.length === 0
|
|
2939
|
+
? undefined
|
|
2940
|
+
: createProfilerLineChartOptions({
|
|
2941
|
+
categories: entries.map((entry) => truncate(entry.label, 28)),
|
|
2942
|
+
colors: [color],
|
|
2943
|
+
height: 280,
|
|
2944
|
+
series: [{ data: entries.map((entry) => toRoundedNumber(entry.value)), name: valueUnit }],
|
|
2945
|
+
title,
|
|
2946
|
+
valueUnit,
|
|
2947
|
+
});
|
|
2948
|
+
|
|
2949
|
+
const buildScatterChartOptions = ({
|
|
2950
|
+
color,
|
|
2951
|
+
points,
|
|
2952
|
+
seriesName,
|
|
2953
|
+
title,
|
|
2954
|
+
xaxisTitle,
|
|
2955
|
+
yaxisTitle,
|
|
2956
|
+
}: {
|
|
2957
|
+
color: string;
|
|
2958
|
+
points: Array<{ x: number; y: number }>;
|
|
2959
|
+
seriesName: string;
|
|
2960
|
+
title: string;
|
|
2961
|
+
xaxisTitle: string;
|
|
2962
|
+
yaxisTitle: string;
|
|
2963
|
+
}) =>
|
|
2964
|
+
points.length === 0
|
|
2965
|
+
? undefined
|
|
2966
|
+
: createProfilerScatterChartOptions({
|
|
2967
|
+
colors: [color],
|
|
2968
|
+
height: 300,
|
|
2969
|
+
series: [{ data: points, name: seriesName }],
|
|
2970
|
+
title,
|
|
2971
|
+
xaxisTitle,
|
|
2972
|
+
yaxisTitle,
|
|
2973
|
+
});
|
|
2974
|
+
|
|
2975
|
+
const buildStackedColumnChartOptions = ({
|
|
2976
|
+
categories,
|
|
2977
|
+
colors,
|
|
2978
|
+
series,
|
|
2979
|
+
title,
|
|
2980
|
+
valueUnit,
|
|
2981
|
+
}: {
|
|
2982
|
+
categories: string[];
|
|
2983
|
+
colors: string[];
|
|
2984
|
+
series: Array<{ data: number[]; name: string }>;
|
|
2985
|
+
title: string;
|
|
2986
|
+
valueUnit: string;
|
|
2987
|
+
}) =>
|
|
2988
|
+
categories.length === 0 || series.length === 0 || series.every((entry) => entry.data.every((value) => value === 0))
|
|
2989
|
+
? undefined
|
|
2990
|
+
: createProfilerColumnChartOptions({
|
|
2991
|
+
categories,
|
|
2992
|
+
colors,
|
|
2993
|
+
height: 300,
|
|
2994
|
+
series,
|
|
2995
|
+
stacked: true,
|
|
2996
|
+
title,
|
|
2997
|
+
valueUnit,
|
|
2998
|
+
});
|
|
2999
|
+
|
|
3000
|
+
const buildHeatmapChartOptions = ({
|
|
3001
|
+
rows,
|
|
3002
|
+
title,
|
|
3003
|
+
valueUnit,
|
|
3004
|
+
}: {
|
|
3005
|
+
rows: Array<{ data: Array<{ x: string; y: number }>; name: string }>;
|
|
3006
|
+
title: string;
|
|
3007
|
+
valueUnit: string;
|
|
3008
|
+
}) =>
|
|
3009
|
+
rows.length === 0 || rows.every((row) => row.data.every((entry) => entry.y === 0))
|
|
3010
|
+
? undefined
|
|
3011
|
+
: createProfilerHeatmapOptions({
|
|
3012
|
+
height: buildChartHeight(rows.length, { max: 360, min: 240, rowHeight: 36 }),
|
|
3013
|
+
series: rows,
|
|
3014
|
+
title,
|
|
3015
|
+
valueUnit,
|
|
3016
|
+
});
|
|
3017
|
+
|
|
3018
|
+
const getSessionChartLabel = (candidate: TProfilerNavigationSession, summary: TSessionSummary) =>
|
|
3019
|
+
truncate(`${formatTimeLabel(candidate.startedAt)} ${summary.routeLabel}`, 28);
|
|
3020
|
+
|
|
3021
|
+
const getApiOriginLabel = (item: TApiRequestItem) => item.originLabel || item.groupLabel;
|
|
3022
|
+
|
|
3023
|
+
const getRequestStatusGroup = (statusCode?: number, statusLabel?: string) => {
|
|
3024
|
+
if (statusCode === undefined) return statusLabel || 'pending';
|
|
3025
|
+
if (statusCode >= 500) return '5xx';
|
|
3026
|
+
if (statusCode >= 400) return '4xx';
|
|
3027
|
+
if (statusCode >= 300) return '3xx';
|
|
3028
|
+
if (statusCode >= 200) return '2xx';
|
|
3029
|
+
return String(statusCode);
|
|
3030
|
+
};
|
|
3031
|
+
|
|
3032
|
+
const buildPerfTopLatencyChartOptions = (rows: NonNullable<TProfilerState['perf']['top']>['rows']): ApexOptions | undefined => {
|
|
3033
|
+
if (rows.length === 0) return undefined;
|
|
3034
|
+
|
|
3035
|
+
return createProfilerBarChartOptions({
|
|
3036
|
+
categories: rows.map((row) => truncate(row.label, 40)),
|
|
3037
|
+
colors: [profilerChartTheme.blue, profilerChartTheme.indigo],
|
|
3038
|
+
height: buildChartHeight(rows.length),
|
|
3039
|
+
series: [
|
|
3040
|
+
{ data: rows.map((row) => toRoundedNumber(row.avgDurationMs)), name: 'Avg ms' },
|
|
3041
|
+
{ data: rows.map((row) => toRoundedNumber(row.p95DurationMs)), name: 'P95 ms' },
|
|
3042
|
+
],
|
|
3043
|
+
title: 'Latency by hot path',
|
|
3044
|
+
valueUnit: 'Milliseconds',
|
|
3045
|
+
});
|
|
3046
|
+
};
|
|
3047
|
+
|
|
3048
|
+
const buildPerfBreakdownChartOptions = (rows: NonNullable<TProfilerState['perf']['top']>['rows']): ApexOptions | undefined => {
|
|
3049
|
+
if (rows.length === 0) return undefined;
|
|
3050
|
+
|
|
3051
|
+
return createProfilerBarChartOptions({
|
|
3052
|
+
categories: rows.map((row) => truncate(row.label, 40)),
|
|
3053
|
+
colors: [profilerChartTheme.blue, profilerChartTheme.amber, profilerChartTheme.teal, profilerChartTheme.green],
|
|
3054
|
+
height: buildChartHeight(rows.length),
|
|
3055
|
+
series: [
|
|
3056
|
+
{ data: rows.map((row) => toRoundedNumber(row.avgSelfDurationMs)), name: 'Self ms' },
|
|
3057
|
+
{ data: rows.map((row) => toRoundedNumber(row.avgSqlDurationMs)), name: 'SQL ms' },
|
|
3058
|
+
{ data: rows.map((row) => toRoundedNumber(row.avgCallDurationMs)), name: 'Calls ms' },
|
|
3059
|
+
{ data: rows.map((row) => toRoundedNumber(row.avgRenderDurationMs)), name: 'Render ms' },
|
|
3060
|
+
],
|
|
3061
|
+
stacked: true,
|
|
3062
|
+
title: 'Average time breakdown',
|
|
3063
|
+
valueUnit: 'Milliseconds',
|
|
3064
|
+
});
|
|
3065
|
+
};
|
|
3066
|
+
|
|
3067
|
+
const buildPerfCompareChartOptions = (rows: NonNullable<TProfilerState['perf']['compare']>['rows']): ApexOptions | undefined => {
|
|
3068
|
+
if (rows.length === 0) return undefined;
|
|
3069
|
+
|
|
3070
|
+
const values = rows.map((row) => toRoundedNumber(row.p95DurationMs.deltaPercent));
|
|
3071
|
+
const minValue = Math.min(...values, 0);
|
|
3072
|
+
const maxValue = Math.max(...values, 0);
|
|
3073
|
+
|
|
3074
|
+
return {
|
|
3075
|
+
...createProfilerBarChartOptions({
|
|
3076
|
+
categories: rows.map((row) => truncate(row.label, 40)),
|
|
3077
|
+
colors: rows.map((row) =>
|
|
3078
|
+
row.change === 'improved'
|
|
3079
|
+
? profilerChartTheme.green
|
|
3080
|
+
: row.change === 'regressed'
|
|
3081
|
+
? profilerChartTheme.red
|
|
3082
|
+
: row.change === 'new'
|
|
3083
|
+
? profilerChartTheme.blue
|
|
3084
|
+
: row.change === 'removed'
|
|
3085
|
+
? profilerChartTheme.muted
|
|
3086
|
+
: profilerChartTheme.orange,
|
|
3087
|
+
),
|
|
3088
|
+
height: buildChartHeight(rows.length, { max: 420 }),
|
|
3089
|
+
series: [{ data: values, name: 'P95 delta %' }],
|
|
3090
|
+
title: 'Regression pressure',
|
|
3091
|
+
valueUnit: 'Percent vs baseline',
|
|
3092
|
+
}),
|
|
3093
|
+
plotOptions: {
|
|
3094
|
+
bar: {
|
|
3095
|
+
barHeight: '68%',
|
|
3096
|
+
distributed: true,
|
|
3097
|
+
horizontal: true,
|
|
3098
|
+
},
|
|
3099
|
+
},
|
|
3100
|
+
xaxis: {
|
|
3101
|
+
axisBorder: { show: false },
|
|
3102
|
+
axisTicks: { show: false },
|
|
3103
|
+
categories: rows.map((row) => truncate(row.label, 40)),
|
|
3104
|
+
labels: { style: { colors: profilerChartTheme.muted, fontSize: '11px' } },
|
|
3105
|
+
max: Math.max(10, Math.ceil(maxValue / 10) * 10),
|
|
3106
|
+
min: Math.min(-10, Math.floor(minValue / 10) * 10),
|
|
3107
|
+
title: {
|
|
3108
|
+
style: {
|
|
3109
|
+
color: profilerChartTheme.muted,
|
|
3110
|
+
fontSize: '10px',
|
|
3111
|
+
fontWeight: 600,
|
|
3112
|
+
},
|
|
3113
|
+
text: 'Percent vs baseline',
|
|
3114
|
+
},
|
|
3115
|
+
},
|
|
3116
|
+
};
|
|
3117
|
+
};
|
|
3118
|
+
|
|
3119
|
+
const buildPerfMemoryChartOptions = (rows: NonNullable<TProfilerState['perf']['memory']>['rows']): ApexOptions | undefined => {
|
|
3120
|
+
if (rows.length === 0) return undefined;
|
|
3121
|
+
|
|
3122
|
+
return createProfilerBarChartOptions({
|
|
3123
|
+
categories: rows.map((row) => truncate(row.label, 40)),
|
|
3124
|
+
colors: [profilerChartTheme.amber, profilerChartTheme.red, profilerChartTheme.cyan],
|
|
3125
|
+
height: buildChartHeight(rows.length),
|
|
3126
|
+
series: [
|
|
3127
|
+
{ data: rows.map((row) => toKilobytes(row.avgHeapDeltaBytes)), name: 'Avg heap KB' },
|
|
3128
|
+
{ data: rows.map((row) => toKilobytes(row.maxHeapDeltaBytes)), name: 'Max heap KB' },
|
|
3129
|
+
{ data: rows.map((row) => toKilobytes(row.avgRssDeltaBytes)), name: 'Avg RSS KB' },
|
|
3130
|
+
],
|
|
3131
|
+
title: 'Memory drift by group',
|
|
3132
|
+
valueUnit: 'Kilobytes',
|
|
3133
|
+
});
|
|
3134
|
+
};
|
|
3135
|
+
|
|
3136
|
+
const ChartSection = ({
|
|
3137
|
+
emptyLabel,
|
|
3138
|
+
options,
|
|
3139
|
+
subtitle,
|
|
3140
|
+
title,
|
|
3141
|
+
}: {
|
|
3142
|
+
emptyLabel: string;
|
|
3143
|
+
options?: ApexOptions;
|
|
3144
|
+
subtitle: string;
|
|
3145
|
+
title: string;
|
|
3146
|
+
}) => (
|
|
3147
|
+
<div className="proteum-profiler__chartCard">
|
|
3148
|
+
<div className="proteum-profiler__chartHeader">
|
|
3149
|
+
<div className="proteum-profiler__sectionTitle">{title}</div>
|
|
3150
|
+
<div className="proteum-profiler__chartSubtitle">{subtitle}</div>
|
|
3151
|
+
</div>
|
|
3152
|
+
<ApexChart emptyLabel={emptyLabel} options={options} />
|
|
3153
|
+
</div>
|
|
3154
|
+
);
|
|
3155
|
+
|
|
1919
3156
|
const TimelinePanel = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
1920
3157
|
const selections: TTraceEventInspectorSelection[] = session.traces.flatMap((traceItem) =>
|
|
1921
3158
|
traceItem.trace
|
|
@@ -2011,6 +3248,46 @@ const AuthPanel = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
|
2011
3248
|
? [{ authEvents, id: traceItem.id, label: formatSessionTraceDisplay(traceItem), trace: traceItem.trace }]
|
|
2012
3249
|
: [];
|
|
2013
3250
|
});
|
|
3251
|
+
const allAuthEvents = authSections.flatMap((section) => section.authEvents);
|
|
3252
|
+
const authEventTypeChart = buildHorizontalBarChartOptions({
|
|
3253
|
+
color: profilerChartTheme.cyan,
|
|
3254
|
+
entries: buildCountEntries(allAuthEvents.map((event) => event.type.replace(/^auth\./, ''))),
|
|
3255
|
+
title: 'Auth event frequency',
|
|
3256
|
+
valueUnit: 'Events',
|
|
3257
|
+
});
|
|
3258
|
+
const authRuleChart = buildColumnChartOptions({
|
|
3259
|
+
colors: [profilerChartTheme.indigo],
|
|
3260
|
+
entries: buildCountEntries(
|
|
3261
|
+
allAuthEvents
|
|
3262
|
+
.filter((event) => event.type === 'auth.check.rule')
|
|
3263
|
+
.map(
|
|
3264
|
+
(event) =>
|
|
3265
|
+
readString(event.details.rule) ||
|
|
3266
|
+
readString(event.details.name) ||
|
|
3267
|
+
readString(event.details.label) ||
|
|
3268
|
+
`rule ${event.index}`,
|
|
3269
|
+
),
|
|
3270
|
+
),
|
|
3271
|
+
title: 'Rule hits',
|
|
3272
|
+
valueUnit: 'Checks',
|
|
3273
|
+
});
|
|
3274
|
+
const authResultChart = buildColumnChartOptions({
|
|
3275
|
+
colors: [profilerChartTheme.green],
|
|
3276
|
+
entries: buildCountEntries(
|
|
3277
|
+
allAuthEvents
|
|
3278
|
+
.filter((event) => event.type === 'auth.check.result' || event.type === 'auth.route')
|
|
3279
|
+
.map(
|
|
3280
|
+
(event) =>
|
|
3281
|
+
readString(event.details.result) ||
|
|
3282
|
+
readString(event.details.status) ||
|
|
3283
|
+
readString(event.details.mode) ||
|
|
3284
|
+
renderSummaryValue(event.details.allowed) ||
|
|
3285
|
+
event.type,
|
|
3286
|
+
),
|
|
3287
|
+
),
|
|
3288
|
+
title: 'Auth outcomes',
|
|
3289
|
+
valueUnit: 'Events',
|
|
3290
|
+
});
|
|
2014
3291
|
const selections: TTraceEventInspectorSelection[] = authSections.flatMap((section) =>
|
|
2015
3292
|
section.authEvents.map((event) => ({
|
|
2016
3293
|
event,
|
|
@@ -2033,6 +3310,27 @@ const AuthPanel = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
|
2033
3310
|
return (
|
|
2034
3311
|
<div className="proteum-profiler__splitView">
|
|
2035
3312
|
<div className="proteum-profiler__splitColumn">
|
|
3313
|
+
<div className="proteum-profiler__chartGrid">
|
|
3314
|
+
<ChartSection
|
|
3315
|
+
emptyLabel="No auth events were captured for this session."
|
|
3316
|
+
options={authEventTypeChart}
|
|
3317
|
+
subtitle="See which auth phases are actually active for the selected navigation."
|
|
3318
|
+
title="Auth Flow"
|
|
3319
|
+
/>
|
|
3320
|
+
<ChartSection
|
|
3321
|
+
emptyLabel="No rule checks were captured for this session."
|
|
3322
|
+
options={authRuleChart}
|
|
3323
|
+
subtitle="Highlight the rules that are firing most often in the current auth flow."
|
|
3324
|
+
title="Rule Pressure"
|
|
3325
|
+
/>
|
|
3326
|
+
<ChartSection
|
|
3327
|
+
emptyLabel="No auth outcome events were captured for this session."
|
|
3328
|
+
options={authResultChart}
|
|
3329
|
+
subtitle="Summarize allow, deny, and routing outcomes without reading every trace row."
|
|
3330
|
+
title="Outcomes"
|
|
3331
|
+
/>
|
|
3332
|
+
</div>
|
|
3333
|
+
|
|
2036
3334
|
{authSections.map((section) => (
|
|
2037
3335
|
<AuthTraceSection
|
|
2038
3336
|
authEvents={section.authEvents}
|
|
@@ -2111,24 +3409,101 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
2111
3409
|
const primaryTrace = summary.primaryTrace?.trace;
|
|
2112
3410
|
|
|
2113
3411
|
if (panel === 'summary') {
|
|
3412
|
+
const recentSessionRows = state.sessions.slice(-8).map((candidate) => ({
|
|
3413
|
+
session: candidate,
|
|
3414
|
+
summary: getSummary(candidate),
|
|
3415
|
+
}));
|
|
3416
|
+
const recentSessionLabels = recentSessionRows.map(({ session: candidate, summary: candidateSummary }) =>
|
|
3417
|
+
getSessionChartLabel(candidate, candidateSummary),
|
|
3418
|
+
);
|
|
3419
|
+
const durationTrendChart = buildLineChartOptions({
|
|
3420
|
+
color: profilerChartTheme.blue,
|
|
3421
|
+
entries: recentSessionRows.map(({ session: candidate, summary: candidateSummary }) => ({
|
|
3422
|
+
label: getSessionChartLabel(candidate, candidateSummary),
|
|
3423
|
+
value: candidateSummary.totalMs || 0,
|
|
3424
|
+
})),
|
|
3425
|
+
title: 'Recent navigation duration',
|
|
3426
|
+
valueUnit: 'Milliseconds',
|
|
3427
|
+
});
|
|
3428
|
+
const workloadChart =
|
|
3429
|
+
recentSessionLabels.length === 0
|
|
3430
|
+
? undefined
|
|
3431
|
+
: createProfilerColumnChartOptions({
|
|
3432
|
+
categories: recentSessionLabels,
|
|
3433
|
+
colors: [profilerChartTheme.indigo, profilerChartTheme.teal, profilerChartTheme.red],
|
|
3434
|
+
height: 300,
|
|
3435
|
+
series: [
|
|
3436
|
+
{
|
|
3437
|
+
data: recentSessionRows.map(({ summary: candidateSummary }) => candidateSummary.apiSyncCount + candidateSummary.apiAsyncCount),
|
|
3438
|
+
name: 'API',
|
|
3439
|
+
},
|
|
3440
|
+
{ data: recentSessionRows.map(({ summary: candidateSummary }) => candidateSummary.sqlCount), name: 'SQL' },
|
|
3441
|
+
{ data: recentSessionRows.map(({ summary: candidateSummary }) => candidateSummary.errorCount), name: 'Errors' },
|
|
3442
|
+
],
|
|
3443
|
+
title: 'Recent workload mix',
|
|
3444
|
+
valueUnit: 'Count',
|
|
3445
|
+
});
|
|
3446
|
+
const routeFrequencyChart = buildHorizontalBarChartOptions({
|
|
3447
|
+
color: profilerChartTheme.amber,
|
|
3448
|
+
entries: buildCountEntries(recentSessionRows.map(({ summary: candidateSummary }) => candidateSummary.routeLabel)),
|
|
3449
|
+
title: 'Hot recent routes',
|
|
3450
|
+
valueUnit: 'Sessions',
|
|
3451
|
+
});
|
|
3452
|
+
const statusSpreadChart = buildColumnChartOptions({
|
|
3453
|
+
colors: [profilerChartTheme.green],
|
|
3454
|
+
entries: buildCountEntries(recentSessionRows.map(({ summary: candidateSummary }) => candidateSummary.statusLabel)),
|
|
3455
|
+
title: 'Recent status spread',
|
|
3456
|
+
valueUnit: 'Sessions',
|
|
3457
|
+
});
|
|
3458
|
+
|
|
2114
3459
|
return (
|
|
2115
|
-
|
|
2116
|
-
<
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
3460
|
+
<>
|
|
3461
|
+
<div className="proteum-profiler__chartGrid">
|
|
3462
|
+
<ChartSection
|
|
3463
|
+
emptyLabel="No recent session durations were captured yet."
|
|
3464
|
+
options={durationTrendChart}
|
|
3465
|
+
subtitle="Track how the last few navigations are trending instead of reading one request in isolation."
|
|
3466
|
+
title="Duration Trend"
|
|
3467
|
+
/>
|
|
3468
|
+
<ChartSection
|
|
3469
|
+
emptyLabel="No recent workload data was captured yet."
|
|
3470
|
+
options={workloadChart}
|
|
3471
|
+
subtitle="Compare API, SQL, and error volume across the most recent sessions."
|
|
3472
|
+
title="Workload Mix"
|
|
3473
|
+
/>
|
|
3474
|
+
<ChartSection
|
|
3475
|
+
emptyLabel="No recent route frequency data was captured yet."
|
|
3476
|
+
options={routeFrequencyChart}
|
|
3477
|
+
subtitle="See which routes are dominating the recent debugging session."
|
|
3478
|
+
title="Route Frequency"
|
|
3479
|
+
/>
|
|
3480
|
+
<ChartSection
|
|
3481
|
+
emptyLabel="No recent session statuses were captured yet."
|
|
3482
|
+
options={statusSpreadChart}
|
|
3483
|
+
subtitle="Quick view of SSR, navigation, and request status patterns."
|
|
3484
|
+
title="Status Spread"
|
|
3485
|
+
/>
|
|
3486
|
+
</div>
|
|
3487
|
+
|
|
3488
|
+
<div className="proteum-profiler__metrics">
|
|
3489
|
+
<SummaryRow label="Session" value={session.label} />
|
|
3490
|
+
<SummaryRow label="Status" value={summary.statusLabel} />
|
|
3491
|
+
<SummaryRow label="Duration" value={formatDuration(summary.totalMs)} />
|
|
3492
|
+
<SummaryRow label="Route" value={summary.routeLabel} />
|
|
3493
|
+
<SummaryRow
|
|
3494
|
+
label="SSR"
|
|
3495
|
+
value={
|
|
3496
|
+
summary.ssrPayloadBytes !== undefined
|
|
3497
|
+
? `${formatDuration(summary.renderMs)} | ${formatBytes(summary.ssrPayloadBytes)}`
|
|
3498
|
+
: formatDuration(summary.renderMs)
|
|
3499
|
+
}
|
|
3500
|
+
/>
|
|
3501
|
+
<SummaryRow label="API" value={`sync ${summary.apiSyncCount} / async ${summary.apiAsyncCount}`} />
|
|
3502
|
+
<SummaryRow label="SQL" value={String(summary.sqlCount)} />
|
|
3503
|
+
<SummaryRow label="Errors" value={String(summary.errorCount)} />
|
|
3504
|
+
<SummaryRow label="Request" value={session.requestId || 'client-only'} />
|
|
3505
|
+
</div>
|
|
3506
|
+
</>
|
|
2132
3507
|
);
|
|
2133
3508
|
}
|
|
2134
3509
|
|
|
@@ -2136,66 +3511,435 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
2136
3511
|
return <TimelinePanel session={session} />;
|
|
2137
3512
|
}
|
|
2138
3513
|
|
|
3514
|
+
if (panel === 'perf') {
|
|
3515
|
+
const perf = state.perf;
|
|
3516
|
+
const currentRequest = primaryTrace ? buildRequestPerformance(primaryTrace) : undefined;
|
|
3517
|
+
const waterfallItems = currentRequest ? buildPerfWaterfallItems(currentRequest) : [];
|
|
3518
|
+
const topRows =
|
|
3519
|
+
perf.top?.rows.map((row) => ({
|
|
3520
|
+
key: `top:${row.key}`,
|
|
3521
|
+
title: `${row.label} · ${formatDuration(row.avgDurationMs)}`,
|
|
3522
|
+
value: `requests=${row.requestCount} p95=${formatDuration(row.p95DurationMs)} cpu=${formatDuration(row.avgCpuMs)} sql=${formatDuration(row.avgSqlDurationMs)} heap=${formatSignedBytes(row.avgHeapDeltaBytes)} slowest=${row.slowestRequestId || 'n/a'}`,
|
|
3523
|
+
})) || [];
|
|
3524
|
+
const compareRows =
|
|
3525
|
+
perf.compare?.rows.map((row) => ({
|
|
3526
|
+
key: `compare:${row.key}`,
|
|
3527
|
+
title: `[${row.change}] ${row.label}`,
|
|
3528
|
+
value: `p95=${formatSignedPercent(row.p95DurationMs.deltaPercent)} avg=${formatSignedPercent(row.avgDurationMs.deltaPercent)} cpu=${formatSignedPercent(row.avgCpuMs.deltaPercent)} heap=${formatSignedBytes(row.avgHeapDeltaBytes.delta)} sql=${formatSignedPercent(row.avgSqlDurationMs.deltaPercent)}`,
|
|
3529
|
+
})) || [];
|
|
3530
|
+
const memoryRows =
|
|
3531
|
+
perf.memory?.rows.map((row) => ({
|
|
3532
|
+
key: `memory:${row.key}`,
|
|
3533
|
+
title: `[${row.trend}] ${row.label}`,
|
|
3534
|
+
value: `requests=${row.requestCount} heap avg=${formatSignedBytes(row.avgHeapDeltaBytes)} max=${formatSignedBytes(row.maxHeapDeltaBytes)} rss avg=${formatSignedBytes(row.avgRssDeltaBytes)} drift=${formatSignedPercent(row.positiveHeapDriftRatio * 100)}`,
|
|
3535
|
+
})) || [];
|
|
3536
|
+
const callRows =
|
|
3537
|
+
currentRequest?.hottestCalls.map((call) => ({
|
|
3538
|
+
key: `call:${call.id}`,
|
|
3539
|
+
title: call.label,
|
|
3540
|
+
value: `duration=${formatDuration(call.durationMs)} status=${call.statusCode ?? 'pending'} origin=${call.origin}${call.errorMessage ? ` error=${call.errorMessage}` : ''}`,
|
|
3541
|
+
})) || [];
|
|
3542
|
+
const sqlRows =
|
|
3543
|
+
currentRequest?.hottestSqlQueries.map((query) => ({
|
|
3544
|
+
key: `sql:${query.id}`,
|
|
3545
|
+
title: `${query.callerLabel} · ${formatDuration(query.durationMs)}`,
|
|
3546
|
+
value: `${query.operation}${query.model ? ` ${query.model}` : ''} | ${truncate(query.query, 160)}`,
|
|
3547
|
+
})) || [];
|
|
3548
|
+
const topLatencyChart = perf.top ? buildPerfTopLatencyChartOptions(perf.top.rows) : undefined;
|
|
3549
|
+
const topBreakdownChart = perf.top ? buildPerfBreakdownChartOptions(perf.top.rows) : undefined;
|
|
3550
|
+
const compareChart = perf.compare ? buildPerfCompareChartOptions(perf.compare.rows) : undefined;
|
|
3551
|
+
const memoryChart = perf.memory ? buildPerfMemoryChartOptions(perf.memory.rows) : undefined;
|
|
3552
|
+
|
|
3553
|
+
return (
|
|
3554
|
+
<div className="proteum-profiler__section">
|
|
3555
|
+
<div className="proteum-profiler__sectionHeader">
|
|
3556
|
+
<div className="proteum-profiler__sectionTitle">Performance</div>
|
|
3557
|
+
<div className="proteum-profiler__actions">
|
|
3558
|
+
<select
|
|
3559
|
+
aria-label="Performance top window"
|
|
3560
|
+
className="proteum-profiler__select"
|
|
3561
|
+
onChange={(event) => void profilerRuntime.refreshPerf({ since: event.currentTarget.value })}
|
|
3562
|
+
value={perf.since}
|
|
3563
|
+
>
|
|
3564
|
+
{perfWindowPresets.map((windowPreset) => (
|
|
3565
|
+
<option key={`since:${windowPreset}`} value={windowPreset}>
|
|
3566
|
+
since {windowPreset}
|
|
3567
|
+
</option>
|
|
3568
|
+
))}
|
|
3569
|
+
</select>
|
|
3570
|
+
<select
|
|
3571
|
+
aria-label="Performance baseline window"
|
|
3572
|
+
className="proteum-profiler__select"
|
|
3573
|
+
onChange={(event) => void profilerRuntime.refreshPerf({ baseline: event.currentTarget.value })}
|
|
3574
|
+
value={perf.baseline}
|
|
3575
|
+
>
|
|
3576
|
+
{perfWindowPresets.map((windowPreset) => (
|
|
3577
|
+
<option key={`baseline:${windowPreset}`} value={windowPreset}>
|
|
3578
|
+
baseline {windowPreset}
|
|
3579
|
+
</option>
|
|
3580
|
+
))}
|
|
3581
|
+
</select>
|
|
3582
|
+
<select
|
|
3583
|
+
aria-label="Performance target window"
|
|
3584
|
+
className="proteum-profiler__select"
|
|
3585
|
+
onChange={(event) => void profilerRuntime.refreshPerf({ target: event.currentTarget.value })}
|
|
3586
|
+
value={perf.target}
|
|
3587
|
+
>
|
|
3588
|
+
{perfWindowPresets.map((windowPreset) => (
|
|
3589
|
+
<option key={`target:${windowPreset}`} value={windowPreset}>
|
|
3590
|
+
target {windowPreset}
|
|
3591
|
+
</option>
|
|
3592
|
+
))}
|
|
3593
|
+
</select>
|
|
3594
|
+
<select
|
|
3595
|
+
aria-label="Performance grouping"
|
|
3596
|
+
className="proteum-profiler__select"
|
|
3597
|
+
onChange={(event) => void profilerRuntime.refreshPerf({ groupBy: event.currentTarget.value as (typeof perfGroupByValues)[number] })}
|
|
3598
|
+
value={perf.groupBy}
|
|
3599
|
+
>
|
|
3600
|
+
{perfGroupByValues.map((groupBy) => (
|
|
3601
|
+
<option key={`group:${groupBy}`} value={groupBy}>
|
|
3602
|
+
group {groupBy}
|
|
3603
|
+
</option>
|
|
3604
|
+
))}
|
|
3605
|
+
</select>
|
|
3606
|
+
<button className="proteum-profiler__pill" onClick={() => void profilerRuntime.refreshPerf()} type="button">
|
|
3607
|
+
Refresh
|
|
3608
|
+
</button>
|
|
3609
|
+
</div>
|
|
3610
|
+
</div>
|
|
3611
|
+
|
|
3612
|
+
{perf.errorMessage ? (
|
|
3613
|
+
<div className="proteum-profiler__row">
|
|
3614
|
+
<div className="proteum-profiler__rowHeader">
|
|
3615
|
+
<strong>Last perf panel error</strong>
|
|
3616
|
+
</div>
|
|
3617
|
+
<div className="proteum-profiler__mono">{perf.errorMessage}</div>
|
|
3618
|
+
</div>
|
|
3619
|
+
) : null}
|
|
3620
|
+
|
|
3621
|
+
{perf.status === 'loading' && !perf.top && !currentRequest ? (
|
|
3622
|
+
<div className="proteum-profiler__empty">Loading performance data...</div>
|
|
3623
|
+
) : (
|
|
3624
|
+
<>
|
|
3625
|
+
<div className="proteum-profiler__metrics">
|
|
3626
|
+
<SummaryRow
|
|
3627
|
+
label="Window"
|
|
3628
|
+
value={
|
|
3629
|
+
perf.top
|
|
3630
|
+
? `${perf.top.window.label} (${perf.top.window.requestCount}/${perf.top.window.availableRequestCount})`
|
|
3631
|
+
: perf.since
|
|
3632
|
+
}
|
|
3633
|
+
/>
|
|
3634
|
+
<SummaryRow label="Group By" value={perf.groupBy} />
|
|
3635
|
+
<SummaryRow label="Avg" value={perf.top ? formatDuration(perf.top.summary.avgDurationMs) : 'n/a'} />
|
|
3636
|
+
<SummaryRow label="P95" value={perf.top ? formatDuration(perf.top.summary.p95DurationMs) : 'n/a'} />
|
|
3637
|
+
<SummaryRow label="CPU" value={perf.top ? formatDuration(perf.top.summary.avgCpuMs) : 'n/a'} />
|
|
3638
|
+
<SummaryRow label="Heap" value={perf.top ? formatSignedBytes(perf.top.summary.avgHeapDeltaBytes) : 'n/a'} />
|
|
3639
|
+
<SummaryRow
|
|
3640
|
+
label="Current Request"
|
|
3641
|
+
value={currentRequest ? `${currentRequest.requestId} ${formatDuration(currentRequest.totalDurationMs)}` : 'No request'}
|
|
3642
|
+
/>
|
|
3643
|
+
<SummaryRow label="Refreshed" value={perf.lastLoadedAt ? formatTimestamp(perf.lastLoadedAt) : 'Not loaded'} />
|
|
3644
|
+
</div>
|
|
3645
|
+
|
|
3646
|
+
{currentRequest ? (
|
|
3647
|
+
<>
|
|
3648
|
+
<WaterfallChart
|
|
3649
|
+
emptyLabel="No request stages were captured."
|
|
3650
|
+
itemLabel="stage"
|
|
3651
|
+
items={waterfallItems}
|
|
3652
|
+
/>
|
|
3653
|
+
<div className="proteum-profiler__metrics">
|
|
3654
|
+
<SummaryRow label="Route" value={currentRequest.routeLabel} />
|
|
3655
|
+
<SummaryRow label="Controller" value={currentRequest.controllerLabel} />
|
|
3656
|
+
<SummaryRow label="Total" value={formatDuration(currentRequest.totalDurationMs)} />
|
|
3657
|
+
<SummaryRow label="SQL" value={`${currentRequest.sqlCount} / ${formatDuration(currentRequest.sqlDurationMs)}`} />
|
|
3658
|
+
<SummaryRow label="Calls" value={`${currentRequest.callCount} / ${formatDuration(currentRequest.callDurationMs)}`} />
|
|
3659
|
+
<SummaryRow
|
|
3660
|
+
label="Render"
|
|
3661
|
+
value={`${formatDuration(currentRequest.renderDurationMs)} / ${formatBytes(currentRequest.ssrPayloadBytes)}`}
|
|
3662
|
+
/>
|
|
3663
|
+
<SummaryRow label="CPU" value={formatDuration(currentRequest.cpuTotalMs)} />
|
|
3664
|
+
<SummaryRow label="Heap" value={formatSignedBytes(currentRequest.heapDeltaBytes)} />
|
|
3665
|
+
</div>
|
|
3666
|
+
</>
|
|
3667
|
+
) : (
|
|
3668
|
+
<div className="proteum-profiler__empty">No traced request is attached to this session yet.</div>
|
|
3669
|
+
)}
|
|
3670
|
+
|
|
3671
|
+
<div className="proteum-profiler__chartGrid">
|
|
3672
|
+
<ChartSection
|
|
3673
|
+
emptyLabel="No hot-path latency data matched this window."
|
|
3674
|
+
options={topLatencyChart}
|
|
3675
|
+
subtitle={`Compare average and p95 latency across the hottest ${perf.groupBy}s.`}
|
|
3676
|
+
title={`Hot ${perf.groupBy}s`}
|
|
3677
|
+
/>
|
|
3678
|
+
<ChartSection
|
|
3679
|
+
emptyLabel="No breakdown data matched this window."
|
|
3680
|
+
options={topBreakdownChart}
|
|
3681
|
+
subtitle="See whether self time, SQL, external calls, or render work dominates the response."
|
|
3682
|
+
title="Time Breakdown"
|
|
3683
|
+
/>
|
|
3684
|
+
<ChartSection
|
|
3685
|
+
emptyLabel="No compare data matched these windows."
|
|
3686
|
+
options={compareChart}
|
|
3687
|
+
subtitle={`Track p95 regression pressure between ${perf.baseline} and ${perf.target}.`}
|
|
3688
|
+
title="Regression Delta"
|
|
3689
|
+
/>
|
|
3690
|
+
<ChartSection
|
|
3691
|
+
emptyLabel="No memory drift data matched this window."
|
|
3692
|
+
options={memoryChart}
|
|
3693
|
+
subtitle="Compare average heap growth, peak heap growth, and average RSS drift per group."
|
|
3694
|
+
title="Memory Drift"
|
|
3695
|
+
/>
|
|
3696
|
+
</div>
|
|
3697
|
+
|
|
3698
|
+
<SimpleSection empty="No hot calls captured for this request." rows={callRows} title="Current Request Calls" />
|
|
3699
|
+
<SimpleSection empty="No hot SQL captured for this request." rows={sqlRows} title="Current Request SQL" />
|
|
3700
|
+
<SimpleSection empty="No perf rollups matched this window." rows={topRows} title={`Hot ${perf.groupBy}s`} />
|
|
3701
|
+
<SimpleSection empty="No compare deltas matched these windows." rows={compareRows} title="Compare" />
|
|
3702
|
+
<SimpleSection empty="No memory drift data matched this window." rows={memoryRows} title="Memory" />
|
|
3703
|
+
</>
|
|
3704
|
+
)}
|
|
3705
|
+
</div>
|
|
3706
|
+
);
|
|
3707
|
+
}
|
|
3708
|
+
|
|
2139
3709
|
if (panel === 'auth') {
|
|
2140
3710
|
return <AuthPanel session={session} />;
|
|
2141
3711
|
}
|
|
2142
3712
|
|
|
2143
3713
|
if (panel === 'routing') {
|
|
3714
|
+
const routingEvents = findTraceEvents(primaryTrace, [
|
|
3715
|
+
'resolve.start',
|
|
3716
|
+
'resolve.controller-route',
|
|
3717
|
+
'resolve.route-match',
|
|
3718
|
+
'resolve.route-skip',
|
|
3719
|
+
'resolve.routes-evaluated',
|
|
3720
|
+
'resolve.not-found',
|
|
3721
|
+
]);
|
|
3722
|
+
const routingFlowChart = buildHorizontalBarChartOptions({
|
|
3723
|
+
color: profilerChartTheme.blue,
|
|
3724
|
+
entries: buildCountEntries(routingEvents.map((event) => event.type.replace(/^resolve\./, ''))),
|
|
3725
|
+
title: 'Resolve event flow',
|
|
3726
|
+
valueUnit: 'Events',
|
|
3727
|
+
});
|
|
3728
|
+
const routingTimelineChart =
|
|
3729
|
+
routingEvents.length === 0
|
|
3730
|
+
? undefined
|
|
3731
|
+
: createProfilerColumnChartOptions({
|
|
3732
|
+
categories: routingEvents.map((event) => truncate(event.type.replace(/^resolve\./, ''), 22)),
|
|
3733
|
+
colors: [profilerChartTheme.indigo],
|
|
3734
|
+
height: 300,
|
|
3735
|
+
series: [{ data: routingEvents.map((event) => toRoundedNumber(event.elapsedMs)), name: 'Elapsed ms' }],
|
|
3736
|
+
title: 'Resolve milestone timing',
|
|
3737
|
+
valueUnit: 'Milliseconds',
|
|
3738
|
+
});
|
|
3739
|
+
const routingDecisionChart = buildHorizontalBarChartOptions({
|
|
3740
|
+
color: profilerChartTheme.amber,
|
|
3741
|
+
entries: buildCountEntries(
|
|
3742
|
+
routingEvents.map((event) => {
|
|
3743
|
+
if (event.type === 'resolve.route-skip') {
|
|
3744
|
+
return `skip:${readString(event.details.reason) || readString(event.details.code) || 'unknown'}`;
|
|
3745
|
+
}
|
|
3746
|
+
|
|
3747
|
+
if (event.type === 'resolve.route-match') {
|
|
3748
|
+
return `match:${readString(event.details.path) || readString(event.details.routePath) || 'route'}`;
|
|
3749
|
+
}
|
|
3750
|
+
|
|
3751
|
+
if (event.type === 'resolve.controller-route') {
|
|
3752
|
+
return `controller:${readString(event.details.httpPath) || readString(event.details.routePath) || 'route'}`;
|
|
3753
|
+
}
|
|
3754
|
+
|
|
3755
|
+
return event.type.replace(/^resolve\./, '');
|
|
3756
|
+
}),
|
|
3757
|
+
),
|
|
3758
|
+
title: 'Resolve decisions',
|
|
3759
|
+
valueUnit: 'Events',
|
|
3760
|
+
});
|
|
3761
|
+
|
|
2144
3762
|
return (
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
3763
|
+
<>
|
|
3764
|
+
<div className="proteum-profiler__chartGrid">
|
|
3765
|
+
<ChartSection
|
|
3766
|
+
emptyLabel="No routing events were captured yet."
|
|
3767
|
+
options={routingFlowChart}
|
|
3768
|
+
subtitle="Summarize the current request’s resolve flow without reading each trace event."
|
|
3769
|
+
title="Resolve Flow"
|
|
3770
|
+
/>
|
|
3771
|
+
<ChartSection
|
|
3772
|
+
emptyLabel="No routing milestones were captured yet."
|
|
3773
|
+
options={routingTimelineChart}
|
|
3774
|
+
subtitle="Order the resolve milestones by elapsed time to spot slow route decisions."
|
|
3775
|
+
title="Resolve Timing"
|
|
3776
|
+
/>
|
|
3777
|
+
<ChartSection
|
|
3778
|
+
emptyLabel="No routing decisions were captured yet."
|
|
3779
|
+
options={routingDecisionChart}
|
|
3780
|
+
subtitle="Highlight skip reasons, matched routes, and controller routing outcomes."
|
|
3781
|
+
title="Decisions"
|
|
3782
|
+
/>
|
|
3783
|
+
</div>
|
|
3784
|
+
|
|
3785
|
+
<SimpleSection
|
|
3786
|
+
empty="No routing data captured yet."
|
|
3787
|
+
rows={routingEvents.map((event) => ({
|
|
3788
|
+
key: `${event.index}:${event.type}`,
|
|
3789
|
+
title: event.type,
|
|
3790
|
+
value: Object.entries(event.details)
|
|
3791
|
+
.map(([key, value]) => `${key}=${renderSummaryValue(value)}`)
|
|
3792
|
+
.join(' '),
|
|
3793
|
+
}))}
|
|
3794
|
+
showTitle={false}
|
|
3795
|
+
title="Routing"
|
|
3796
|
+
/>
|
|
3797
|
+
</>
|
|
2163
3798
|
);
|
|
2164
3799
|
}
|
|
2165
3800
|
|
|
2166
3801
|
if (panel === 'controller') {
|
|
3802
|
+
const controllerEvents = findTraceEvents(primaryTrace, ['controller.start', 'controller.result', 'setup.options', 'context.create']);
|
|
3803
|
+
const controllerFlowChart = buildHorizontalBarChartOptions({
|
|
3804
|
+
color: profilerChartTheme.indigo,
|
|
3805
|
+
entries: buildCountEntries(controllerEvents.map((event) => event.type.replace(/\./g, ' '))),
|
|
3806
|
+
title: 'Controller lifecycle',
|
|
3807
|
+
valueUnit: 'Events',
|
|
3808
|
+
});
|
|
3809
|
+
const controllerTimelineChart =
|
|
3810
|
+
controllerEvents.length === 0
|
|
3811
|
+
? undefined
|
|
3812
|
+
: createProfilerColumnChartOptions({
|
|
3813
|
+
categories: controllerEvents.map((event) => truncate(event.type.replace(/\./g, ' '), 22)),
|
|
3814
|
+
colors: [profilerChartTheme.teal],
|
|
3815
|
+
height: 300,
|
|
3816
|
+
series: [{ data: controllerEvents.map((event) => toRoundedNumber(event.elapsedMs)), name: 'Elapsed ms' }],
|
|
3817
|
+
title: 'Lifecycle timing',
|
|
3818
|
+
valueUnit: 'Milliseconds',
|
|
3819
|
+
});
|
|
3820
|
+
const controllerDetailChart = buildHorizontalBarChartOptions({
|
|
3821
|
+
color: profilerChartTheme.cyan,
|
|
3822
|
+
entries: buildCountEntries(
|
|
3823
|
+
controllerEvents.flatMap((event) => Object.keys(event.details).map((key) => `${event.type}:${key}`)),
|
|
3824
|
+
),
|
|
3825
|
+
title: 'Detail coverage',
|
|
3826
|
+
valueUnit: 'Fields',
|
|
3827
|
+
});
|
|
3828
|
+
|
|
2167
3829
|
return (
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
3830
|
+
<>
|
|
3831
|
+
<div className="proteum-profiler__chartGrid">
|
|
3832
|
+
<ChartSection
|
|
3833
|
+
emptyLabel="No controller events were captured yet."
|
|
3834
|
+
options={controllerFlowChart}
|
|
3835
|
+
subtitle="See which controller phases are present in the traced request."
|
|
3836
|
+
title="Lifecycle"
|
|
3837
|
+
/>
|
|
3838
|
+
<ChartSection
|
|
3839
|
+
emptyLabel="No controller timing data was captured yet."
|
|
3840
|
+
options={controllerTimelineChart}
|
|
3841
|
+
subtitle="Compare elapsed time at each controller milestone."
|
|
3842
|
+
title="Timing"
|
|
3843
|
+
/>
|
|
3844
|
+
<ChartSection
|
|
3845
|
+
emptyLabel="No controller event details were captured yet."
|
|
3846
|
+
options={controllerDetailChart}
|
|
3847
|
+
subtitle="Highlight which detail fields are most common across controller events."
|
|
3848
|
+
title="Detail Keys"
|
|
3849
|
+
/>
|
|
3850
|
+
</div>
|
|
3851
|
+
|
|
3852
|
+
<SimpleSection
|
|
3853
|
+
empty="No controller data captured yet."
|
|
3854
|
+
rows={controllerEvents.map((event) => ({
|
|
2172
3855
|
key: `${event.index}:${event.type}`,
|
|
2173
3856
|
title: event.type,
|
|
2174
3857
|
value: Object.entries(event.details)
|
|
2175
3858
|
.map(([key, value]) => `${key}=${renderSummaryValue(value)}`)
|
|
2176
3859
|
.join(' '),
|
|
2177
|
-
})
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
3860
|
+
}))}
|
|
3861
|
+
showTitle={false}
|
|
3862
|
+
title="Controller"
|
|
3863
|
+
/>
|
|
3864
|
+
</>
|
|
2182
3865
|
);
|
|
2183
3866
|
}
|
|
2184
3867
|
|
|
2185
3868
|
if (panel === 'ssr') {
|
|
3869
|
+
const ssrEvents = findTraceEvents(primaryTrace, ['page.data', 'ssr.payload', 'render.start', 'render.end']);
|
|
3870
|
+
const recentSsrRows = state.sessions
|
|
3871
|
+
.slice(-10)
|
|
3872
|
+
.map((candidate) => ({ session: candidate, summary: getSummary(candidate) }))
|
|
3873
|
+
.filter(({ summary: candidateSummary }) => candidateSummary.renderMs !== undefined || candidateSummary.ssrPayloadBytes !== undefined);
|
|
3874
|
+
const ssrScatterChart = buildScatterChartOptions({
|
|
3875
|
+
color: profilerChartTheme.amber,
|
|
3876
|
+
points: recentSsrRows.map(({ summary: candidateSummary }) => ({
|
|
3877
|
+
x: toRoundedNumber(candidateSummary.renderMs),
|
|
3878
|
+
y: toKilobytes(candidateSummary.ssrPayloadBytes, 2),
|
|
3879
|
+
})),
|
|
3880
|
+
seriesName: 'Session',
|
|
3881
|
+
title: 'Render vs payload',
|
|
3882
|
+
xaxisTitle: 'Render ms',
|
|
3883
|
+
yaxisTitle: 'Payload KB',
|
|
3884
|
+
});
|
|
3885
|
+
const payloadChart = buildHorizontalBarChartOptions({
|
|
3886
|
+
color: profilerChartTheme.teal,
|
|
3887
|
+
entries: buildTopEntries(
|
|
3888
|
+
recentSsrRows.map(({ summary: candidateSummary }) => ({
|
|
3889
|
+
label: candidateSummary.routeLabel,
|
|
3890
|
+
value: toKilobytes(candidateSummary.ssrPayloadBytes, 2),
|
|
3891
|
+
})),
|
|
3892
|
+
8,
|
|
3893
|
+
),
|
|
3894
|
+
title: 'Largest payloads',
|
|
3895
|
+
valueUnit: 'KB',
|
|
3896
|
+
});
|
|
3897
|
+
const renderTrendChart = buildLineChartOptions({
|
|
3898
|
+
color: profilerChartTheme.orange,
|
|
3899
|
+
entries: recentSsrRows.map(({ session: candidate, summary: candidateSummary }) => ({
|
|
3900
|
+
label: getSessionChartLabel(candidate, candidateSummary),
|
|
3901
|
+
value: candidateSummary.renderMs || 0,
|
|
3902
|
+
})),
|
|
3903
|
+
title: 'Recent render time',
|
|
3904
|
+
valueUnit: 'Milliseconds',
|
|
3905
|
+
});
|
|
3906
|
+
|
|
2186
3907
|
return (
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
3908
|
+
<>
|
|
3909
|
+
<div className="proteum-profiler__chartGrid">
|
|
3910
|
+
<ChartSection
|
|
3911
|
+
emptyLabel="No SSR timing and payload data was captured yet."
|
|
3912
|
+
options={ssrScatterChart}
|
|
3913
|
+
subtitle="Correlate render cost with payload size across recent navigations."
|
|
3914
|
+
title="Render vs Payload"
|
|
3915
|
+
/>
|
|
3916
|
+
<ChartSection
|
|
3917
|
+
emptyLabel="No SSR payload sizes were captured yet."
|
|
3918
|
+
options={payloadChart}
|
|
3919
|
+
subtitle="Rank the largest payload producers from the recent session window."
|
|
3920
|
+
title="Payload Pressure"
|
|
3921
|
+
/>
|
|
3922
|
+
<ChartSection
|
|
3923
|
+
emptyLabel="No SSR render timings were captured yet."
|
|
3924
|
+
options={renderTrendChart}
|
|
3925
|
+
subtitle="Track render time across the last few SSR sessions."
|
|
3926
|
+
title="Render Trend"
|
|
3927
|
+
/>
|
|
3928
|
+
</div>
|
|
3929
|
+
|
|
3930
|
+
<SimpleSection
|
|
3931
|
+
empty="No SSR data captured for this session."
|
|
3932
|
+
rows={ssrEvents.map((event) => ({
|
|
3933
|
+
key: `${event.index}:${event.type}`,
|
|
3934
|
+
title: event.type,
|
|
3935
|
+
value: Object.entries(event.details)
|
|
3936
|
+
.map(([key, value]) => `${key}=${renderSummaryValue(value)}`)
|
|
3937
|
+
.join(' '),
|
|
3938
|
+
}))}
|
|
3939
|
+
showTitle={false}
|
|
3940
|
+
title="SSR"
|
|
3941
|
+
/>
|
|
3942
|
+
</>
|
|
2199
3943
|
);
|
|
2200
3944
|
}
|
|
2201
3945
|
|
|
@@ -2203,6 +3947,147 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
2203
3947
|
return <ApiPanel session={session} />;
|
|
2204
3948
|
}
|
|
2205
3949
|
|
|
3950
|
+
if (panel === 'sql') {
|
|
3951
|
+
return <SqlPanel session={session} />;
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3954
|
+
if (panel === 'diagnose') {
|
|
3955
|
+
const diagnose = state.diagnose;
|
|
3956
|
+
const response = diagnose.response;
|
|
3957
|
+
const suspectScoreChart = buildHorizontalBarChartOptions({
|
|
3958
|
+
color: profilerChartTheme.red,
|
|
3959
|
+
entries:
|
|
3960
|
+
response?.suspects.map((suspect) => ({
|
|
3961
|
+
label: suspect.label,
|
|
3962
|
+
value: suspect.score,
|
|
3963
|
+
})) || [],
|
|
3964
|
+
title: 'Suspect scoring',
|
|
3965
|
+
valueUnit: 'Score',
|
|
3966
|
+
});
|
|
3967
|
+
const ownerScoreChart = buildHorizontalBarChartOptions({
|
|
3968
|
+
color: profilerChartTheme.indigo,
|
|
3969
|
+
entries:
|
|
3970
|
+
response?.owner.matches.map((match) => ({
|
|
3971
|
+
label: `[${match.kind}] ${match.label}`,
|
|
3972
|
+
value: match.score,
|
|
3973
|
+
})) || [],
|
|
3974
|
+
title: 'Owner confidence',
|
|
3975
|
+
valueUnit: 'Score',
|
|
3976
|
+
});
|
|
3977
|
+
const diagnoseSeverityChart =
|
|
3978
|
+
response === undefined
|
|
3979
|
+
? undefined
|
|
3980
|
+
: createProfilerColumnChartOptions({
|
|
3981
|
+
categories: ['Errors', 'Warnings'],
|
|
3982
|
+
colors: [profilerChartTheme.red, profilerChartTheme.amber],
|
|
3983
|
+
height: 300,
|
|
3984
|
+
series: [
|
|
3985
|
+
{
|
|
3986
|
+
data: [response.doctor.summary.errors, response.doctor.summary.warnings],
|
|
3987
|
+
name: 'Doctor',
|
|
3988
|
+
},
|
|
3989
|
+
{
|
|
3990
|
+
data: [response.contracts.summary.errors, response.contracts.summary.warnings],
|
|
3991
|
+
name: 'Contracts',
|
|
3992
|
+
},
|
|
3993
|
+
],
|
|
3994
|
+
title: 'Diagnostic severity',
|
|
3995
|
+
valueUnit: 'Count',
|
|
3996
|
+
});
|
|
3997
|
+
const suspectRows =
|
|
3998
|
+
response?.suspects.map((suspect, index) => ({
|
|
3999
|
+
key: `suspect:${index}`,
|
|
4000
|
+
title: `${suspect.score} · ${suspect.label}`,
|
|
4001
|
+
value: `${suspect.filepath}${formatManifestLocation(suspect.line, suspect.column)} reasons=${suspect.reasons.join(', ')}`,
|
|
4002
|
+
})) || [];
|
|
4003
|
+
const ownerRows =
|
|
4004
|
+
response?.owner.matches.map((match, index) => ({
|
|
4005
|
+
key: `owner:${index}`,
|
|
4006
|
+
title: `[${match.kind}] ${match.label}`,
|
|
4007
|
+
value: `score=${match.score} source=${formatOwnerSource(match)} matchedOn=${match.matchedOn.join(', ') || 'n/a'}`,
|
|
4008
|
+
})) || [];
|
|
4009
|
+
const contractRows =
|
|
4010
|
+
response?.contracts.diagnostics.map((diagnostic, index) => ({
|
|
4011
|
+
key: `contract:${diagnostic.code}:${index}`,
|
|
4012
|
+
title: `[${diagnostic.level}] ${diagnostic.code}`,
|
|
4013
|
+
value: `${diagnostic.message} source=${diagnostic.filepath}${formatManifestLocation(
|
|
4014
|
+
diagnostic.sourceLocation?.line,
|
|
4015
|
+
diagnostic.sourceLocation?.column,
|
|
4016
|
+
)}`,
|
|
4017
|
+
})) || [];
|
|
4018
|
+
const logRows =
|
|
4019
|
+
response?.serverLogs.logs.map((entry, index) => ({
|
|
4020
|
+
key: `log:${index}`,
|
|
4021
|
+
title: `[${entry.level}] ${formatTimestamp(entry.time)}`,
|
|
4022
|
+
value: truncate(entry.text, 220),
|
|
4023
|
+
})) || [];
|
|
4024
|
+
|
|
4025
|
+
return (
|
|
4026
|
+
<div className="proteum-profiler__section">
|
|
4027
|
+
<div className="proteum-profiler__sectionHeader">
|
|
4028
|
+
<div className="proteum-profiler__sectionTitle">Diagnose</div>
|
|
4029
|
+
<div className="proteum-profiler__actions">
|
|
4030
|
+
<button className="proteum-profiler__pill" onClick={() => void profilerRuntime.refreshDiagnose(session.id)} type="button">
|
|
4031
|
+
Refresh
|
|
4032
|
+
</button>
|
|
4033
|
+
</div>
|
|
4034
|
+
</div>
|
|
4035
|
+
|
|
4036
|
+
{diagnose.errorMessage ? (
|
|
4037
|
+
<div className="proteum-profiler__row">
|
|
4038
|
+
<div className="proteum-profiler__rowHeader">
|
|
4039
|
+
<strong>Last diagnose panel error</strong>
|
|
4040
|
+
</div>
|
|
4041
|
+
<div className="proteum-profiler__mono">{diagnose.errorMessage}</div>
|
|
4042
|
+
</div>
|
|
4043
|
+
) : null}
|
|
4044
|
+
|
|
4045
|
+
{diagnose.status === 'loading' && !response ? (
|
|
4046
|
+
<div className="proteum-profiler__empty">Loading diagnose data...</div>
|
|
4047
|
+
) : !response ? (
|
|
4048
|
+
<div className="proteum-profiler__empty">No diagnose data is available for this session yet.</div>
|
|
4049
|
+
) : (
|
|
4050
|
+
<>
|
|
4051
|
+
<div className="proteum-profiler__metrics">
|
|
4052
|
+
<SummaryRow label="Query" value={response.query} />
|
|
4053
|
+
<SummaryRow label="Request" value={response.request ? summarizeTraceForDiagnose(response.request) : 'No trace'} />
|
|
4054
|
+
<SummaryRow label="Doctor" value={`${response.doctor.summary.errors} errors / ${response.doctor.summary.warnings} warnings`} />
|
|
4055
|
+
<SummaryRow label="Contracts" value={`${response.contracts.summary.errors} errors / ${response.contracts.summary.warnings} warnings`} />
|
|
4056
|
+
<SummaryRow
|
|
4057
|
+
label="Refreshed"
|
|
4058
|
+
value={diagnose.lastLoadedAt ? formatTimestamp(diagnose.lastLoadedAt) : 'Not loaded'}
|
|
4059
|
+
/>
|
|
4060
|
+
</div>
|
|
4061
|
+
<div className="proteum-profiler__chartGrid">
|
|
4062
|
+
<ChartSection
|
|
4063
|
+
emptyLabel="No suspect scores were returned for this diagnose run."
|
|
4064
|
+
options={suspectScoreChart}
|
|
4065
|
+
subtitle="Rank the files Proteum currently believes are most likely involved."
|
|
4066
|
+
title="Suspects"
|
|
4067
|
+
/>
|
|
4068
|
+
<ChartSection
|
|
4069
|
+
emptyLabel="No owner matches were returned for this diagnose run."
|
|
4070
|
+
options={ownerScoreChart}
|
|
4071
|
+
subtitle="Compare owner candidates and their confidence scores."
|
|
4072
|
+
title="Owner Matches"
|
|
4073
|
+
/>
|
|
4074
|
+
<ChartSection
|
|
4075
|
+
emptyLabel="No diagnose severity data was returned for this run."
|
|
4076
|
+
options={diagnoseSeverityChart}
|
|
4077
|
+
subtitle="Compare doctor diagnostics against contract diagnostics in one view."
|
|
4078
|
+
title="Severity"
|
|
4079
|
+
/>
|
|
4080
|
+
</div>
|
|
4081
|
+
<SimpleSection empty="No likely suspect files were found." rows={suspectRows} title="Suspects" />
|
|
4082
|
+
<SimpleSection empty="No owner matches were found." rows={ownerRows} title="Owner Matches" />
|
|
4083
|
+
<SimpleSection empty="No contract diagnostics were found." rows={contractRows} title="Contracts" />
|
|
4084
|
+
<SimpleSection empty="No recent server logs were captured." rows={logRows} title="Server Logs" />
|
|
4085
|
+
</>
|
|
4086
|
+
)}
|
|
4087
|
+
</div>
|
|
4088
|
+
);
|
|
4089
|
+
}
|
|
4090
|
+
|
|
2206
4091
|
if (panel === 'explain') {
|
|
2207
4092
|
const explain = state.explain;
|
|
2208
4093
|
const blocks = explain.manifest
|
|
@@ -2211,6 +4096,74 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
2211
4096
|
...buildExplainBlocks(explain.manifest, [...explainSectionNames]),
|
|
2212
4097
|
]
|
|
2213
4098
|
: [];
|
|
4099
|
+
const manifest = explain.manifest;
|
|
4100
|
+
const structureChart = manifest
|
|
4101
|
+
? buildColumnChartOptions({
|
|
4102
|
+
colors: [profilerChartTheme.blue],
|
|
4103
|
+
entries: [
|
|
4104
|
+
{ label: 'app services', value: manifest.services.app.length },
|
|
4105
|
+
{ label: 'router plugins', value: manifest.services.routerPlugins.length },
|
|
4106
|
+
{ label: 'controllers', value: manifest.controllers.length },
|
|
4107
|
+
{ label: 'commands', value: manifest.commands.length },
|
|
4108
|
+
{ label: 'client routes', value: manifest.routes.client.length },
|
|
4109
|
+
{ label: 'server routes', value: manifest.routes.server.length },
|
|
4110
|
+
{ label: 'layouts', value: manifest.layouts.length },
|
|
4111
|
+
{ label: 'diagnostics', value: manifest.diagnostics.length },
|
|
4112
|
+
],
|
|
4113
|
+
title: 'Manifest structure',
|
|
4114
|
+
valueUnit: 'Count',
|
|
4115
|
+
})
|
|
4116
|
+
: undefined;
|
|
4117
|
+
const manifestScopeChart = manifest
|
|
4118
|
+
? buildColumnChartOptions({
|
|
4119
|
+
colors: [profilerChartTheme.indigo],
|
|
4120
|
+
entries: [
|
|
4121
|
+
{
|
|
4122
|
+
label: 'app',
|
|
4123
|
+
value: [
|
|
4124
|
+
...manifest.services.app,
|
|
4125
|
+
...manifest.services.routerPlugins,
|
|
4126
|
+
...manifest.controllers,
|
|
4127
|
+
...manifest.commands,
|
|
4128
|
+
...manifest.routes.client,
|
|
4129
|
+
...manifest.routes.server,
|
|
4130
|
+
...manifest.layouts,
|
|
4131
|
+
].filter((entry) => entry.scope === 'app').length,
|
|
4132
|
+
},
|
|
4133
|
+
{
|
|
4134
|
+
label: 'framework',
|
|
4135
|
+
value: [
|
|
4136
|
+
...manifest.services.app,
|
|
4137
|
+
...manifest.services.routerPlugins,
|
|
4138
|
+
...manifest.controllers,
|
|
4139
|
+
...manifest.commands,
|
|
4140
|
+
...manifest.routes.client,
|
|
4141
|
+
...manifest.routes.server,
|
|
4142
|
+
...manifest.layouts,
|
|
4143
|
+
].filter((entry) => entry.scope === 'framework').length,
|
|
4144
|
+
},
|
|
4145
|
+
],
|
|
4146
|
+
title: 'Scope split',
|
|
4147
|
+
valueUnit: 'Entries',
|
|
4148
|
+
})
|
|
4149
|
+
: undefined;
|
|
4150
|
+
const envReadinessChart = manifest
|
|
4151
|
+
? buildColumnChartOptions({
|
|
4152
|
+
colors: [profilerChartTheme.green, profilerChartTheme.red],
|
|
4153
|
+
entries: [
|
|
4154
|
+
{
|
|
4155
|
+
label: 'provided',
|
|
4156
|
+
value: manifest.env.requiredVariables.filter((variable) => variable.provided).length,
|
|
4157
|
+
},
|
|
4158
|
+
{
|
|
4159
|
+
label: 'missing',
|
|
4160
|
+
value: manifest.env.requiredVariables.filter((variable) => !variable.provided).length,
|
|
4161
|
+
},
|
|
4162
|
+
],
|
|
4163
|
+
title: 'Env readiness',
|
|
4164
|
+
valueUnit: 'Variables',
|
|
4165
|
+
})
|
|
4166
|
+
: undefined;
|
|
2214
4167
|
|
|
2215
4168
|
return (
|
|
2216
4169
|
<div className="proteum-profiler__section">
|
|
@@ -2238,6 +4191,27 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
2238
4191
|
<div className="proteum-profiler__empty">No explain manifest is available.</div>
|
|
2239
4192
|
) : (
|
|
2240
4193
|
<>
|
|
4194
|
+
<div className="proteum-profiler__chartGrid">
|
|
4195
|
+
<ChartSection
|
|
4196
|
+
emptyLabel="No manifest structure data is available."
|
|
4197
|
+
options={structureChart}
|
|
4198
|
+
subtitle="Summarize the main object counts in the current Proteum manifest."
|
|
4199
|
+
title="Structure"
|
|
4200
|
+
/>
|
|
4201
|
+
<ChartSection
|
|
4202
|
+
emptyLabel="No manifest scope data is available."
|
|
4203
|
+
options={manifestScopeChart}
|
|
4204
|
+
subtitle="Show how much of the current manifest comes from app code vs framework code."
|
|
4205
|
+
title="Scope Split"
|
|
4206
|
+
/>
|
|
4207
|
+
<ChartSection
|
|
4208
|
+
emptyLabel="No manifest env readiness data is available."
|
|
4209
|
+
options={envReadinessChart}
|
|
4210
|
+
subtitle="Check required variable coverage before digging through the raw manifest."
|
|
4211
|
+
title="Env Ready"
|
|
4212
|
+
/>
|
|
4213
|
+
</div>
|
|
4214
|
+
|
|
2241
4215
|
<div className="proteum-profiler__row">
|
|
2242
4216
|
<div className="proteum-profiler__rowHeader">
|
|
2243
4217
|
<strong>Manifest snapshot</strong>
|
|
@@ -2258,6 +4232,50 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
2258
4232
|
|
|
2259
4233
|
if (panel === 'doctor') {
|
|
2260
4234
|
const doctor = state.doctor;
|
|
4235
|
+
const doctorSeverityChart =
|
|
4236
|
+
doctor.response === undefined
|
|
4237
|
+
? undefined
|
|
4238
|
+
: createProfilerColumnChartOptions({
|
|
4239
|
+
categories: ['Errors', 'Warnings'],
|
|
4240
|
+
colors: [profilerChartTheme.red, profilerChartTheme.amber],
|
|
4241
|
+
height: 300,
|
|
4242
|
+
series: [
|
|
4243
|
+
{
|
|
4244
|
+
data: [doctor.response.summary.errors, doctor.response.summary.warnings],
|
|
4245
|
+
name: 'Doctor',
|
|
4246
|
+
},
|
|
4247
|
+
{
|
|
4248
|
+
data: [
|
|
4249
|
+
doctor.contracts?.summary.errors || 0,
|
|
4250
|
+
doctor.contracts?.summary.warnings || 0,
|
|
4251
|
+
],
|
|
4252
|
+
name: 'Contracts',
|
|
4253
|
+
},
|
|
4254
|
+
],
|
|
4255
|
+
title: 'Severity overview',
|
|
4256
|
+
valueUnit: 'Count',
|
|
4257
|
+
});
|
|
4258
|
+
const doctorCodeChart = buildHorizontalBarChartOptions({
|
|
4259
|
+
color: profilerChartTheme.orange,
|
|
4260
|
+
entries: buildCountEntries(doctor.response?.diagnostics.map((diagnostic) => diagnostic.code) || []),
|
|
4261
|
+
title: 'Diagnostic codes',
|
|
4262
|
+
valueUnit: 'Hits',
|
|
4263
|
+
});
|
|
4264
|
+
const doctorFileChart = buildHorizontalBarChartOptions({
|
|
4265
|
+
color: profilerChartTheme.cyan,
|
|
4266
|
+
entries: buildCountEntries(doctor.response?.diagnostics.map((diagnostic) => diagnostic.filepath) || []),
|
|
4267
|
+
title: 'Hot files',
|
|
4268
|
+
valueUnit: 'Diagnostics',
|
|
4269
|
+
});
|
|
4270
|
+
const contractRows =
|
|
4271
|
+
doctor.contracts?.diagnostics.map((diagnostic, index) => ({
|
|
4272
|
+
key: `contract:${diagnostic.code}:${index}`,
|
|
4273
|
+
title: `[${diagnostic.level}] ${diagnostic.code}`,
|
|
4274
|
+
value: `${diagnostic.message} source=${diagnostic.filepath}${formatManifestLocation(
|
|
4275
|
+
diagnostic.sourceLocation?.line,
|
|
4276
|
+
diagnostic.sourceLocation?.column,
|
|
4277
|
+
)}${diagnostic.relatedFilepaths?.length ? ` related=${diagnostic.relatedFilepaths.join(',')}` : ''}`,
|
|
4278
|
+
})) || [];
|
|
2261
4279
|
const doctorRows =
|
|
2262
4280
|
doctor.response?.diagnostics.map((diagnostic, index) => ({
|
|
2263
4281
|
key: `${diagnostic.code}:${index}`,
|
|
@@ -2298,17 +4316,46 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
2298
4316
|
<div className="proteum-profiler__metrics">
|
|
2299
4317
|
<SummaryRow label="Errors" value={String(doctor.response.summary.errors)} />
|
|
2300
4318
|
<SummaryRow label="Warnings" value={String(doctor.response.summary.warnings)} />
|
|
4319
|
+
<SummaryRow
|
|
4320
|
+
label="Contracts"
|
|
4321
|
+
value={
|
|
4322
|
+
doctor.contracts
|
|
4323
|
+
? `${doctor.contracts.summary.errors} errors / ${doctor.contracts.summary.warnings} warnings`
|
|
4324
|
+
: 'not loaded'
|
|
4325
|
+
}
|
|
4326
|
+
/>
|
|
2301
4327
|
<SummaryRow label="Strict" value={doctor.response.summary.strictFailed ? 'failed' : 'ok'} />
|
|
2302
4328
|
<SummaryRow
|
|
2303
4329
|
label="Refreshed"
|
|
2304
4330
|
value={doctor.lastLoadedAt ? formatTimestamp(doctor.lastLoadedAt) : 'Not loaded'}
|
|
2305
4331
|
/>
|
|
2306
4332
|
</div>
|
|
4333
|
+
<div className="proteum-profiler__chartGrid">
|
|
4334
|
+
<ChartSection
|
|
4335
|
+
emptyLabel="No doctor severity data is available."
|
|
4336
|
+
options={doctorSeverityChart}
|
|
4337
|
+
subtitle="Compare doctor and contract diagnostics across errors and warnings."
|
|
4338
|
+
title="Severity"
|
|
4339
|
+
/>
|
|
4340
|
+
<ChartSection
|
|
4341
|
+
emptyLabel="No diagnostic codes are available."
|
|
4342
|
+
options={doctorCodeChart}
|
|
4343
|
+
subtitle="See which diagnostic families are dominating the current manifest."
|
|
4344
|
+
title="Codes"
|
|
4345
|
+
/>
|
|
4346
|
+
<ChartSection
|
|
4347
|
+
emptyLabel="No diagnostic file hotspots are available."
|
|
4348
|
+
options={doctorFileChart}
|
|
4349
|
+
subtitle="Highlight the files attracting the most diagnostics."
|
|
4350
|
+
title="Files"
|
|
4351
|
+
/>
|
|
4352
|
+
</div>
|
|
2307
4353
|
{doctorBlocks.length > 0 ? (
|
|
2308
4354
|
<TextBlocks blocks={doctorBlocks} />
|
|
2309
4355
|
) : (
|
|
2310
4356
|
<SimpleSection empty="No manifest diagnostics were found." rows={doctorRows} title="Diagnostics" />
|
|
2311
4357
|
)}
|
|
4358
|
+
<SimpleSection empty="No contract diagnostics were found." rows={contractRows} title="Contracts" />
|
|
2312
4359
|
</>
|
|
2313
4360
|
)}
|
|
2314
4361
|
</div>
|
|
@@ -2317,6 +4364,33 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
2317
4364
|
|
|
2318
4365
|
if (panel === 'commands') {
|
|
2319
4366
|
const commandsState = state.commands;
|
|
4367
|
+
const commandScopeChart = buildColumnChartOptions({
|
|
4368
|
+
colors: [profilerChartTheme.indigo],
|
|
4369
|
+
entries: buildCountEntries(commandsState.commands.map((command) => command.scope)),
|
|
4370
|
+
title: 'Command scope split',
|
|
4371
|
+
valueUnit: 'Commands',
|
|
4372
|
+
});
|
|
4373
|
+
const commandDurationChart = buildHorizontalBarChartOptions({
|
|
4374
|
+
color: profilerChartTheme.blue,
|
|
4375
|
+
entries: buildTopEntries(
|
|
4376
|
+
commandsState.commands
|
|
4377
|
+
.map((command) => ({
|
|
4378
|
+
label: command.path,
|
|
4379
|
+
value: commandsState.executions[command.path]?.durationMs || 0,
|
|
4380
|
+
}))
|
|
4381
|
+
.filter((entry) => entry.value > 0),
|
|
4382
|
+
),
|
|
4383
|
+
title: 'Latest execution duration',
|
|
4384
|
+
valueUnit: 'Milliseconds',
|
|
4385
|
+
});
|
|
4386
|
+
const commandStatusChart = buildColumnChartOptions({
|
|
4387
|
+
colors: [profilerChartTheme.green],
|
|
4388
|
+
entries: buildCountEntries(
|
|
4389
|
+
commandsState.commands.map((command) => commandsState.executions[command.path]?.status || 'never-run'),
|
|
4390
|
+
),
|
|
4391
|
+
title: 'Execution status',
|
|
4392
|
+
valueUnit: 'Commands',
|
|
4393
|
+
});
|
|
2320
4394
|
|
|
2321
4395
|
return (
|
|
2322
4396
|
<div className="proteum-profiler__section">
|
|
@@ -2355,65 +4429,88 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
2355
4429
|
) : commandsState.commands.length === 0 ? (
|
|
2356
4430
|
<div className="proteum-profiler__empty">No commands are registered for this app.</div>
|
|
2357
4431
|
) : (
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
4432
|
+
<>
|
|
4433
|
+
<div className="proteum-profiler__chartGrid">
|
|
4434
|
+
<ChartSection
|
|
4435
|
+
emptyLabel="No command scope data is available."
|
|
4436
|
+
options={commandScopeChart}
|
|
4437
|
+
subtitle="Show how the registered dev commands split between app and framework scopes."
|
|
4438
|
+
title="Scope"
|
|
4439
|
+
/>
|
|
4440
|
+
<ChartSection
|
|
4441
|
+
emptyLabel="No command execution durations have been captured yet."
|
|
4442
|
+
options={commandDurationChart}
|
|
4443
|
+
subtitle="Highlight the commands with the slowest latest execution."
|
|
4444
|
+
title="Latest Duration"
|
|
4445
|
+
/>
|
|
4446
|
+
<ChartSection
|
|
4447
|
+
emptyLabel="No command execution statuses have been captured yet."
|
|
4448
|
+
options={commandStatusChart}
|
|
4449
|
+
subtitle="Separate commands that have never run from completed or failed commands."
|
|
4450
|
+
title="Status"
|
|
4451
|
+
/>
|
|
4452
|
+
</div>
|
|
4453
|
+
|
|
4454
|
+
<div className="proteum-profiler__list">
|
|
4455
|
+
{commandsState.commands.map((command: TDevCommandDefinition) => {
|
|
4456
|
+
const execution = commandsState.executions[command.path] as TDevCommandExecution | undefined;
|
|
4457
|
+
return (
|
|
4458
|
+
<div className="proteum-profiler__row" key={command.path}>
|
|
4459
|
+
<div className="proteum-profiler__rowHeader">
|
|
4460
|
+
<strong>{command.path}</strong>
|
|
4461
|
+
<div className="proteum-profiler__actions">
|
|
4462
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">
|
|
4463
|
+
{execution ? formatTimestamp(execution.finishedAt) : 'Never run'}
|
|
4464
|
+
</span>
|
|
4465
|
+
<button
|
|
4466
|
+
className="proteum-profiler__pill"
|
|
4467
|
+
onClick={() => void profilerRuntime.runCommand(command.path)}
|
|
4468
|
+
type="button"
|
|
4469
|
+
>
|
|
4470
|
+
Run now
|
|
4471
|
+
</button>
|
|
4472
|
+
</div>
|
|
4473
|
+
</div>
|
|
4474
|
+
|
|
4475
|
+
<div className="proteum-profiler__tags">
|
|
4476
|
+
<span className="proteum-profiler__tag">{command.className}</span>
|
|
4477
|
+
<span className="proteum-profiler__tag">{command.methodName}</span>
|
|
4478
|
+
<span className="proteum-profiler__tag">{command.scope}</span>
|
|
4479
|
+
{execution ? <span className="proteum-profiler__tag">{execution.status}</span> : null}
|
|
4480
|
+
{execution ? (
|
|
4481
|
+
<span className="proteum-profiler__tag">{formatDuration(execution.durationMs)}</span>
|
|
4482
|
+
) : null}
|
|
4483
|
+
{execution?.errorMessage ? (
|
|
4484
|
+
<span className="proteum-profiler__tag">{truncate(execution.errorMessage, 72)}</span>
|
|
4485
|
+
) : null}
|
|
4486
|
+
</div>
|
|
4487
|
+
|
|
4488
|
+
<div className="proteum-profiler__mono proteum-profiler__muted">
|
|
4489
|
+
source {command.filepath}:{command.sourceLocation.line}:{command.sourceLocation.column}
|
|
4490
|
+
{commandsState.lastLoadedAt
|
|
4491
|
+
? ` | refreshed ${formatTimestamp(commandsState.lastLoadedAt)}`
|
|
4492
|
+
: ''}
|
|
2376
4493
|
</div>
|
|
2377
|
-
</div>
|
|
2378
4494
|
|
|
2379
|
-
<div className="proteum-profiler__tags">
|
|
2380
|
-
<span className="proteum-profiler__tag">{command.className}</span>
|
|
2381
|
-
<span className="proteum-profiler__tag">{command.methodName}</span>
|
|
2382
|
-
<span className="proteum-profiler__tag">{command.scope}</span>
|
|
2383
|
-
{execution ? <span className="proteum-profiler__tag">{execution.status}</span> : null}
|
|
2384
4495
|
{execution ? (
|
|
2385
|
-
<
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
4496
|
+
<div className="proteum-profiler__section">
|
|
4497
|
+
<div className="proteum-profiler__sectionTitle">Last result</div>
|
|
4498
|
+
<JsonCodeBlock
|
|
4499
|
+
value={
|
|
4500
|
+
execution.result?.json !== undefined
|
|
4501
|
+
? formatStructuredValue(execution.result.json)
|
|
4502
|
+
: execution.result
|
|
4503
|
+
? formatStructuredValue(execution.result.summary)
|
|
4504
|
+
: execution.errorMessage || 'undefined'
|
|
4505
|
+
}
|
|
4506
|
+
/>
|
|
4507
|
+
</div>
|
|
2389
4508
|
) : null}
|
|
2390
4509
|
</div>
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
? ` | refreshed ${formatTimestamp(commandsState.lastLoadedAt)}`
|
|
2396
|
-
: ''}
|
|
2397
|
-
</div>
|
|
2398
|
-
|
|
2399
|
-
{execution ? (
|
|
2400
|
-
<div className="proteum-profiler__section">
|
|
2401
|
-
<div className="proteum-profiler__sectionTitle">Last result</div>
|
|
2402
|
-
<JsonCodeBlock
|
|
2403
|
-
value={
|
|
2404
|
-
execution.result?.json !== undefined
|
|
2405
|
-
? formatStructuredValue(execution.result.json)
|
|
2406
|
-
: execution.result
|
|
2407
|
-
? formatStructuredValue(execution.result.summary)
|
|
2408
|
-
: execution.errorMessage || 'undefined'
|
|
2409
|
-
}
|
|
2410
|
-
/>
|
|
2411
|
-
</div>
|
|
2412
|
-
) : null}
|
|
2413
|
-
</div>
|
|
2414
|
-
);
|
|
2415
|
-
})}
|
|
2416
|
-
</div>
|
|
4510
|
+
);
|
|
4511
|
+
})}
|
|
4512
|
+
</div>
|
|
4513
|
+
</>
|
|
2417
4514
|
)}
|
|
2418
4515
|
</div>
|
|
2419
4516
|
);
|
|
@@ -2421,6 +4518,30 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
2421
4518
|
|
|
2422
4519
|
if (panel === 'cron') {
|
|
2423
4520
|
const cron = state.cron;
|
|
4521
|
+
const cronRunCountChart = buildHorizontalBarChartOptions({
|
|
4522
|
+
color: profilerChartTheme.blue,
|
|
4523
|
+
entries: buildTopEntries(cron.tasks.map((task) => ({ label: task.name, value: task.runCount }))),
|
|
4524
|
+
title: 'Run counts',
|
|
4525
|
+
valueUnit: 'Runs',
|
|
4526
|
+
});
|
|
4527
|
+
const cronDurationChart = buildHorizontalBarChartOptions({
|
|
4528
|
+
color: profilerChartTheme.orange,
|
|
4529
|
+
entries: buildTopEntries(
|
|
4530
|
+
cron.tasks
|
|
4531
|
+
.map((task) => ({ label: task.name, value: task.lastRunDurationMs || 0 }))
|
|
4532
|
+
.filter((entry) => entry.value > 0),
|
|
4533
|
+
),
|
|
4534
|
+
title: 'Last run duration',
|
|
4535
|
+
valueUnit: 'Milliseconds',
|
|
4536
|
+
});
|
|
4537
|
+
const cronStatusChart = buildColumnChartOptions({
|
|
4538
|
+
colors: [profilerChartTheme.teal],
|
|
4539
|
+
entries: buildCountEntries(
|
|
4540
|
+
cron.tasks.map((task) => (task.running ? 'running' : task.lastRunStatus || 'never-run')),
|
|
4541
|
+
),
|
|
4542
|
+
title: 'Task status',
|
|
4543
|
+
valueUnit: 'Tasks',
|
|
4544
|
+
});
|
|
2424
4545
|
|
|
2425
4546
|
return (
|
|
2426
4547
|
<div className="proteum-profiler__section">
|
|
@@ -2460,81 +4581,140 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
2460
4581
|
) : cron.tasks.length === 0 ? (
|
|
2461
4582
|
<div className="proteum-profiler__empty">No cron tasks are registered for this app.</div>
|
|
2462
4583
|
) : (
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
<
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
4584
|
+
<>
|
|
4585
|
+
<div className="proteum-profiler__chartGrid">
|
|
4586
|
+
<ChartSection
|
|
4587
|
+
emptyLabel="No cron run counts are available."
|
|
4588
|
+
options={cronRunCountChart}
|
|
4589
|
+
subtitle="Highlight which tasks have been exercised the most in the current dev session."
|
|
4590
|
+
title="Runs"
|
|
4591
|
+
/>
|
|
4592
|
+
<ChartSection
|
|
4593
|
+
emptyLabel="No cron duration data is available."
|
|
4594
|
+
options={cronDurationChart}
|
|
4595
|
+
subtitle="Compare the latest duration of tasks that have been executed."
|
|
4596
|
+
title="Duration"
|
|
4597
|
+
/>
|
|
4598
|
+
<ChartSection
|
|
4599
|
+
emptyLabel="No cron status data is available."
|
|
4600
|
+
options={cronStatusChart}
|
|
4601
|
+
subtitle="Separate currently running, completed, failed, and never-run tasks."
|
|
4602
|
+
title="Status"
|
|
4603
|
+
/>
|
|
4604
|
+
</div>
|
|
4605
|
+
|
|
4606
|
+
<div className="proteum-profiler__list">
|
|
4607
|
+
{cron.tasks.map((task) => (
|
|
4608
|
+
<div className="proteum-profiler__row" key={task.name}>
|
|
4609
|
+
<div className="proteum-profiler__rowHeader">
|
|
4610
|
+
<strong>{task.name}</strong>
|
|
4611
|
+
<div className="proteum-profiler__actions">
|
|
4612
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">
|
|
4613
|
+
{task.running
|
|
4614
|
+
? 'Running...'
|
|
4615
|
+
: task.lastRunFinishedAt
|
|
4616
|
+
? formatTimestamp(task.lastRunFinishedAt)
|
|
4617
|
+
: 'Never run'}
|
|
4618
|
+
</span>
|
|
4619
|
+
<button
|
|
4620
|
+
className="proteum-profiler__pill"
|
|
4621
|
+
disabled={task.running}
|
|
4622
|
+
onClick={() => void profilerRuntime.runCronTask(task.name)}
|
|
4623
|
+
type="button"
|
|
4624
|
+
>
|
|
4625
|
+
{task.running ? 'Running...' : 'Run now'}
|
|
4626
|
+
</button>
|
|
4627
|
+
</div>
|
|
2484
4628
|
</div>
|
|
2485
|
-
</div>
|
|
2486
4629
|
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
4630
|
+
<div className="proteum-profiler__tags">
|
|
4631
|
+
<span className="proteum-profiler__tag">schedule:{truncate(formatCronFrequency(task), 64)}</span>
|
|
4632
|
+
<span className="proteum-profiler__tag">
|
|
4633
|
+
next:{task.nextInvocation ? formatTimestamp(task.nextInvocation) : 'none'}
|
|
4634
|
+
</span>
|
|
4635
|
+
<span className="proteum-profiler__tag">autoexec:{task.autoexec ? 'yes' : 'no'}</span>
|
|
4636
|
+
<span className="proteum-profiler__tag">
|
|
4637
|
+
automatic:{task.automaticExecution ? 'enabled' : 'disabled in dev'}
|
|
4638
|
+
</span>
|
|
4639
|
+
<span className="proteum-profiler__tag">runs:{task.runCount}</span>
|
|
4640
|
+
{task.lastTrigger ? <span className="proteum-profiler__tag">trigger:{task.lastTrigger}</span> : null}
|
|
4641
|
+
{task.lastRunStatus ? <span className="proteum-profiler__tag">{task.lastRunStatus}</span> : null}
|
|
4642
|
+
{task.lastRunDurationMs !== undefined ? (
|
|
4643
|
+
<span className="proteum-profiler__tag">{formatDuration(task.lastRunDurationMs)}</span>
|
|
4644
|
+
) : null}
|
|
4645
|
+
{task.lastErrorMessage ? (
|
|
4646
|
+
<span className="proteum-profiler__tag">{truncate(task.lastErrorMessage, 72)}</span>
|
|
4647
|
+
) : null}
|
|
4648
|
+
</div>
|
|
2506
4649
|
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
4650
|
+
<div className="proteum-profiler__mono proteum-profiler__muted">
|
|
4651
|
+
registered {formatTimestamp(task.registeredAt)}
|
|
4652
|
+
{cron.lastLoadedAt ? ` | refreshed ${formatTimestamp(cron.lastLoadedAt)}` : ''}
|
|
4653
|
+
</div>
|
|
2510
4654
|
</div>
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
4655
|
+
))}
|
|
4656
|
+
</div>
|
|
4657
|
+
</>
|
|
2514
4658
|
)}
|
|
2515
4659
|
</div>
|
|
2516
4660
|
);
|
|
2517
4661
|
}
|
|
2518
4662
|
|
|
2519
|
-
const
|
|
2520
|
-
...session.steps
|
|
4663
|
+
const stepErrors = session.steps
|
|
2521
4664
|
.filter((step) => step.status === 'error')
|
|
2522
|
-
.map((step) => ({ key: step.id, title: step.label, value: step.errorMessage || 'Step failed' }))
|
|
2523
|
-
|
|
4665
|
+
.map((step) => ({ key: step.id, title: step.label, value: step.errorMessage || 'Step failed' }));
|
|
4666
|
+
const traceErrors = session.traces
|
|
2524
4667
|
.filter((trace) => trace.status === 'error')
|
|
2525
|
-
.map((trace) => ({ key: trace.id, title: trace.label, value: trace.errorMessage || 'Request failed' }))
|
|
2526
|
-
|
|
4668
|
+
.map((trace) => ({ key: trace.id, title: trace.label, value: trace.errorMessage || 'Request failed' }));
|
|
4669
|
+
const eventErrors = findTraceEvents(primaryTrace, ['error']).map((event) => ({
|
|
2527
4670
|
key: `${event.index}:error`,
|
|
2528
4671
|
title: event.type,
|
|
2529
4672
|
value: Object.entries(event.details)
|
|
2530
4673
|
.map(([key, value]) => `${key}=${renderSummaryValue(value)}`)
|
|
2531
4674
|
.join(' '),
|
|
2532
|
-
}))
|
|
2533
|
-
];
|
|
4675
|
+
}));
|
|
4676
|
+
const errorRows = [...stepErrors, ...traceErrors, ...eventErrors];
|
|
4677
|
+
const errorSourceChart = buildColumnChartOptions({
|
|
4678
|
+
colors: [profilerChartTheme.red],
|
|
4679
|
+
entries: [
|
|
4680
|
+
{ label: 'steps', value: stepErrors.length },
|
|
4681
|
+
{ label: 'traces', value: traceErrors.length },
|
|
4682
|
+
{ label: 'events', value: eventErrors.length },
|
|
4683
|
+
],
|
|
4684
|
+
title: 'Error sources',
|
|
4685
|
+
valueUnit: 'Errors',
|
|
4686
|
+
});
|
|
4687
|
+
const errorLabelChart = buildHorizontalBarChartOptions({
|
|
4688
|
+
color: profilerChartTheme.orange,
|
|
4689
|
+
entries: buildCountEntries(errorRows.map((row) => row.title)),
|
|
4690
|
+
title: 'Error groups',
|
|
4691
|
+
valueUnit: 'Errors',
|
|
4692
|
+
});
|
|
2534
4693
|
|
|
2535
|
-
return
|
|
4694
|
+
return (
|
|
4695
|
+
<>
|
|
4696
|
+
<div className="proteum-profiler__chartGrid">
|
|
4697
|
+
<ChartSection
|
|
4698
|
+
emptyLabel="No errors were captured for this session."
|
|
4699
|
+
options={errorSourceChart}
|
|
4700
|
+
subtitle="Split captured failures between navigation steps, requests, and trace events."
|
|
4701
|
+
title="Sources"
|
|
4702
|
+
/>
|
|
4703
|
+
<ChartSection
|
|
4704
|
+
emptyLabel="No grouped error labels were captured for this session."
|
|
4705
|
+
options={errorLabelChart}
|
|
4706
|
+
subtitle="Highlight the error families that are repeating in the selected session."
|
|
4707
|
+
title="Groups"
|
|
4708
|
+
/>
|
|
4709
|
+
</div>
|
|
4710
|
+
|
|
4711
|
+
<SimpleSection empty="No errors captured." rows={errorRows} showTitle={false} title="Errors" />
|
|
4712
|
+
</>
|
|
4713
|
+
);
|
|
2536
4714
|
};
|
|
2537
4715
|
|
|
4716
|
+
const splitScrollPanels = new Set<TProfilerPanel>(['timeline', 'auth', 'api', 'sql']);
|
|
4717
|
+
|
|
2538
4718
|
export default function DevProfiler() {
|
|
2539
4719
|
const [state, setState] = React.useState(() => profilerRuntime.getState());
|
|
2540
4720
|
|
|
@@ -2563,6 +4743,7 @@ export default function DevProfiler() {
|
|
|
2563
4743
|
const summary = getSummary(session);
|
|
2564
4744
|
const tone = summary.errorCount > 0 ? 'error' : (summary.totalMs || 0) > 500 ? 'warn' : 'ok';
|
|
2565
4745
|
const primaryTrace = summary.primaryTrace?.trace;
|
|
4746
|
+
const currentPerfRequest = primaryTrace ? buildRequestPerformance(primaryTrace) : undefined;
|
|
2566
4747
|
const minimizedLabel =
|
|
2567
4748
|
session.kind === 'client-navigation'
|
|
2568
4749
|
? session.label
|
|
@@ -2630,7 +4811,13 @@ export default function DevProfiler() {
|
|
|
2630
4811
|
</div>
|
|
2631
4812
|
</div>
|
|
2632
4813
|
|
|
2633
|
-
<div
|
|
4814
|
+
<div
|
|
4815
|
+
className={`proteum-profiler__panelBody ${
|
|
4816
|
+
splitScrollPanels.has(state.activePanel) ? 'proteum-profiler__panelBody--split' : ''
|
|
4817
|
+
}`}
|
|
4818
|
+
>
|
|
4819
|
+
{renderPanel(state.activePanel, session, summary, state)}
|
|
4820
|
+
</div>
|
|
2634
4821
|
</div>
|
|
2635
4822
|
) : null}
|
|
2636
4823
|
|
|
@@ -2667,6 +4854,20 @@ export default function DevProfiler() {
|
|
|
2667
4854
|
onClick={() => profilerRuntime.openPanel('api')}
|
|
2668
4855
|
tone={summary.apiAsyncCount > 0 || summary.apiSyncCount > 0 ? 'ok' : 'warn'}
|
|
2669
4856
|
/>
|
|
4857
|
+
<StatusToken
|
|
4858
|
+
label={`SQL ${summary.sqlCount}`}
|
|
4859
|
+
onClick={() => profilerRuntime.openPanel('sql')}
|
|
4860
|
+
tone={summary.sqlCount > 0 ? 'ok' : 'warn'}
|
|
4861
|
+
/>
|
|
4862
|
+
<StatusToken
|
|
4863
|
+
label={
|
|
4864
|
+
currentPerfRequest
|
|
4865
|
+
? `Perf ${formatDuration(currentPerfRequest.cpuTotalMs)} ${formatSignedBytes(currentPerfRequest.heapDeltaBytes)}`
|
|
4866
|
+
: 'Perf'
|
|
4867
|
+
}
|
|
4868
|
+
onClick={() => profilerRuntime.openPanel('perf')}
|
|
4869
|
+
tone={currentPerfRequest?.heapDeltaBytes && currentPerfRequest.heapDeltaBytes > 0 ? 'warn' : 'ok'}
|
|
4870
|
+
/>
|
|
2670
4871
|
{summary.errorCount > 0 ? (
|
|
2671
4872
|
<StatusToken
|
|
2672
4873
|
label={`${summary.errorCount} error${summary.errorCount === 1 ? '' : 's'}`}
|
|
@@ -2674,6 +4875,11 @@ export default function DevProfiler() {
|
|
|
2674
4875
|
tone="error"
|
|
2675
4876
|
/>
|
|
2676
4877
|
) : null}
|
|
4878
|
+
<StatusToken
|
|
4879
|
+
label="Diagnose"
|
|
4880
|
+
onClick={() => profilerRuntime.openPanel('diagnose')}
|
|
4881
|
+
tone={summary.errorCount > 0 ? 'error' : 'warn'}
|
|
4882
|
+
/>
|
|
2677
4883
|
<div className="proteum-profiler__spacer" />
|
|
2678
4884
|
<button className="proteum-profiler__token" onClick={() => profilerRuntime.setUiState('pinned-handle')} type="button">
|
|
2679
4885
|
Hide
|