proteum 2.1.2 → 2.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +22 -14
- package/README.md +112 -17
- package/agents/project/AGENTS.md +188 -25
- package/agents/project/CODING_STYLE.md +1 -0
- package/agents/project/client/AGENTS.md +13 -8
- package/agents/project/client/pages/AGENTS.md +17 -9
- package/agents/project/diagnostics.md +52 -0
- package/agents/project/optimizations.md +48 -0
- package/agents/project/server/routes/AGENTS.md +9 -6
- package/agents/project/server/services/AGENTS.md +10 -6
- package/agents/project/tests/AGENTS.md +11 -5
- package/cli/app/config.ts +13 -14
- package/cli/app/index.ts +58 -0
- package/cli/commands/command.ts +8 -0
- package/cli/commands/connect.ts +45 -0
- package/cli/commands/dev.ts +26 -11
- package/cli/commands/diagnose.ts +286 -0
- package/cli/commands/doctor.ts +18 -5
- package/cli/commands/explain.ts +25 -0
- package/cli/commands/perf.ts +243 -0
- package/cli/commands/session.ts +254 -0
- package/cli/commands/sessionLocalRunner.js +188 -0
- package/cli/commands/trace.ts +17 -1
- package/cli/commands/verify.ts +281 -0
- package/cli/compiler/artifacts/connectedProjects.ts +453 -0
- package/cli/compiler/artifacts/controllers.ts +198 -49
- package/cli/compiler/artifacts/discovery.ts +0 -34
- package/cli/compiler/artifacts/manifest.ts +90 -6
- package/cli/compiler/artifacts/routing.ts +2 -2
- package/cli/compiler/artifacts/services.ts +277 -130
- package/cli/compiler/client/index.ts +3 -0
- package/cli/compiler/common/files/style.ts +52 -0
- package/cli/compiler/common/generatedRouteModules.ts +34 -5
- package/cli/compiler/common/scripts.ts +11 -5
- package/cli/compiler/index.ts +2 -1
- package/cli/compiler/server/index.ts +3 -0
- package/cli/presentation/commands.ts +136 -7
- package/cli/presentation/devSession.ts +32 -7
- package/cli/runtime/commands.ts +193 -6
- package/cli/scaffold/index.ts +14 -25
- package/cli/scaffold/templates.ts +41 -27
- package/cli/utils/agents.ts +4 -2
- package/cli/utils/keyboard.ts +8 -0
- package/client/dev/profiler/ApexChart.tsx +66 -0
- package/client/dev/profiler/index.tsx +2798 -417
- package/client/dev/profiler/runtime.noop.ts +12 -0
- package/client/dev/profiler/runtime.ts +195 -4
- package/client/services/router/request/api.ts +6 -1
- package/common/applicationConfig.ts +173 -0
- package/common/applicationConfigLoader.ts +102 -0
- package/common/connectedProjects.ts +113 -0
- package/common/dev/connect.ts +267 -0
- package/common/dev/console.ts +31 -0
- package/common/dev/contractsDoctor.ts +128 -0
- package/common/dev/diagnostics.ts +59 -15
- package/common/dev/inspection.ts +491 -0
- package/common/dev/performance.ts +809 -0
- package/common/dev/profiler.ts +3 -0
- package/common/dev/proteumManifest.ts +31 -6
- package/common/dev/requestTrace.ts +56 -1
- package/common/dev/session.ts +24 -0
- package/common/env/proteumEnv.ts +176 -50
- package/common/router/index.ts +1 -0
- package/common/router/request/api.ts +2 -0
- package/config.ts +5 -0
- package/docs/dev-commands.md +5 -1
- package/docs/dev-sessions.md +90 -0
- package/docs/diagnostics.md +74 -11
- package/docs/request-tracing.md +50 -3
- package/package.json +1 -1
- package/server/app/container/config.ts +16 -87
- package/server/app/container/console/index.ts +42 -8
- package/server/app/container/index.ts +3 -1
- package/server/app/container/trace/index.ts +153 -0
- package/server/app/devDiagnostics.ts +138 -0
- package/server/app/index.ts +18 -8
- package/server/app/service/container.ts +0 -12
- package/server/app/service/index.ts +0 -2
- package/server/services/prisma/index.ts +121 -4
- package/server/services/router/http/index.ts +352 -0
- package/server/services/router/index.ts +50 -47
- package/server/services/router/request/api.ts +160 -19
- package/server/services/router/request/index.ts +8 -0
- package/server/services/router/response/index.ts +24 -1
- package/server/services/router/response/page/document.tsx +5 -0
- package/server/services/router/response/page/index.tsx +10 -0
- package/agents/framework/AGENTS.md +0 -177
- package/server/services/auth/router/service.json +0 -6
- package/server/services/auth/service.json +0 -6
- package/server/services/cron/service.json +0 -6
- package/server/services/disks/drivers/local/service.json +0 -6
- package/server/services/disks/drivers/s3/service.json +0 -6
- package/server/services/disks/service.json +0 -6
- package/server/services/fetch/service.json +0 -7
- package/server/services/prisma/service.json +0 -6
- package/server/services/router/service.json +0 -6
- package/server/services/schema/router/service.json +0 -6
- package/server/services/schema/service.json +0 -6
- package/server/services/security/encrypt/aes/service.json +0 -6
|
@@ -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;
|
|
@@ -367,6 +380,47 @@ const profilerStyles = `
|
|
|
367
380
|
line-height: 1.45;
|
|
368
381
|
}
|
|
369
382
|
|
|
383
|
+
.proteum-profiler__rowTitle {
|
|
384
|
+
min-width: 0;
|
|
385
|
+
word-break: break-word;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.proteum-profiler__rowMeta {
|
|
389
|
+
display: inline-flex;
|
|
390
|
+
align-items: center;
|
|
391
|
+
justify-content: flex-end;
|
|
392
|
+
gap: 8px;
|
|
393
|
+
margin-left: auto;
|
|
394
|
+
white-space: nowrap;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.proteum-profiler__statusBadge {
|
|
398
|
+
display: inline-flex;
|
|
399
|
+
align-items: center;
|
|
400
|
+
justify-content: center;
|
|
401
|
+
min-height: 18px;
|
|
402
|
+
padding: 0 8px;
|
|
403
|
+
border: 1px solid currentColor;
|
|
404
|
+
color: var(--profiler-muted);
|
|
405
|
+
background: transparent;
|
|
406
|
+
font-size: 10px;
|
|
407
|
+
font-weight: 700;
|
|
408
|
+
letter-spacing: 0.08em;
|
|
409
|
+
text-transform: uppercase;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.proteum-profiler__statusBadge--ok {
|
|
413
|
+
color: var(--profiler-ok);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.proteum-profiler__statusBadge--warn {
|
|
417
|
+
color: var(--profiler-warn);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.proteum-profiler__statusBadge--error {
|
|
421
|
+
color: var(--profiler-error);
|
|
422
|
+
}
|
|
423
|
+
|
|
370
424
|
.proteum-profiler__mono {
|
|
371
425
|
font-family: inherit;
|
|
372
426
|
font-size: 11px;
|
|
@@ -467,6 +521,7 @@ const profilerStyles = `
|
|
|
467
521
|
align-items: stretch;
|
|
468
522
|
min-height: 100%;
|
|
469
523
|
height: 100%;
|
|
524
|
+
max-height: 100%;
|
|
470
525
|
}
|
|
471
526
|
|
|
472
527
|
.proteum-profiler__splitView {
|
|
@@ -476,6 +531,7 @@ const profilerStyles = `
|
|
|
476
531
|
align-items: stretch;
|
|
477
532
|
min-height: 100%;
|
|
478
533
|
height: 100%;
|
|
534
|
+
max-height: 100%;
|
|
479
535
|
}
|
|
480
536
|
|
|
481
537
|
.proteum-profiler__splitView--stacked {
|
|
@@ -487,7 +543,12 @@ const profilerStyles = `
|
|
|
487
543
|
display: grid;
|
|
488
544
|
gap: 0;
|
|
489
545
|
min-width: 0;
|
|
546
|
+
min-height: 0;
|
|
547
|
+
height: 100%;
|
|
490
548
|
align-content: start;
|
|
549
|
+
overflow: auto;
|
|
550
|
+
overscroll-behavior: contain;
|
|
551
|
+
scrollbar-width: thin;
|
|
491
552
|
}
|
|
492
553
|
|
|
493
554
|
.proteum-profiler__requestGroups {
|
|
@@ -518,8 +579,6 @@ const profilerStyles = `
|
|
|
518
579
|
}
|
|
519
580
|
|
|
520
581
|
.proteum-profiler__sidebar {
|
|
521
|
-
position: sticky;
|
|
522
|
-
top: 0;
|
|
523
582
|
display: flex;
|
|
524
583
|
align-self: stretch;
|
|
525
584
|
height: 100%;
|
|
@@ -530,6 +589,7 @@ const profilerStyles = `
|
|
|
530
589
|
border-radius: 0;
|
|
531
590
|
background: transparent;
|
|
532
591
|
box-shadow: none;
|
|
592
|
+
overflow: hidden;
|
|
533
593
|
}
|
|
534
594
|
|
|
535
595
|
.proteum-profiler__sidebarScroller {
|
|
@@ -619,6 +679,44 @@ const profilerStyles = `
|
|
|
619
679
|
background: transparent !important;
|
|
620
680
|
}
|
|
621
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
|
+
|
|
622
720
|
.proteum-profiler__traceEventRow {
|
|
623
721
|
--profiler-trace-depth: 0;
|
|
624
722
|
--profiler-trace-guide-opacity: 0;
|
|
@@ -669,6 +767,18 @@ const profilerStyles = `
|
|
|
669
767
|
gap: 10px;
|
|
670
768
|
}
|
|
671
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
|
+
|
|
672
782
|
.proteum-profiler__metricRow {
|
|
673
783
|
grid-template-columns: minmax(90px, 110px) 1fr;
|
|
674
784
|
}
|
|
@@ -681,12 +791,19 @@ const profilerStyles = `
|
|
|
681
791
|
grid-template-columns: 1fr;
|
|
682
792
|
min-height: 0;
|
|
683
793
|
height: auto;
|
|
794
|
+
max-height: none;
|
|
684
795
|
}
|
|
685
796
|
|
|
686
797
|
.proteum-profiler__splitView {
|
|
687
798
|
grid-template-columns: 1fr;
|
|
688
799
|
min-height: 0;
|
|
689
800
|
height: auto;
|
|
801
|
+
max-height: none;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
.proteum-profiler__splitColumn {
|
|
805
|
+
height: auto;
|
|
806
|
+
overflow: visible;
|
|
690
807
|
}
|
|
691
808
|
|
|
692
809
|
.proteum-profiler__sidebar {
|
|
@@ -713,6 +830,7 @@ type TSessionSummary = {
|
|
|
713
830
|
primaryTrace?: TProfilerSessionTrace;
|
|
714
831
|
renderMs?: number;
|
|
715
832
|
routeLabel: string;
|
|
833
|
+
sqlCount: number;
|
|
716
834
|
ssrPayloadBytes?: number;
|
|
717
835
|
statusLabel: string;
|
|
718
836
|
totalMs?: number;
|
|
@@ -725,35 +843,60 @@ type TApiRequestItem = {
|
|
|
725
843
|
finishedAt?: string;
|
|
726
844
|
label?: string;
|
|
727
845
|
method: string;
|
|
846
|
+
originLabel?: string;
|
|
728
847
|
path: string;
|
|
729
848
|
requestData?: TTraceSummaryValue;
|
|
849
|
+
requestDataJson?: unknown;
|
|
730
850
|
result?: TTraceSummaryValue;
|
|
851
|
+
resultJson?: unknown;
|
|
731
852
|
startedAt: string;
|
|
732
853
|
statusCode?: number;
|
|
733
854
|
statusLabel?: string;
|
|
734
855
|
tags: string[];
|
|
735
856
|
};
|
|
736
|
-
type
|
|
737
|
-
|
|
738
|
-
color: string;
|
|
857
|
+
type TSqlQueryItem = {
|
|
858
|
+
callerLabel: string;
|
|
739
859
|
durationMs: number;
|
|
740
|
-
|
|
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
|
+
};
|
|
877
|
+
type TWaterfallChartItem = {
|
|
878
|
+
barLabel: string;
|
|
879
|
+
color: string;
|
|
880
|
+
detailLines: string[];
|
|
741
881
|
endOffsetMs: number;
|
|
742
|
-
|
|
743
|
-
startMs: number;
|
|
882
|
+
id: string;
|
|
744
883
|
startOffsetMs: number;
|
|
745
|
-
|
|
884
|
+
subtitle?: string;
|
|
885
|
+
title: string;
|
|
746
886
|
};
|
|
747
887
|
type TProfilerState = ReturnType<typeof profilerRuntime.getState>;
|
|
748
888
|
|
|
749
889
|
const panelLabels: Record<TProfilerPanel, string> = {
|
|
750
890
|
summary: 'Summary',
|
|
751
891
|
timeline: 'Timeline',
|
|
892
|
+
perf: 'Perf',
|
|
752
893
|
routing: 'Routing',
|
|
753
894
|
auth: 'Auth',
|
|
754
895
|
controller: 'Controller',
|
|
755
896
|
ssr: 'SSR',
|
|
756
897
|
api: 'API',
|
|
898
|
+
sql: 'SQL',
|
|
899
|
+
diagnose: 'Diagnose',
|
|
757
900
|
errors: 'Errors',
|
|
758
901
|
explain: 'Explain',
|
|
759
902
|
doctor: 'Doctor',
|
|
@@ -772,11 +915,24 @@ const readNumber = (value: TTraceSummaryValue | undefined) => (typeof value ===
|
|
|
772
915
|
const readString = (value: TTraceSummaryValue | undefined) => (typeof value === 'string' ? value : undefined);
|
|
773
916
|
const formatDuration = (value?: number) => (value === undefined ? 'pending' : `${Math.round(value)} ms`);
|
|
774
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)}%`);
|
|
775
921
|
const formatTimestamp = (value?: string) => {
|
|
776
922
|
if (!value) return 'never';
|
|
777
923
|
const date = new Date(value);
|
|
778
924
|
return Number.isNaN(date.valueOf()) ? value : date.toLocaleString();
|
|
779
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
|
+
};
|
|
780
936
|
const formatCronFrequency = (task: TProfilerCronTask) =>
|
|
781
937
|
task.frequency.kind === 'cron' ? task.frequency.value : `once at ${formatTimestamp(task.frequency.value)}`;
|
|
782
938
|
const formatStructuredValue = (value: unknown) => {
|
|
@@ -786,6 +942,29 @@ const formatStructuredValue = (value: unknown) => {
|
|
|
786
942
|
return String(value);
|
|
787
943
|
}
|
|
788
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
|
+
};
|
|
789
968
|
|
|
790
969
|
const renderSummaryValue = (value: TTraceSummaryValue | undefined): string => {
|
|
791
970
|
if (value === undefined) return '';
|
|
@@ -835,6 +1014,9 @@ const formatSummaryJson = (value: TTraceSummaryValue | undefined) => {
|
|
|
835
1014
|
return JSON.stringify(toSummaryJsonValue(value), null, 2);
|
|
836
1015
|
};
|
|
837
1016
|
|
|
1017
|
+
const formatApiPanelJson = (jsonValue: unknown, summaryValue: TTraceSummaryValue | undefined) =>
|
|
1018
|
+
jsonValue !== undefined ? formatStructuredValue(jsonValue) : formatSummaryJson(summaryValue);
|
|
1019
|
+
|
|
838
1020
|
const formatTraceEventDetailsJson = (details: Record<string, TTraceSummaryValue>) =>
|
|
839
1021
|
JSON.stringify(
|
|
840
1022
|
Object.fromEntries(Object.entries(details).map(([key, value]) => [key, toSummaryJsonValue(value)])),
|
|
@@ -940,6 +1122,11 @@ const formatProfilerRequestReference = ({
|
|
|
940
1122
|
return rawReference || fallbackLabel || 'request';
|
|
941
1123
|
};
|
|
942
1124
|
|
|
1125
|
+
const formatConnectedTraceCallReference = (call: TTraceCall) =>
|
|
1126
|
+
call.connectedProjectNamespace && call.connectedControllerAccessor
|
|
1127
|
+
? `${call.connectedProjectNamespace}.${call.connectedControllerAccessor}`
|
|
1128
|
+
: undefined;
|
|
1129
|
+
|
|
943
1130
|
const getTraceRequestData = (trace: TRequestTrace | undefined) =>
|
|
944
1131
|
trace?.events.find((event) => event.type === 'request.start')?.details.data;
|
|
945
1132
|
|
|
@@ -951,6 +1138,14 @@ const getTraceResultData = (trace: TRequestTrace | undefined) =>
|
|
|
951
1138
|
const getRequestStatusText = (statusCode?: number, statusLabel?: string) =>
|
|
952
1139
|
statusCode !== undefined ? String(statusCode) : statusLabel || 'pending';
|
|
953
1140
|
|
|
1141
|
+
const getRequestStatusTone = (statusCode?: number, statusLabel?: string): 'ok' | 'warn' | 'error' => {
|
|
1142
|
+
if (statusCode === undefined) return statusLabel === 'pending' ? 'warn' : 'ok';
|
|
1143
|
+
if (statusCode >= 500) return 'error';
|
|
1144
|
+
if (statusCode >= 400) return 'error';
|
|
1145
|
+
if (statusCode >= 300) return 'warn';
|
|
1146
|
+
return 'ok';
|
|
1147
|
+
};
|
|
1148
|
+
|
|
954
1149
|
const findTraceEvents = (trace: TRequestTrace | undefined, eventTypes: string[]) =>
|
|
955
1150
|
trace?.events.filter((event) => eventTypes.includes(event.type)) || [];
|
|
956
1151
|
|
|
@@ -1007,6 +1202,7 @@ const getSummary = (session: TProfilerNavigationSession): TSessionSummary => {
|
|
|
1007
1202
|
session.steps.filter((step) => step.status === 'error').length +
|
|
1008
1203
|
session.traces.filter((traceItem) => traceItem.status === 'error').length +
|
|
1009
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);
|
|
1010
1206
|
const renderStart = trace?.events.find((event) => event.type === 'render.start');
|
|
1011
1207
|
const renderEnd = trace?.events.find((event) => event.type === 'render.end');
|
|
1012
1208
|
const localRender = [...session.steps].reverse().find((step) => step.label === 'Render' && step.durationMs !== undefined);
|
|
@@ -1023,6 +1219,7 @@ const getSummary = (session: TProfilerNavigationSession): TSessionSummary => {
|
|
|
1023
1219
|
? Math.max(0, renderEnd.elapsedMs - renderStart.elapsedMs)
|
|
1024
1220
|
: localRender?.durationMs,
|
|
1025
1221
|
routeLabel,
|
|
1222
|
+
sqlCount,
|
|
1026
1223
|
ssrPayloadBytes: readNumber(ssrPayload?.details.serializedBytes),
|
|
1027
1224
|
statusLabel: session.kind === 'client-navigation' ? 'NAV' : trace ? `${trace.statusCode || 'pending'} ${trace.method}` : 'SSR',
|
|
1028
1225
|
totalMs: session.kind === 'client-navigation' ? session.durationMs : trace?.durationMs ?? session.durationMs,
|
|
@@ -1046,7 +1243,12 @@ const JsonCodeBlock = ({ value }: { value: string }) => (
|
|
|
1046
1243
|
<pre className="proteum-profiler__mono proteum-profiler__pre">{renderHighlightedJson(value)}</pre>
|
|
1047
1244
|
);
|
|
1048
1245
|
|
|
1246
|
+
const PlainCodeBlock = ({ value }: { value: string }) => <pre className="proteum-profiler__mono proteum-profiler__pre">{value}</pre>;
|
|
1247
|
+
|
|
1049
1248
|
const formatTraceCallDisplay = (call: TTraceCall) => {
|
|
1249
|
+
const connectedReference = formatConnectedTraceCallReference(call);
|
|
1250
|
+
if (connectedReference) return connectedReference;
|
|
1251
|
+
|
|
1050
1252
|
if (call.path.startsWith('/api/')) {
|
|
1051
1253
|
return formatProfilerRequestReference({
|
|
1052
1254
|
fallbackLabel: call.label,
|
|
@@ -1074,6 +1276,83 @@ const formatSessionTraceDisplay = (traceItem: TProfilerSessionTrace) => {
|
|
|
1074
1276
|
return traceItem.label || formatProfilerRequestReference({ method: traceItem.method, path: traceItem.path });
|
|
1075
1277
|
};
|
|
1076
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
|
+
|
|
1077
1356
|
const ApiRequestListEntry = ({
|
|
1078
1357
|
isSelected,
|
|
1079
1358
|
item,
|
|
@@ -1084,6 +1363,7 @@ const ApiRequestListEntry = ({
|
|
|
1084
1363
|
onSelect: () => void;
|
|
1085
1364
|
}) => {
|
|
1086
1365
|
const statusText = getRequestStatusText(item.statusCode, item.statusLabel);
|
|
1366
|
+
const statusTone = getRequestStatusTone(item.statusCode, item.statusLabel);
|
|
1087
1367
|
|
|
1088
1368
|
return (
|
|
1089
1369
|
<button
|
|
@@ -1093,19 +1373,14 @@ const ApiRequestListEntry = ({
|
|
|
1093
1373
|
type="button"
|
|
1094
1374
|
>
|
|
1095
1375
|
<div className="proteum-profiler__rowHeader">
|
|
1096
|
-
<strong
|
|
1097
|
-
|
|
1098
|
-
|
|
1376
|
+
<strong className="proteum-profiler__rowTitle">
|
|
1377
|
+
{formatApiReference(item.method, item.path, item.requestData, item.label)}
|
|
1378
|
+
</strong>
|
|
1379
|
+
<span className="proteum-profiler__rowMeta">
|
|
1380
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">{formatDuration(item.durationMs)}</span>
|
|
1381
|
+
<span className={`proteum-profiler__statusBadge proteum-profiler__statusBadge--${statusTone}`}>{statusText}</span>
|
|
1099
1382
|
</span>
|
|
1100
1383
|
</div>
|
|
1101
|
-
<div className="proteum-profiler__tags">
|
|
1102
|
-
{item.tags.map((tag) => (
|
|
1103
|
-
<span className="proteum-profiler__tag" key={`${item.id}:${tag}`}>
|
|
1104
|
-
{tag}
|
|
1105
|
-
</span>
|
|
1106
|
-
))}
|
|
1107
|
-
{item.errorMessage ? <span className="proteum-profiler__tag">{truncate(item.errorMessage, 72)}</span> : null}
|
|
1108
|
-
</div>
|
|
1109
1384
|
</button>
|
|
1110
1385
|
);
|
|
1111
1386
|
};
|
|
@@ -1177,12 +1452,12 @@ const ApiRequestSidebar = ({ item }: { item?: TApiRequestItem }) => {
|
|
|
1177
1452
|
|
|
1178
1453
|
<div className="proteum-profiler__sidebarSection">
|
|
1179
1454
|
<div className="proteum-profiler__sidebarSectionTitle">Arguments</div>
|
|
1180
|
-
<JsonCodeBlock value={
|
|
1455
|
+
<JsonCodeBlock value={formatApiPanelJson(item.requestDataJson, item.requestData)} />
|
|
1181
1456
|
</div>
|
|
1182
1457
|
|
|
1183
1458
|
<div className="proteum-profiler__sidebarSection">
|
|
1184
1459
|
<div className="proteum-profiler__sidebarSectionTitle">Result</div>
|
|
1185
|
-
<JsonCodeBlock value={
|
|
1460
|
+
<JsonCodeBlock value={formatApiPanelJson(item.resultJson, item.result)} />
|
|
1186
1461
|
</div>
|
|
1187
1462
|
|
|
1188
1463
|
{item.errorMessage ? (
|
|
@@ -1196,6 +1471,106 @@ const ApiRequestSidebar = ({ item }: { item?: TApiRequestItem }) => {
|
|
|
1196
1471
|
);
|
|
1197
1472
|
};
|
|
1198
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
|
+
|
|
1199
1574
|
const ApiPanel = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
1200
1575
|
const syncItems: TApiRequestItem[] = session.traces
|
|
1201
1576
|
.flatMap((trace) => trace.trace?.calls.filter((call) => call.origin !== 'client-async') || [])
|
|
@@ -1207,13 +1582,18 @@ const ApiPanel = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
|
1207
1582
|
finishedAt: call.finishedAt,
|
|
1208
1583
|
label: call.label,
|
|
1209
1584
|
method: call.method,
|
|
1585
|
+
originLabel: call.origin,
|
|
1210
1586
|
path: call.path,
|
|
1211
1587
|
requestData: call.requestData,
|
|
1588
|
+
requestDataJson: call.requestDataJson,
|
|
1212
1589
|
result: call.result,
|
|
1590
|
+
resultJson: call.resultJson,
|
|
1213
1591
|
startedAt: call.startedAt,
|
|
1214
1592
|
statusCode: call.statusCode,
|
|
1215
1593
|
tags: [
|
|
1216
1594
|
call.origin,
|
|
1595
|
+
...(call.connectedProjectNamespace ? [`connected:${call.connectedProjectNamespace}`] : []),
|
|
1596
|
+
...(call.connectedControllerAccessor ? [`target:${call.connectedControllerAccessor}`] : []),
|
|
1217
1597
|
...(call.fetcherId ? [`fetcher:${call.fetcherId}`] : []),
|
|
1218
1598
|
...call.requestDataKeys.map((key) => `arg:${key}`),
|
|
1219
1599
|
...call.resultKeys.map((key) => `res:${key}`),
|
|
@@ -1229,9 +1609,12 @@ const ApiPanel = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
|
1229
1609
|
finishedAt: trace.finishedAt,
|
|
1230
1610
|
label: trace.label,
|
|
1231
1611
|
method: trace.method,
|
|
1612
|
+
originLabel: 'client-async',
|
|
1232
1613
|
path: trace.path,
|
|
1233
1614
|
requestData: getTraceRequestData(trace.trace),
|
|
1615
|
+
requestDataJson: trace.trace?.requestDataJson,
|
|
1234
1616
|
result: getTraceResultData(trace.trace),
|
|
1617
|
+
resultJson: trace.trace?.resultJson,
|
|
1235
1618
|
startedAt: trace.startedAt,
|
|
1236
1619
|
statusCode: trace.trace?.statusCode,
|
|
1237
1620
|
statusLabel: trace.status,
|
|
@@ -1239,67 +1622,232 @@ const ApiPanel = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
|
1239
1622
|
}));
|
|
1240
1623
|
const requestItems = [...syncItems, ...asyncItems];
|
|
1241
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
|
+
});
|
|
1242
1652
|
|
|
1243
1653
|
React.useEffect(() => {
|
|
1244
1654
|
if (requestItems.some((item) => item.id === selectedRequestId)) return;
|
|
1245
1655
|
setSelectedRequestId(requestItems[0]?.id);
|
|
1246
1656
|
}, [requestItems, selectedRequestId]);
|
|
1247
1657
|
|
|
1658
|
+
const waterfallItems = buildApiWaterfallItems(requestItems);
|
|
1248
1659
|
const selectedItem = requestItems.find((item) => item.id === selectedRequestId) || requestItems[0];
|
|
1249
1660
|
|
|
1250
1661
|
return (
|
|
1251
1662
|
<div className="proteum-profiler__requestWorkspace">
|
|
1252
|
-
<div className="proteum-
|
|
1253
|
-
<div className="proteum-
|
|
1254
|
-
<
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
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
|
+
|
|
1685
|
+
<WaterfallChart
|
|
1686
|
+
emptyLabel="No API requests were captured for this session."
|
|
1687
|
+
itemLabel="request"
|
|
1688
|
+
items={waterfallItems}
|
|
1689
|
+
onSelect={setSelectedRequestId}
|
|
1690
|
+
/>
|
|
1691
|
+
|
|
1692
|
+
<div className="proteum-profiler__requestGroups">
|
|
1693
|
+
<div className="proteum-profiler__requestGroup">
|
|
1694
|
+
<div className="proteum-profiler__requestGroupHeader">
|
|
1695
|
+
<div className="proteum-profiler__sectionTitle">Synchronous calls</div>
|
|
1696
|
+
<div className="proteum-profiler__requestGroupCount">
|
|
1697
|
+
{syncItems.length} item{syncItems.length === 1 ? '' : 's'}
|
|
1698
|
+
</div>
|
|
1258
1699
|
</div>
|
|
1700
|
+
|
|
1701
|
+
{syncItems.length === 0 ? (
|
|
1702
|
+
<div className="proteum-profiler__empty">No synchronous SSR or batched API calls captured.</div>
|
|
1703
|
+
) : (
|
|
1704
|
+
<div className="proteum-profiler__list">
|
|
1705
|
+
{syncItems.map((item) => (
|
|
1706
|
+
<ApiRequestListEntry
|
|
1707
|
+
isSelected={item.id === selectedItem?.id}
|
|
1708
|
+
item={item}
|
|
1709
|
+
key={item.id}
|
|
1710
|
+
onSelect={() => setSelectedRequestId(item.id)}
|
|
1711
|
+
/>
|
|
1712
|
+
))}
|
|
1713
|
+
</div>
|
|
1714
|
+
)}
|
|
1259
1715
|
</div>
|
|
1260
1716
|
|
|
1261
|
-
|
|
1262
|
-
<div className="proteum-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
isSelected={item.id === selectedItem?.id}
|
|
1268
|
-
item={item}
|
|
1269
|
-
key={item.id}
|
|
1270
|
-
onSelect={() => setSelectedRequestId(item.id)}
|
|
1271
|
-
/>
|
|
1272
|
-
))}
|
|
1717
|
+
<div className="proteum-profiler__requestGroup">
|
|
1718
|
+
<div className="proteum-profiler__requestGroupHeader">
|
|
1719
|
+
<div className="proteum-profiler__sectionTitle">Async requests</div>
|
|
1720
|
+
<div className="proteum-profiler__requestGroupCount">
|
|
1721
|
+
{asyncItems.length} item{asyncItems.length === 1 ? '' : 's'}
|
|
1722
|
+
</div>
|
|
1273
1723
|
</div>
|
|
1274
|
-
)}
|
|
1275
|
-
</div>
|
|
1276
1724
|
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1725
|
+
{asyncItems.length === 0 ? (
|
|
1726
|
+
<div className="proteum-profiler__empty">No async API calls captured.</div>
|
|
1727
|
+
) : (
|
|
1728
|
+
<div className="proteum-profiler__list">
|
|
1729
|
+
{asyncItems.map((item) => (
|
|
1730
|
+
<ApiRequestListEntry
|
|
1731
|
+
isSelected={item.id === selectedItem?.id}
|
|
1732
|
+
item={item}
|
|
1733
|
+
key={item.id}
|
|
1734
|
+
onSelect={() => setSelectedRequestId(item.id)}
|
|
1735
|
+
/>
|
|
1736
|
+
))}
|
|
1737
|
+
</div>
|
|
1738
|
+
)}
|
|
1283
1739
|
</div>
|
|
1740
|
+
</div>
|
|
1741
|
+
</div>
|
|
1742
|
+
|
|
1743
|
+
<ApiRequestSidebar item={selectedItem} />
|
|
1744
|
+
</div>
|
|
1745
|
+
);
|
|
1746
|
+
};
|
|
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>
|
|
1284
1813
|
|
|
1285
|
-
|
|
1286
|
-
|
|
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>
|
|
1287
1824
|
) : (
|
|
1288
|
-
|
|
1289
|
-
{
|
|
1290
|
-
<
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
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
|
+
))
|
|
1298
1846
|
)}
|
|
1299
1847
|
</div>
|
|
1300
1848
|
</div>
|
|
1301
1849
|
|
|
1302
|
-
<
|
|
1850
|
+
<SqlQuerySidebar item={selectedItem} />
|
|
1303
1851
|
</div>
|
|
1304
1852
|
);
|
|
1305
1853
|
};
|
|
@@ -1315,6 +1863,8 @@ const TraceEventSidebar = ({
|
|
|
1315
1863
|
label: string;
|
|
1316
1864
|
trace?: TRequestTrace;
|
|
1317
1865
|
}) => {
|
|
1866
|
+
const detailEntries = Object.entries(event?.details || {});
|
|
1867
|
+
|
|
1318
1868
|
if (!event) {
|
|
1319
1869
|
return (
|
|
1320
1870
|
<aside className="proteum-profiler__sidebar">
|
|
@@ -1353,19 +1903,23 @@ const TraceEventSidebar = ({
|
|
|
1353
1903
|
<SummaryRow label="Trace" value={trace?.id || 'n/a'} />
|
|
1354
1904
|
</div>
|
|
1355
1905
|
|
|
1356
|
-
|
|
1357
|
-
<div className="proteum-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1906
|
+
{detailEntries.length > 0 ? (
|
|
1907
|
+
<div className="proteum-profiler__sidebarSection">
|
|
1908
|
+
<div className="proteum-profiler__sidebarSectionTitle">Summary</div>
|
|
1909
|
+
<div>
|
|
1910
|
+
{detailEntries.map(([key, value]) => (
|
|
1911
|
+
<SummaryRow
|
|
1912
|
+
key={`${trace?.id || 'trace'}:${event.index}:detail:${key}`}
|
|
1913
|
+
label={key}
|
|
1914
|
+
value={<span className="proteum-profiler__mono">{truncate(renderSummaryValue(value), 120)}</span>}
|
|
1915
|
+
/>
|
|
1916
|
+
))}
|
|
1917
|
+
</div>
|
|
1364
1918
|
</div>
|
|
1365
|
-
|
|
1919
|
+
) : null}
|
|
1366
1920
|
|
|
1367
1921
|
<div className="proteum-profiler__sidebarSection">
|
|
1368
|
-
<div className="proteum-profiler__sidebarSectionTitle">
|
|
1922
|
+
<div className="proteum-profiler__sidebarSectionTitle">Raw JSON</div>
|
|
1369
1923
|
<JsonCodeBlock value={formatTraceEventDetailsJson(event.details)} />
|
|
1370
1924
|
</div>
|
|
1371
1925
|
</div>
|
|
@@ -1407,6 +1961,12 @@ const TraceRows = ({
|
|
|
1407
1961
|
</div>
|
|
1408
1962
|
<div className="proteum-profiler__tags">
|
|
1409
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}
|
|
1410
1970
|
{call.fetcherId ? <span className="proteum-profiler__tag">fetcher:{call.fetcherId}</span> : null}
|
|
1411
1971
|
{call.requestDataKeys.map((key) => (
|
|
1412
1972
|
<span className="proteum-profiler__tag" key={`${call.id}:req:${key}`}>
|
|
@@ -1565,26 +2125,22 @@ const escapeHtml = (value: string) =>
|
|
|
1565
2125
|
.replace(/'/g, ''');
|
|
1566
2126
|
|
|
1567
2127
|
const timelineWaterfallMinDurationMs = 6;
|
|
1568
|
-
const
|
|
1569
|
-
const
|
|
1570
|
-
const
|
|
1571
|
-
|
|
1572
|
-
const TimelineChart = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
1573
|
-
const [ApexChartComponent, setApexChartComponent] = React.useState<unknown>(null);
|
|
2128
|
+
const waterfallBarHeight = 15;
|
|
2129
|
+
const waterfallRowGap = 1;
|
|
2130
|
+
const waterfallRowHeight = waterfallBarHeight + waterfallRowGap;
|
|
1574
2131
|
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
};
|
|
1586
|
-
}, []);
|
|
2132
|
+
const buildWaterfallEndMs = ({ durationMs, fallbackEndMs, finishedAt, startMs }: {
|
|
2133
|
+
durationMs?: number;
|
|
2134
|
+
fallbackEndMs?: number;
|
|
2135
|
+
finishedAt?: string;
|
|
2136
|
+
startMs: number;
|
|
2137
|
+
}) => {
|
|
2138
|
+
const finishedMs = readDateMs(finishedAt);
|
|
2139
|
+
const durationEndMs = durationMs !== undefined ? startMs + Math.max(durationMs, 1) : undefined;
|
|
2140
|
+
return Math.max(startMs + 1, fallbackEndMs ?? finishedMs ?? durationEndMs ?? startMs + 1);
|
|
2141
|
+
};
|
|
1587
2142
|
|
|
2143
|
+
const buildTimelineWaterfallItems = (session: TProfilerNavigationSession): TWaterfallChartItem[] => {
|
|
1588
2144
|
const sessionStartMs = readDateMs(session.startedAt) ?? 0;
|
|
1589
2145
|
const rawItems = session.traces.flatMap((traceItem) => {
|
|
1590
2146
|
const trace = traceItem.trace;
|
|
@@ -1594,17 +2150,21 @@ const TimelineChart = ({ session }: { session: TProfilerNavigationSession }) =>
|
|
|
1594
2150
|
const traceFinishedMs = readDateMs(trace.finishedAt) ?? (trace.durationMs !== undefined ? traceStartMs + trace.durationMs : undefined);
|
|
1595
2151
|
const traceLabel = formatSessionTraceDisplay(traceItem);
|
|
1596
2152
|
|
|
1597
|
-
return trace.events.map((event, index)
|
|
2153
|
+
return trace.events.map((event, index) => {
|
|
1598
2154
|
const nextEvent = trace.events[index + 1];
|
|
1599
2155
|
const startMs = readDateMs(event.at) ?? traceStartMs + event.elapsedMs;
|
|
1600
2156
|
const nextStartMs = nextEvent ? readDateMs(nextEvent.at) ?? traceStartMs + nextEvent.elapsedMs : undefined;
|
|
1601
|
-
const endMs =
|
|
2157
|
+
const endMs = buildWaterfallEndMs({
|
|
2158
|
+
fallbackEndMs: nextStartMs ?? traceFinishedMs,
|
|
2159
|
+
startMs,
|
|
2160
|
+
});
|
|
1602
2161
|
|
|
1603
2162
|
return {
|
|
1604
2163
|
durationMs: Math.max(1, endMs - startMs),
|
|
1605
2164
|
endMs,
|
|
1606
2165
|
event,
|
|
1607
2166
|
startMs,
|
|
2167
|
+
trace,
|
|
1608
2168
|
traceLabel,
|
|
1609
2169
|
};
|
|
1610
2170
|
});
|
|
@@ -1612,139 +2172,986 @@ const TimelineChart = ({ session }: { session: TProfilerNavigationSession }) =>
|
|
|
1612
2172
|
|
|
1613
2173
|
const sortedItems = [...rawItems].sort((left, right) => left.startMs - right.startMs || left.event.index - right.event.index);
|
|
1614
2174
|
const chartStartMs = sortedItems.length > 0 ? Math.min(...sortedItems.map((item) => item.startMs)) : 0;
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
const items: TTimelineWaterfallEventItem[] = sortedItems
|
|
2175
|
+
|
|
2176
|
+
return sortedItems
|
|
1618
2177
|
.filter((item) => item.durationMs >= timelineWaterfallMinDurationMs)
|
|
1619
|
-
.map((item) =>
|
|
1620
|
-
|
|
1621
|
-
|
|
2178
|
+
.map((item) => {
|
|
2179
|
+
const startOffsetMs = item.startMs - chartStartMs;
|
|
2180
|
+
const endOffsetMs = item.endMs - chartStartMs;
|
|
2181
|
+
|
|
2182
|
+
return {
|
|
2183
|
+
barLabel: truncate(`${item.event.type} | ${item.traceLabel}`, 84),
|
|
2184
|
+
color: getTimelineDurationColor(item.durationMs),
|
|
2185
|
+
detailLines: [
|
|
2186
|
+
`Start: +${Math.round(startOffsetMs)} ms`,
|
|
2187
|
+
`End: +${Math.round(endOffsetMs)} ms`,
|
|
2188
|
+
`Span: ${formatDuration(item.durationMs)}`,
|
|
2189
|
+
],
|
|
2190
|
+
endOffsetMs,
|
|
2191
|
+
id: getTraceEventKey(item.trace.id, item.event),
|
|
2192
|
+
startOffsetMs,
|
|
2193
|
+
subtitle: item.traceLabel,
|
|
2194
|
+
title: item.event.type,
|
|
2195
|
+
};
|
|
2196
|
+
});
|
|
2197
|
+
};
|
|
2198
|
+
|
|
2199
|
+
const buildApiWaterfallItems = (requestItems: TApiRequestItem[]): TWaterfallChartItem[] => {
|
|
2200
|
+
const rawItems = requestItems.map((item) => {
|
|
2201
|
+
const startMs = readDateMs(item.startedAt) ?? 0;
|
|
2202
|
+
const endMs = buildWaterfallEndMs({
|
|
2203
|
+
durationMs: item.durationMs,
|
|
2204
|
+
finishedAt: item.finishedAt,
|
|
2205
|
+
startMs,
|
|
2206
|
+
});
|
|
2207
|
+
const statusText = getRequestStatusText(item.statusCode, item.statusLabel);
|
|
2208
|
+
const reference = formatApiReference(item.method, item.path, item.requestData, item.label);
|
|
2209
|
+
|
|
2210
|
+
return {
|
|
2211
|
+
endMs,
|
|
2212
|
+
item,
|
|
2213
|
+
reference,
|
|
2214
|
+
startMs,
|
|
2215
|
+
statusText,
|
|
2216
|
+
};
|
|
2217
|
+
});
|
|
2218
|
+
|
|
2219
|
+
const sortedItems = [...rawItems].sort((left, right) => left.startMs - right.startMs || left.reference.localeCompare(right.reference));
|
|
2220
|
+
const chartStartMs = sortedItems.length > 0 ? Math.min(...sortedItems.map((item) => item.startMs)) : 0;
|
|
2221
|
+
|
|
2222
|
+
return sortedItems.map(({ endMs, item, reference, startMs, statusText }) => {
|
|
2223
|
+
const startOffsetMs = startMs - chartStartMs;
|
|
2224
|
+
const endOffsetMs = endMs - chartStartMs;
|
|
2225
|
+
|
|
2226
|
+
return {
|
|
2227
|
+
barLabel: truncate(reference, 84),
|
|
1622
2228
|
color: getTimelineDurationColor(item.durationMs),
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
2229
|
+
detailLines: [
|
|
2230
|
+
`Status: ${statusText}`,
|
|
2231
|
+
`Duration: ${formatDuration(item.durationMs)}`,
|
|
2232
|
+
`Start: +${Math.round(startOffsetMs)} ms`,
|
|
2233
|
+
`End: +${Math.round(endOffsetMs)} ms`,
|
|
2234
|
+
],
|
|
2235
|
+
endOffsetMs,
|
|
2236
|
+
id: item.id,
|
|
2237
|
+
startOffsetMs,
|
|
2238
|
+
subtitle: item.groupLabel,
|
|
2239
|
+
title: reference,
|
|
2240
|
+
};
|
|
2241
|
+
});
|
|
2242
|
+
};
|
|
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
|
+
|
|
2317
|
+
const WaterfallChart = ({
|
|
2318
|
+
emptyLabel,
|
|
2319
|
+
itemLabel,
|
|
2320
|
+
items,
|
|
2321
|
+
onSelect,
|
|
2322
|
+
}: {
|
|
2323
|
+
emptyLabel: string;
|
|
2324
|
+
itemLabel: string;
|
|
2325
|
+
items: TWaterfallChartItem[];
|
|
2326
|
+
onSelect?: (itemId: string) => void;
|
|
2327
|
+
}) => {
|
|
2328
|
+
const totalDurationMs = Math.max(items.length > 0 ? Math.max(...items.map((item) => item.endOffsetMs)) : 1, 1);
|
|
2329
|
+
const chartHeight = Math.max(260, items.length * waterfallRowHeight + 24);
|
|
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',
|
|
2379
|
+
},
|
|
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;
|
|
2446
|
+
|
|
2447
|
+
return (
|
|
2448
|
+
<div className="proteum-profiler__section">
|
|
2449
|
+
<div className="proteum-profiler__timelineChart">
|
|
2450
|
+
<div className="proteum-profiler__timelineChartMeta">
|
|
2451
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">
|
|
2452
|
+
{items.length} {pluralizeCountLabel(itemLabel, items.length)}
|
|
2453
|
+
</span>
|
|
2454
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">{formatDuration(totalDurationMs)}</span>
|
|
2455
|
+
</div>
|
|
2456
|
+
|
|
2457
|
+
<div className="proteum-profiler__timelineChartCanvas" style={{ height: `${chartHeight}px` }}>
|
|
2458
|
+
{options ? (
|
|
2459
|
+
<ApexChart emptyLabel={emptyLabel} options={options} />
|
|
2460
|
+
) : (
|
|
2461
|
+
<div className="proteum-profiler__empty">{emptyLabel}</div>
|
|
2462
|
+
)}
|
|
2463
|
+
</div>
|
|
2464
|
+
</div>
|
|
2465
|
+
</div>
|
|
2466
|
+
);
|
|
2467
|
+
};
|
|
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,
|
|
1637
2515
|
},
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
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,
|
|
1649
2540
|
},
|
|
1650
|
-
|
|
1651
|
-
|
|
2541
|
+
},
|
|
2542
|
+
yaxis: {
|
|
2543
|
+
labels: {
|
|
2544
|
+
style: { colors: profilerChartTheme.text, fontSize: '11px' },
|
|
1652
2545
|
},
|
|
1653
|
-
|
|
1654
|
-
|
|
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,
|
|
1655
2596
|
},
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
2597
|
+
},
|
|
2598
|
+
series,
|
|
2599
|
+
stroke: { width: 1 },
|
|
2600
|
+
title: {
|
|
2601
|
+
style: {
|
|
2602
|
+
color: profilerChartTheme.text,
|
|
2603
|
+
fontSize: '12px',
|
|
2604
|
+
fontWeight: 700,
|
|
1661
2605
|
},
|
|
1662
|
-
|
|
1663
|
-
|
|
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,
|
|
1664
2617
|
},
|
|
1665
|
-
|
|
2618
|
+
},
|
|
2619
|
+
yaxis: {
|
|
2620
|
+
labels: { style: { colors: profilerChartTheme.muted, fontSize: '11px' } },
|
|
2621
|
+
title: {
|
|
1666
2622
|
style: {
|
|
1667
|
-
color:
|
|
1668
|
-
|
|
1669
|
-
|
|
2623
|
+
color: profilerChartTheme.muted,
|
|
2624
|
+
fontSize: '10px',
|
|
2625
|
+
fontWeight: 600,
|
|
1670
2626
|
},
|
|
1671
|
-
text:
|
|
2627
|
+
text: valueUnit,
|
|
1672
2628
|
},
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
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,
|
|
1679
2761
|
},
|
|
2762
|
+
text: xaxisTitle,
|
|
1680
2763
|
},
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
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,
|
|
1684
2775
|
},
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
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,
|
|
1699
3098
|
},
|
|
1700
3099
|
},
|
|
1701
3100
|
xaxis: {
|
|
1702
3101
|
axisBorder: { show: false },
|
|
1703
3102
|
axisTicks: { show: false },
|
|
1704
|
-
|
|
1705
|
-
|
|
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: {
|
|
1706
3108
|
style: {
|
|
1707
|
-
|
|
1708
|
-
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
|
|
3109
|
+
color: profilerChartTheme.muted,
|
|
1709
3110
|
fontSize: '10px',
|
|
3111
|
+
fontWeight: 600,
|
|
1710
3112
|
},
|
|
1711
|
-
|
|
1712
|
-
max: totalDurationMs,
|
|
1713
|
-
min: 0,
|
|
1714
|
-
tickAmount: Math.min(6, Math.max(2, items.length > 0 ? 6 : 2)),
|
|
1715
|
-
type: 'numeric',
|
|
1716
|
-
},
|
|
1717
|
-
yaxis: {
|
|
1718
|
-
show: false,
|
|
1719
|
-
labels: {
|
|
1720
|
-
show: false,
|
|
3113
|
+
text: 'Percent vs baseline',
|
|
1721
3114
|
},
|
|
1722
3115
|
},
|
|
1723
3116
|
};
|
|
3117
|
+
};
|
|
1724
3118
|
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
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
|
+
};
|
|
1734
3135
|
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
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>
|
|
1745
3151
|
</div>
|
|
1746
|
-
|
|
1747
|
-
|
|
3152
|
+
<ApexChart emptyLabel={emptyLabel} options={options} />
|
|
3153
|
+
</div>
|
|
3154
|
+
);
|
|
1748
3155
|
|
|
1749
3156
|
const TimelinePanel = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
1750
3157
|
const selections: TTraceEventInspectorSelection[] = session.traces.flatMap((traceItem) =>
|
|
@@ -1764,67 +3171,72 @@ const TimelinePanel = ({ session }: { session: TProfilerNavigationSession }) =>
|
|
|
1764
3171
|
setSelectedEventKey(selections[0]?.key);
|
|
1765
3172
|
}, [selectedEventKey, selections]);
|
|
1766
3173
|
|
|
3174
|
+
const waterfallItems = buildTimelineWaterfallItems(session);
|
|
1767
3175
|
const selected = selections.find((selection) => selection.key === selectedEventKey) || selections[0];
|
|
1768
3176
|
|
|
1769
3177
|
return (
|
|
1770
|
-
<div className="proteum-
|
|
1771
|
-
<
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
<div className="proteum-profiler__list">
|
|
1779
|
-
{session.steps.map((step) => (
|
|
1780
|
-
<div className="proteum-profiler__row" key={step.id}>
|
|
1781
|
-
<div className="proteum-profiler__rowHeader">
|
|
1782
|
-
<strong>{step.label}</strong>
|
|
1783
|
-
<span className="proteum-profiler__mono proteum-profiler__muted">{formatDuration(step.durationMs)}</span>
|
|
1784
|
-
</div>
|
|
1785
|
-
<div className="proteum-profiler__tags">
|
|
1786
|
-
<span className="proteum-profiler__tag">{step.status}</span>
|
|
1787
|
-
{Object.entries(step.details || {}).map(([key, value]) => (
|
|
1788
|
-
<span className="proteum-profiler__tag" key={`${step.id}:${key}`}>
|
|
1789
|
-
{key}:{String(value)}
|
|
1790
|
-
</span>
|
|
1791
|
-
))}
|
|
1792
|
-
{step.errorMessage ? <span className="proteum-profiler__tag">{truncate(step.errorMessage, 72)}</span> : null}
|
|
1793
|
-
</div>
|
|
1794
|
-
</div>
|
|
1795
|
-
))}
|
|
1796
|
-
</div>
|
|
1797
|
-
</div>
|
|
3178
|
+
<div className="proteum-profiler__splitView">
|
|
3179
|
+
<div className="proteum-profiler__splitColumn">
|
|
3180
|
+
<WaterfallChart
|
|
3181
|
+
emptyLabel="No timeline events were captured for this session."
|
|
3182
|
+
itemLabel="event"
|
|
3183
|
+
items={waterfallItems}
|
|
3184
|
+
onSelect={setSelectedEventKey}
|
|
3185
|
+
/>
|
|
1798
3186
|
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
<div className="proteum-profiler__rowHeader">
|
|
1810
|
-
<strong>{formatSessionTraceDisplay(traceItem)}</strong>
|
|
1811
|
-
<span className="proteum-profiler__mono proteum-profiler__muted">{traceItem.status}</span>
|
|
3187
|
+
<div className="proteum-profiler__section">
|
|
3188
|
+
<div className="proteum-profiler__titleRow">
|
|
3189
|
+
<div className="proteum-profiler__sectionTitle">Navigation steps</div>
|
|
3190
|
+
</div>
|
|
3191
|
+
<div className="proteum-profiler__list">
|
|
3192
|
+
{session.steps.map((step) => (
|
|
3193
|
+
<div className="proteum-profiler__row" key={step.id}>
|
|
3194
|
+
<div className="proteum-profiler__rowHeader">
|
|
3195
|
+
<strong>{step.label}</strong>
|
|
3196
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">{formatDuration(step.durationMs)}</span>
|
|
1812
3197
|
</div>
|
|
1813
|
-
<div className="proteum-
|
|
1814
|
-
{
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
3198
|
+
<div className="proteum-profiler__tags">
|
|
3199
|
+
<span className="proteum-profiler__tag">{step.status}</span>
|
|
3200
|
+
{Object.entries(step.details || {}).map(([key, value]) => (
|
|
3201
|
+
<span className="proteum-profiler__tag" key={`${step.id}:${key}`}>
|
|
3202
|
+
{key}:{String(value)}
|
|
3203
|
+
</span>
|
|
3204
|
+
))}
|
|
3205
|
+
{step.errorMessage ? <span className="proteum-profiler__tag">{truncate(step.errorMessage, 72)}</span> : null}
|
|
1820
3206
|
</div>
|
|
1821
3207
|
</div>
|
|
1822
|
-
)
|
|
1823
|
-
|
|
3208
|
+
))}
|
|
3209
|
+
</div>
|
|
1824
3210
|
</div>
|
|
1825
3211
|
|
|
1826
|
-
|
|
3212
|
+
{session.traces.map((traceItem) =>
|
|
3213
|
+
traceItem.trace ? (
|
|
3214
|
+
<TraceRows
|
|
3215
|
+
key={traceItem.id}
|
|
3216
|
+
onSelect={setSelectedEventKey}
|
|
3217
|
+
selectedEventKey={selectedEventKey}
|
|
3218
|
+
trace={traceItem.trace}
|
|
3219
|
+
/>
|
|
3220
|
+
) : (
|
|
3221
|
+
<div className="proteum-profiler__row" key={traceItem.id}>
|
|
3222
|
+
<div className="proteum-profiler__rowHeader">
|
|
3223
|
+
<strong>{formatSessionTraceDisplay(traceItem)}</strong>
|
|
3224
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">{traceItem.status}</span>
|
|
3225
|
+
</div>
|
|
3226
|
+
<div className="proteum-profiler__mono">
|
|
3227
|
+
{formatProfilerRequestReference({
|
|
3228
|
+
fallbackLabel: traceItem.label,
|
|
3229
|
+
method: traceItem.method,
|
|
3230
|
+
path: traceItem.path,
|
|
3231
|
+
requestData: getTraceRequestData(traceItem.trace),
|
|
3232
|
+
})}
|
|
3233
|
+
</div>
|
|
3234
|
+
</div>
|
|
3235
|
+
),
|
|
3236
|
+
)}
|
|
1827
3237
|
</div>
|
|
3238
|
+
|
|
3239
|
+
<TraceEventSidebar event={selected?.event} label={selected?.label || 'Trace event'} trace={selected?.trace} />
|
|
1828
3240
|
</div>
|
|
1829
3241
|
);
|
|
1830
3242
|
};
|
|
@@ -1836,6 +3248,46 @@ const AuthPanel = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
|
1836
3248
|
? [{ authEvents, id: traceItem.id, label: formatSessionTraceDisplay(traceItem), trace: traceItem.trace }]
|
|
1837
3249
|
: [];
|
|
1838
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
|
+
});
|
|
1839
3291
|
const selections: TTraceEventInspectorSelection[] = authSections.flatMap((section) =>
|
|
1840
3292
|
section.authEvents.map((event) => ({
|
|
1841
3293
|
event,
|
|
@@ -1858,6 +3310,27 @@ const AuthPanel = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
|
1858
3310
|
return (
|
|
1859
3311
|
<div className="proteum-profiler__splitView">
|
|
1860
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
|
+
|
|
1861
3334
|
{authSections.map((section) => (
|
|
1862
3335
|
<AuthTraceSection
|
|
1863
3336
|
authEvents={section.authEvents}
|
|
@@ -1936,24 +3409,101 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
1936
3409
|
const primaryTrace = summary.primaryTrace?.trace;
|
|
1937
3410
|
|
|
1938
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
|
+
|
|
1939
3459
|
return (
|
|
1940
|
-
|
|
1941
|
-
<
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
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
|
+
</>
|
|
1957
3507
|
);
|
|
1958
3508
|
}
|
|
1959
3509
|
|
|
@@ -1961,66 +3511,435 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
1961
3511
|
return <TimelinePanel session={session} />;
|
|
1962
3512
|
}
|
|
1963
3513
|
|
|
1964
|
-
if (panel === '
|
|
1965
|
-
|
|
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
|
+
|
|
3709
|
+
if (panel === 'auth') {
|
|
3710
|
+
return <AuthPanel session={session} />;
|
|
1966
3711
|
}
|
|
1967
3712
|
|
|
1968
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
|
+
|
|
1969
3762
|
return (
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
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
|
+
</>
|
|
1988
3798
|
);
|
|
1989
3799
|
}
|
|
1990
3800
|
|
|
1991
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
|
+
|
|
1992
3829
|
return (
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
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) => ({
|
|
1997
3855
|
key: `${event.index}:${event.type}`,
|
|
1998
3856
|
title: event.type,
|
|
1999
3857
|
value: Object.entries(event.details)
|
|
2000
3858
|
.map(([key, value]) => `${key}=${renderSummaryValue(value)}`)
|
|
2001
3859
|
.join(' '),
|
|
2002
|
-
})
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
3860
|
+
}))}
|
|
3861
|
+
showTitle={false}
|
|
3862
|
+
title="Controller"
|
|
3863
|
+
/>
|
|
3864
|
+
</>
|
|
2007
3865
|
);
|
|
2008
3866
|
}
|
|
2009
3867
|
|
|
2010
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
|
+
|
|
2011
3907
|
return (
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
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
|
+
</>
|
|
2024
3943
|
);
|
|
2025
3944
|
}
|
|
2026
3945
|
|
|
@@ -2028,6 +3947,147 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
2028
3947
|
return <ApiPanel session={session} />;
|
|
2029
3948
|
}
|
|
2030
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
|
+
|
|
2031
4091
|
if (panel === 'explain') {
|
|
2032
4092
|
const explain = state.explain;
|
|
2033
4093
|
const blocks = explain.manifest
|
|
@@ -2036,6 +4096,74 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
2036
4096
|
...buildExplainBlocks(explain.manifest, [...explainSectionNames]),
|
|
2037
4097
|
]
|
|
2038
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;
|
|
2039
4167
|
|
|
2040
4168
|
return (
|
|
2041
4169
|
<div className="proteum-profiler__section">
|
|
@@ -2063,6 +4191,27 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
2063
4191
|
<div className="proteum-profiler__empty">No explain manifest is available.</div>
|
|
2064
4192
|
) : (
|
|
2065
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
|
+
|
|
2066
4215
|
<div className="proteum-profiler__row">
|
|
2067
4216
|
<div className="proteum-profiler__rowHeader">
|
|
2068
4217
|
<strong>Manifest snapshot</strong>
|
|
@@ -2083,6 +4232,50 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
2083
4232
|
|
|
2084
4233
|
if (panel === 'doctor') {
|
|
2085
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
|
+
})) || [];
|
|
2086
4279
|
const doctorRows =
|
|
2087
4280
|
doctor.response?.diagnostics.map((diagnostic, index) => ({
|
|
2088
4281
|
key: `${diagnostic.code}:${index}`,
|
|
@@ -2123,17 +4316,46 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
2123
4316
|
<div className="proteum-profiler__metrics">
|
|
2124
4317
|
<SummaryRow label="Errors" value={String(doctor.response.summary.errors)} />
|
|
2125
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
|
+
/>
|
|
2126
4327
|
<SummaryRow label="Strict" value={doctor.response.summary.strictFailed ? 'failed' : 'ok'} />
|
|
2127
4328
|
<SummaryRow
|
|
2128
4329
|
label="Refreshed"
|
|
2129
4330
|
value={doctor.lastLoadedAt ? formatTimestamp(doctor.lastLoadedAt) : 'Not loaded'}
|
|
2130
4331
|
/>
|
|
2131
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>
|
|
2132
4353
|
{doctorBlocks.length > 0 ? (
|
|
2133
4354
|
<TextBlocks blocks={doctorBlocks} />
|
|
2134
4355
|
) : (
|
|
2135
4356
|
<SimpleSection empty="No manifest diagnostics were found." rows={doctorRows} title="Diagnostics" />
|
|
2136
4357
|
)}
|
|
4358
|
+
<SimpleSection empty="No contract diagnostics were found." rows={contractRows} title="Contracts" />
|
|
2137
4359
|
</>
|
|
2138
4360
|
)}
|
|
2139
4361
|
</div>
|
|
@@ -2142,6 +4364,33 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
2142
4364
|
|
|
2143
4365
|
if (panel === 'commands') {
|
|
2144
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
|
+
});
|
|
2145
4394
|
|
|
2146
4395
|
return (
|
|
2147
4396
|
<div className="proteum-profiler__section">
|
|
@@ -2180,65 +4429,88 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
2180
4429
|
) : commandsState.commands.length === 0 ? (
|
|
2181
4430
|
<div className="proteum-profiler__empty">No commands are registered for this app.</div>
|
|
2182
4431
|
) : (
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
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
|
+
: ''}
|
|
2201
4493
|
</div>
|
|
2202
|
-
</div>
|
|
2203
4494
|
|
|
2204
|
-
<div className="proteum-profiler__tags">
|
|
2205
|
-
<span className="proteum-profiler__tag">{command.className}</span>
|
|
2206
|
-
<span className="proteum-profiler__tag">{command.methodName}</span>
|
|
2207
|
-
<span className="proteum-profiler__tag">{command.scope}</span>
|
|
2208
|
-
{execution ? <span className="proteum-profiler__tag">{execution.status}</span> : null}
|
|
2209
4495
|
{execution ? (
|
|
2210
|
-
<
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
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>
|
|
2214
4508
|
) : null}
|
|
2215
4509
|
</div>
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
? ` | refreshed ${formatTimestamp(commandsState.lastLoadedAt)}`
|
|
2221
|
-
: ''}
|
|
2222
|
-
</div>
|
|
2223
|
-
|
|
2224
|
-
{execution ? (
|
|
2225
|
-
<div className="proteum-profiler__section">
|
|
2226
|
-
<div className="proteum-profiler__sectionTitle">Last result</div>
|
|
2227
|
-
<JsonCodeBlock
|
|
2228
|
-
value={
|
|
2229
|
-
execution.result?.json !== undefined
|
|
2230
|
-
? formatStructuredValue(execution.result.json)
|
|
2231
|
-
: execution.result
|
|
2232
|
-
? formatStructuredValue(execution.result.summary)
|
|
2233
|
-
: execution.errorMessage || 'undefined'
|
|
2234
|
-
}
|
|
2235
|
-
/>
|
|
2236
|
-
</div>
|
|
2237
|
-
) : null}
|
|
2238
|
-
</div>
|
|
2239
|
-
);
|
|
2240
|
-
})}
|
|
2241
|
-
</div>
|
|
4510
|
+
);
|
|
4511
|
+
})}
|
|
4512
|
+
</div>
|
|
4513
|
+
</>
|
|
2242
4514
|
)}
|
|
2243
4515
|
</div>
|
|
2244
4516
|
);
|
|
@@ -2246,6 +4518,30 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
2246
4518
|
|
|
2247
4519
|
if (panel === 'cron') {
|
|
2248
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
|
+
});
|
|
2249
4545
|
|
|
2250
4546
|
return (
|
|
2251
4547
|
<div className="proteum-profiler__section">
|
|
@@ -2285,81 +4581,140 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
2285
4581
|
) : cron.tasks.length === 0 ? (
|
|
2286
4582
|
<div className="proteum-profiler__empty">No cron tasks are registered for this app.</div>
|
|
2287
4583
|
) : (
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
<
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
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>
|
|
2309
4628
|
</div>
|
|
2310
|
-
</div>
|
|
2311
4629
|
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
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>
|
|
2331
4649
|
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
4650
|
+
<div className="proteum-profiler__mono proteum-profiler__muted">
|
|
4651
|
+
registered {formatTimestamp(task.registeredAt)}
|
|
4652
|
+
{cron.lastLoadedAt ? ` | refreshed ${formatTimestamp(cron.lastLoadedAt)}` : ''}
|
|
4653
|
+
</div>
|
|
2335
4654
|
</div>
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
4655
|
+
))}
|
|
4656
|
+
</div>
|
|
4657
|
+
</>
|
|
2339
4658
|
)}
|
|
2340
4659
|
</div>
|
|
2341
4660
|
);
|
|
2342
4661
|
}
|
|
2343
4662
|
|
|
2344
|
-
const
|
|
2345
|
-
...session.steps
|
|
4663
|
+
const stepErrors = session.steps
|
|
2346
4664
|
.filter((step) => step.status === 'error')
|
|
2347
|
-
.map((step) => ({ key: step.id, title: step.label, value: step.errorMessage || 'Step failed' }))
|
|
2348
|
-
|
|
4665
|
+
.map((step) => ({ key: step.id, title: step.label, value: step.errorMessage || 'Step failed' }));
|
|
4666
|
+
const traceErrors = session.traces
|
|
2349
4667
|
.filter((trace) => trace.status === 'error')
|
|
2350
|
-
.map((trace) => ({ key: trace.id, title: trace.label, value: trace.errorMessage || 'Request failed' }))
|
|
2351
|
-
|
|
4668
|
+
.map((trace) => ({ key: trace.id, title: trace.label, value: trace.errorMessage || 'Request failed' }));
|
|
4669
|
+
const eventErrors = findTraceEvents(primaryTrace, ['error']).map((event) => ({
|
|
2352
4670
|
key: `${event.index}:error`,
|
|
2353
4671
|
title: event.type,
|
|
2354
4672
|
value: Object.entries(event.details)
|
|
2355
4673
|
.map(([key, value]) => `${key}=${renderSummaryValue(value)}`)
|
|
2356
4674
|
.join(' '),
|
|
2357
|
-
}))
|
|
2358
|
-
];
|
|
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
|
+
});
|
|
2359
4693
|
|
|
2360
|
-
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
|
+
);
|
|
2361
4714
|
};
|
|
2362
4715
|
|
|
4716
|
+
const splitScrollPanels = new Set<TProfilerPanel>(['timeline', 'auth', 'api', 'sql']);
|
|
4717
|
+
|
|
2363
4718
|
export default function DevProfiler() {
|
|
2364
4719
|
const [state, setState] = React.useState(() => profilerRuntime.getState());
|
|
2365
4720
|
|
|
@@ -2388,6 +4743,7 @@ export default function DevProfiler() {
|
|
|
2388
4743
|
const summary = getSummary(session);
|
|
2389
4744
|
const tone = summary.errorCount > 0 ? 'error' : (summary.totalMs || 0) > 500 ? 'warn' : 'ok';
|
|
2390
4745
|
const primaryTrace = summary.primaryTrace?.trace;
|
|
4746
|
+
const currentPerfRequest = primaryTrace ? buildRequestPerformance(primaryTrace) : undefined;
|
|
2391
4747
|
const minimizedLabel =
|
|
2392
4748
|
session.kind === 'client-navigation'
|
|
2393
4749
|
? session.label
|
|
@@ -2455,7 +4811,13 @@ export default function DevProfiler() {
|
|
|
2455
4811
|
</div>
|
|
2456
4812
|
</div>
|
|
2457
4813
|
|
|
2458
|
-
<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>
|
|
2459
4821
|
</div>
|
|
2460
4822
|
) : null}
|
|
2461
4823
|
|
|
@@ -2492,6 +4854,20 @@ export default function DevProfiler() {
|
|
|
2492
4854
|
onClick={() => profilerRuntime.openPanel('api')}
|
|
2493
4855
|
tone={summary.apiAsyncCount > 0 || summary.apiSyncCount > 0 ? 'ok' : 'warn'}
|
|
2494
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
|
+
/>
|
|
2495
4871
|
{summary.errorCount > 0 ? (
|
|
2496
4872
|
<StatusToken
|
|
2497
4873
|
label={`${summary.errorCount} error${summary.errorCount === 1 ? '' : 's'}`}
|
|
@@ -2499,6 +4875,11 @@ export default function DevProfiler() {
|
|
|
2499
4875
|
tone="error"
|
|
2500
4876
|
/>
|
|
2501
4877
|
) : null}
|
|
4878
|
+
<StatusToken
|
|
4879
|
+
label="Diagnose"
|
|
4880
|
+
onClick={() => profilerRuntime.openPanel('diagnose')}
|
|
4881
|
+
tone={summary.errorCount > 0 ? 'error' : 'warn'}
|
|
4882
|
+
/>
|
|
2502
4883
|
<div className="proteum-profiler__spacer" />
|
|
2503
4884
|
<button className="proteum-profiler__token" onClick={() => profilerRuntime.setUiState('pinned-handle')} type="button">
|
|
2504
4885
|
Hide
|