diff --git a/frontend/app/components/static_kernel_viewer/BUILD b/frontend/app/components/static_kernel_viewer/BUILD index 225ddc7d7..5db7f35a2 100644 --- a/frontend/app/components/static_kernel_viewer/BUILD +++ b/frontend/app/components/static_kernel_viewer/BUILD @@ -12,17 +12,25 @@ sass_binary( src = "kernel_event_tooltip.scss", ) +sass_binary( + name = "kernel_selection_panel_css", + src = "kernel_selection_panel.scss", +) + xprof_ng_module( name = "static_kernel_viewer", srcs = [ "kernel_event_tooltip.ts", + "kernel_selection_panel.ts", "static_kernel_viewer.ts", "static_kernel_viewer_module.ts", ], assets = [ ":kernel_event_tooltip_css", + ":kernel_selection_panel_css", ":static_kernel_viewer_css", "kernel_event_tooltip.ng.html", + "kernel_selection_panel.ng.html", "static_kernel_viewer.ng.html", ], deps = [ @@ -48,6 +56,7 @@ xprof_ng_module( testonly = True, srcs = [ "kernel_event_tooltip_test.ts", + "kernel_selection_panel_test.ts", "static_kernel_viewer_test.ts", ], deps = [ diff --git a/frontend/app/components/static_kernel_viewer/kernel_selection_panel.ng.html b/frontend/app/components/static_kernel_viewer/kernel_selection_panel.ng.html new file mode 100644 index 000000000..de648f8ac --- /dev/null +++ b/frontend/app/components/static_kernel_viewer/kernel_selection_panel.ng.html @@ -0,0 +1,810 @@ +
+
+
+
+ + + + {{ c.badgeLabel }} + + + {{ c.name }} + + {{ c.description }} + +
+ {{ chip }} +
+
+ +
+ {{ c.regionRowLabel ?? 'Region' }} +
+ + + + {{ crumb.name }} + + + +
+ + +
+
+ +
+
+ + + {{ spills }} + + + {{ c.ordinalLabel }} + + + {{ c.bundlesLabel }} + {{ c.bundles }} + {{ c.hexBundle }} + · {{ c.length }} + +
+ +
+ + +
+ + + +
+ +
+ + + +
+
+
+
+ + +
+ +
+
+ Dataflow & Properties + {{ c.registerTypeLabel }} +
+ +
+
+ {{ gauge.label }} +
+ + {{ gauge.formattedValue }} +
+
+ +
+
+ {{ out.rowLabel }} +
+ + {{ out.memorySpace }} + + {{ out.throughput }} +
+
+ +
+ Operands ({{ operands.length }}) +
+
+ {{ op.index }} + {{ op.text }} +
+
+
+ +
+ Consumers ({{ consumers.length }}) +
+ +
+
+ +
+ Vector spills +
+ + + {{ spills }} + +
+
+ +
+ Source +
+ + {{ src.shortLocation }} + {{ src.directory }} +
+
+ +
+ Stack ({{ stack.length }}) +
+
+ {{ frame.index }} + {{ frame.shortLocation }} + {{ frame.directory }} +
+
+
+ +
+ Attributes +
+ + {{ attr.key }}: + {{ attr.value }} + +
+
+
+ +
+
+ Functional unit + {{ c.unit.label }} — {{ c.unit.description }} +
+
+ {{ c.bundlesLabel }} + {{ c.bundles }} {{ c.hexBundle ?? '' }} +
+
+ Duration + {{ c.length }} +
+
+ Schedule offset + {{ pos.formattedPercent }} of {{ pos.totalBundles }} bundles +
+
+
+
+ + +
+
+ In This Bundle + {{ c.activeLanesSummary }} +
+ +
+
+ + {{ cell.unit.label }} + ×{{ cell.count }} + +
+ +
+ +
+ +
+ Single instruction scheduled in bundle {{ c.bundles }}. +
+
+
+
+ + +
+
+ Static Utilization + + {{ c.peakUtilizationLabel }} + +
+ +
+
+
+ + + {{ bar.unit.label }} + + + {{ bar.formattedPercent }} +
+
+
+ + +
+
+ + +
+ +
+
+ Instruction Mix + {{ c.density }} +
+ +
+
+
+ +
+ +
+ +
+
+ +
No instructions scheduled in this range.
+
+
+
+ + +
+
+ Mean Utilization + + → likely {{ boundUnit }}-bound + +
+ +
+
+
+ + + {{ bar.unit.label }} + + + {{ bar.formattedPercent }} +
+
+ +
+
+ Region Hierarchy + + {{ children.length }} {{ children.length === 1 ? 'child' : 'children' }} + +
+ +
+ + + +
+
+
+
+ + +
+
+
+ + + + Instructions + +
+ + +
+ +
+
+ +
+ +
+ +
+ +
+ No matching instructions in this selection. +
+
+
+
+
diff --git a/frontend/app/components/static_kernel_viewer/kernel_selection_panel.scss b/frontend/app/components/static_kernel_viewer/kernel_selection_panel.scss new file mode 100644 index 000000000..011c98a24 --- /dev/null +++ b/frontend/app/components/static_kernel_viewer/kernel_selection_panel.scss @@ -0,0 +1,1459 @@ +:host { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + min-height: 0; + box-sizing: border-box; + font-family: + 'Google Sans Text', + 'Google Sans', + Roboto, + -apple-system, + BlinkMacSystemFont, + sans-serif; + color: var(--mat-sys-on-surface, #1f1f1f); +} + +@keyframes panelFadeIn { + from { + opacity: 0; + transform: translateY(6px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.selection-panel { + --accent-color: #1a73e8; + + position: relative; + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + min-height: 0; + background: var(--mat-sys-surface-container-low, #f8f9fa); + border: 1px solid var(--mat-sys-outline-variant, #dadce0); + border-radius: 14px; + overflow: hidden; + box-sizing: border-box; + animation: panelFadeIn 160ms cubic-bezier(0.2, 0, 0, 1); + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: var(--accent-color); + z-index: 2; + } +} + +@media (prefers-reduced-motion: reduce) { + .selection-panel { + animation: none; + } +} + +.panel-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 7px 12px 6px; + background: var(--mat-sys-surface-container, #f1f3f4); + border-bottom: 1px solid var(--mat-sys-outline-variant, #dadce0); + flex-shrink: 0; + flex-wrap: wrap; +} + +.header-main { + display: flex; + flex-direction: column; + gap: 3px; + min-width: 0; + flex: 1 1 300px; +} + +.header-title-row { + display: flex; + align-items: center; + gap: 7px; + flex-wrap: wrap; + min-width: 0; +} + +.unit-chip { + --chip-color: #1a73e8; + + display: inline-flex; + align-items: center; + gap: 5px; + height: 21px; + padding: 0 8px; + border-radius: 999px; + background: color-mix(in srgb, var(--chip-color) 16%, transparent); + border: 1px solid color-mix(in srgb, var(--chip-color) 36%, transparent); + color: var(--mat-sys-on-surface, #1f1f1f); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.02em; + flex-shrink: 0; + + &.neutral-chip { + background: color-mix( + in srgb, + var(--mat-sys-primary, #1a73e8) 14%, + transparent + ); + border-color: color-mix( + in srgb, + var(--mat-sys-primary, #1a73e8) 32%, + transparent + ); + } +} + +.unit-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--chip-color, var(--unit-color, #1a73e8)); + flex-shrink: 0; +} + +.chip-icon { + width: 13px; + height: 13px; + font-size: 13px; + line-height: 13px; + color: var(--accent-color); +} + +.event-name { + font-size: 13.5px; + font-weight: 700; + color: var(--mat-sys-on-surface, #1f1f1f); + line-height: 1.2; + overflow-wrap: anywhere; + + &.code-name { + font-family: 'Roboto Mono', 'Google Sans Mono', monospace; + font-size: 13px; + } +} + +.unit-description { + font-size: 11px; + color: var(--mat-sys-on-surface-variant, #5f6368); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.subtitle-chips { + display: inline-flex; + align-items: center; + gap: 4px; + flex-wrap: wrap; +} + +.subtitle-chip { + display: inline-flex; + align-items: center; + height: 19px; + padding: 0 6px; + border-radius: 5px; + background: var(--mat-sys-surface-container-high, #e8eaed); + color: var(--mat-sys-on-surface-variant, #444746); + font-size: 10.5px; + font-weight: 500; + font-variant-numeric: tabular-nums; +} + +.header-breadcrumb-row { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; + font-size: 10.5px; +} + +.breadcrumb-label { + color: var(--mat-sys-on-surface-variant, #5f6368); + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + font-size: 9.5px; + flex-shrink: 0; +} + +.region-breadcrumb { + display: flex; + align-items: center; + gap: 3px; + flex-wrap: wrap; + min-width: 0; + max-height: 42px; + overflow-y: auto; + scrollbar-width: thin; +} + +.region-crumb { + font-family: 'Roboto Mono', 'Google Sans Mono', monospace; + font-size: 10.5px; + color: var(--mat-sys-on-surface-variant, #5f6368); + + &.innermost { + color: var(--mat-sys-on-surface, #1f1f1f); + font-weight: 600; + } + + &.interactive-crumb { + display: inline-flex; + align-items: center; + gap: 3px; + height: 18px; + padding: 0 6px; + border-radius: 4px; + background: var(--mat-sys-surface, #fff); + border: 1px solid var(--mat-sys-outline-variant, #dadce0); + cursor: pointer; + transition: + background-color 120ms ease, + border-color 120ms ease, + color 120ms ease; + + &:hover { + background: color-mix(in srgb, var(--accent-color) 14%, transparent); + border-color: color-mix(in srgb, var(--accent-color) 42%, transparent); + color: var(--mat-sys-on-surface, #1f1f1f); + } + } +} + +.crumb-icon { + width: 11px; + height: 11px; + font-size: 11px; + line-height: 11px; + color: var(--accent-color); + opacity: 0.85; +} + +.region-sep { + color: var(--mat-sys-outline, #747775); + font-size: 11px; + padding: 0 1px; +} + +.header-meta-actions { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; + flex-wrap: wrap; +} + +.meta-pills { + display: flex; + align-items: center; + gap: 5px; + flex-wrap: wrap; +} + +.parent-jump-pill { + display: inline-flex; + align-items: center; + gap: 3px; + height: 18px; + padding: 0 6px; + border-radius: 4px; + background: color-mix(in srgb, var(--accent-color) 13%, transparent); + border: 1px solid color-mix(in srgb, var(--accent-color) 36%, transparent); + color: var(--mat-sys-on-surface, #1f1f1f); + font-family: 'Roboto Mono', 'Google Sans Mono', monospace; + font-size: 10px; + font-weight: 600; + cursor: pointer; + flex-shrink: 0; + transition: + background-color 120ms ease, + border-color 120ms ease; + + &:hover { + background: color-mix(in srgb, var(--accent-color) 22%, transparent); + border-color: color-mix(in srgb, var(--accent-color) 52%, transparent); + } +} + +.parent-jump-icon { + width: 11px; + height: 11px; + font-size: 11px; + line-height: 11px; + color: var(--accent-color); +} + +.ordinal-pill { + display: inline-flex; + align-items: center; + height: 22px; + padding: 0 7px; + border-radius: 6px; + background: color-mix(in srgb, var(--accent-color) 12%, transparent); + border: 1px solid color-mix(in srgb, var(--accent-color) 28%, transparent); + color: var(--mat-sys-on-surface, #1f1f1f); + font-family: 'Roboto Mono', 'Google Sans Mono', monospace; + font-size: 10.5px; + font-weight: 600; + font-variant-numeric: tabular-nums; +} + +.spill-pill { + display: inline-flex; + align-items: center; + gap: 4px; + height: 22px; + padding: 0 7px; + border-radius: 6px; + background: var(--mat-sys-surface-container-high, #e8eaed); + border: 1px solid var(--mat-sys-outline-variant, #dadce0); + color: var(--mat-sys-on-surface-variant, #444746); + font-family: 'Roboto Mono', 'Google Sans Mono', monospace; + font-size: 10.5px; + font-weight: 600; + font-variant-numeric: tabular-nums; + + &.warn { + background: color-mix( + in srgb, + var(--mat-sys-error, #d93025) 14%, + transparent + ); + border-color: color-mix( + in srgb, + var(--mat-sys-error, #d93025) 34%, + transparent + ); + color: var(--mat-sys-error, #d93025); + } +} + +.spill-pill-icon { + width: 12px; + height: 12px; + font-size: 12px; + line-height: 12px; +} + +.bundle-pill { + display: inline-flex; + align-items: center; + gap: 4px; + height: 22px; + padding: 0 8px; + border-radius: 6px; + background: var(--mat-sys-surface-container-high, #e8eaed); + border: 1px solid var(--mat-sys-outline-variant, #dadce0); + font-size: 10.5px; + font-variant-numeric: tabular-nums; +} + +.bundle-pill-label { + color: var(--mat-sys-on-surface-variant, #5f6368); + font-weight: 500; +} + +.bundle-pill-value { + font-family: 'Roboto Mono', 'Google Sans Mono', monospace; + font-weight: 700; + color: var(--mat-sys-on-surface, #1f1f1f); +} + +.hex-bundle { + font-family: 'Roboto Mono', 'Google Sans Mono', monospace; + color: var(--mat-sys-on-surface-variant, #5f6368); + font-size: 10px; +} + +.bundle-length { + color: var(--mat-sys-on-surface-variant, #5f6368); +} + +.bottleneck-pill { + display: inline-flex; + align-items: center; + height: 22px; + padding: 0 8px; + border-radius: 999px; + background: color-mix(in srgb, var(--accent-color) 15%, transparent); + border: 1px solid color-mix(in srgb, var(--accent-color) 35%, transparent); + color: var(--mat-sys-on-surface, #1f1f1f); + font-size: 10.5px; + font-weight: 600; +} + +.action-buttons { + display: inline-flex; + align-items: center; + gap: 5px; +} + +.lane-stepper { + display: inline-flex; + align-items: center; + height: 24px; + border-radius: 6px; + background: var(--mat-sys-surface, #fff); + border: 1px solid var(--mat-sys-outline-variant, #dadce0); + overflow: hidden; + box-sizing: border-box; +} + +.stepper-divider { + width: 1px; + height: 14px; + background: var(--mat-sys-outline-variant, #dadce0); + flex-shrink: 0; +} + +.stepper-btn { + width: 25px; + height: 22px; + padding: 0; + border: none; + background: transparent; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--mat-sys-on-surface, #1f1f1f); + cursor: pointer; + transition: + background-color 120ms ease, + color 120ms ease, + opacity 120ms ease; + + mat-icon { + width: 17px; + height: 17px; + font-size: 17px; + line-height: 17px; + } + + &:hover:not(:disabled) { + background: color-mix(in srgb, var(--accent-color) 14%, transparent); + color: var(--accent-color); + } + + &:disabled { + color: var(--mat-sys-outline, #9aa0a6); + background: var(--mat-sys-surface-container-low, #f1f3f4); + opacity: 0.32; + cursor: not-allowed; + } +} + +.utility-actions { + display: inline-flex; + align-items: center; + gap: 2px; +} + +.panel-action-btn { + width: 24px; + height: 24px; + padding: 0; + border: 1px solid transparent; + border-radius: 6px; + background: transparent; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--mat-sys-on-surface-variant, #444746); + cursor: pointer; + transition: + background-color 120ms ease, + border-color 120ms ease, + color 120ms ease; + + mat-icon { + width: 16px; + height: 16px; + font-size: 16px; + line-height: 16px; + } + + &:hover:not(:disabled) { + background: var(--mat-sys-surface, #fff); + border-color: var(--mat-sys-outline-variant, #dadce0); + color: var(--mat-sys-on-surface, #1f1f1f); + } + + &:disabled { + opacity: 0.32; + cursor: not-allowed; + } + + &.copied { + color: #188038; + } +} + +.panel-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; + padding: 7px 10px 8px; + flex: 1; + min-height: 0; + overflow: hidden; + box-sizing: border-box; +} + +@media (max-width: 960px) { + .panel-grid { + grid-template-columns: 1fr; + overflow-y: auto; + } +} + +.panel-card { + display: flex; + flex-direction: column; + min-height: 0; + background: var(--mat-sys-surface, #fff); + border: 1px solid var(--mat-sys-outline-variant, #dadce0); + border-radius: 10px; + overflow: hidden; +} + +.card-section-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 5px 10px; + background: var(--mat-sys-surface-container-low, #f8f9fa); + border-bottom: 1px solid var(--mat-sys-outline-variant, #dadce0); + flex-shrink: 0; +} + +.section-title { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--mat-sys-on-surface-variant, #5f6368); + white-space: nowrap; +} + +.section-badge { + display: inline-flex; + align-items: center; + height: 17px; + padding: 0 6px; + border-radius: 5px; + background: var(--mat-sys-surface-container-high, #e8eaed); + color: var(--mat-sys-on-surface-variant, #444746); + font-size: 10px; + font-weight: 600; + font-variant-numeric: tabular-nums; + white-space: nowrap; + flex-shrink: 0; + + &.highlight-badge { + background: color-mix(in srgb, var(--accent-color) 14%, transparent); + color: var(--mat-sys-on-surface, #1f1f1f); + } +} + +.card-section-body { + display: flex; + flex-direction: column; + gap: 5px; + padding: 6px 10px; + overflow-y: auto; + flex: 1; + min-height: 0; + scrollbar-width: thin; + scrollbar-color: var(--mat-sys-outline-variant, #dadce0) transparent; + + &::-webkit-scrollbar { + width: 4px; + height: 4px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: var(--mat-sys-outline-variant, #dadce0); + border-radius: 999px; + } +} + +.card-section-footer { + display: flex; + flex-direction: column; + gap: 4px; + padding: 6px 10px 7px; + background: var(--mat-sys-surface-container-low, #f8f9fa); + border-top: 1px solid var(--mat-sys-outline-variant, #dadce0); + flex-shrink: 0; +} + +.position-header-row, +.hierarchy-header-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px; +} + +.footer-label { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--mat-sys-on-surface-variant, #5f6368); +} + +.hierarchy-count { + font-size: 10px; + color: var(--mat-sys-on-surface-variant, #5f6368); + font-variant-numeric: tabular-nums; +} + +.dataflow-grid { + display: flex; + flex-direction: column; + gap: 5px; +} + +.dataflow-row { + display: grid; + grid-template-columns: 68px minmax(0, 1fr); + align-items: start; + gap: 5px; +} + +.dataflow-label { + font-size: 10px; + font-weight: 600; + color: var(--mat-sys-on-surface-variant, #5f6368); + line-height: 19px; + white-space: nowrap; +} + +.llo-badges { + display: flex; + align-items: center; + gap: 3px; + flex-wrap: wrap; +} + +.register-pill { + display: inline-flex; + align-items: center; + height: 19px; + padding: 0 5px; + border-radius: 5px; + font-family: 'Roboto Mono', 'Google Sans Mono', monospace; + font-size: 10px; + font-weight: 600; + background: color-mix(in srgb, #1a73e8 14%, transparent); + border: 1px solid color-mix(in srgb, #1a73e8 34%, transparent); + color: var(--mat-sys-on-surface, #1f1f1f); + + &[data-kind='sreg'] { + background: color-mix(in srgb, #1e8e3e 14%, transparent); + border-color: color-mix(in srgb, #1e8e3e 34%, transparent); + } + + &[data-kind='preg'] { + background: color-mix(in srgb, #f29900 16%, transparent); + border-color: color-mix(in srgb, #f29900 38%, transparent); + } + + &[data-kind='vmreg'] { + background: color-mix(in srgb, #7c4dff 14%, transparent); + border-color: color-mix(in srgb, #7c4dff 34%, transparent); + } + + &.interactive-pill { + cursor: pointer; + transition: + background-color 120ms ease, + border-color 120ms ease; + + &:hover { + background: color-mix(in srgb, var(--accent-color) 22%, transparent); + border-color: color-mix(in srgb, var(--accent-color) 48%, transparent); + } + } + + &.compact-pill { + height: 16px; + padding: 0 5px; + font-size: 9.5px; + } +} + +.memory-pill, +.metadata-pill, +.throughput-pill { + display: inline-flex; + align-items: center; + height: 19px; + padding: 0 5px; + border-radius: 5px; + font-size: 10px; + font-weight: 600; +} + +.memory-pill { + font-family: 'Roboto Mono', 'Google Sans Mono', monospace; + background: color-mix(in srgb, #00a3bf 14%, transparent); + border: 1px solid color-mix(in srgb, #00a3bf 34%, transparent); + color: var(--mat-sys-on-surface, #1f1f1f); +} + +.metadata-pill { + background: var(--mat-sys-surface-container-high, #e8eaed); + border: 1px solid var(--mat-sys-outline-variant, #dadce0); + color: var(--mat-sys-on-surface, #1f1f1f); +} + +.throughput-pill { + background: color-mix(in srgb, #e8710a 14%, transparent); + border: 1px solid color-mix(in srgb, #e8710a 34%, transparent); + color: var(--mat-sys-on-surface, #1f1f1f); + font-variant-numeric: tabular-nums; +} + +.operand-list { + display: flex; + flex-wrap: wrap; + gap: 4px; +} + +.operand-item { + display: inline-flex; + align-items: center; + gap: 4px; + max-width: 130px; + height: 19px; + padding: 0 5px; + border-radius: 5px; + background: var(--mat-sys-surface-container-low, #f8f9fa); + border: 1px solid var(--mat-sys-outline-variant, #dadce0); + box-sizing: border-box; +} + +.operand-index { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 13px; + height: 13px; + padding: 0 2px; + border-radius: 3px; + background: var(--mat-sys-surface-container-high, #e8eaed); + color: var(--mat-sys-on-surface-variant, #5f6368); + font-family: 'Roboto Mono', 'Google Sans Mono', monospace; + font-size: 9px; + font-weight: 700; + flex-shrink: 0; +} + +.operand-text { + font-family: 'Roboto Mono', 'Google Sans Mono', monospace; + font-size: 10px; + line-height: 1.3; + color: var(--mat-sys-on-surface, #1f1f1f); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.consumer-list, +.attr-chip-list { + display: flex; + align-items: center; + gap: 4px; + flex-wrap: wrap; + min-width: 0; +} + +.consumer-chip { + display: inline-flex; + align-items: center; + gap: 4px; + max-width: 100%; + height: 19px; + padding: 0 6px; + border-radius: 5px; + background: var(--mat-sys-surface-container-low, #f8f9fa); + border: 1px solid var(--mat-sys-outline-variant, #dadce0); + color: var(--mat-sys-on-surface, #1f1f1f); + font-family: 'Roboto Mono', 'Google Sans Mono', monospace; + font-size: 10px; + box-sizing: border-box; + overflow: hidden; + cursor: pointer; + transition: + background-color 120ms ease, + border-color 120ms ease; + + &:hover { + background: color-mix(in srgb, var(--accent-color) 12%, transparent); + border-color: color-mix(in srgb, var(--accent-color) 36%, transparent); + } +} + +.consumer-arrow { + color: var(--accent-color); + font-weight: 700; + flex-shrink: 0; +} + +.consumer-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; +} + +.source-pill { + display: flex; + align-items: center; + gap: 5px; + min-width: 0; + max-width: 100%; + height: 20px; + padding: 0 6px; + border-radius: 5px; + background: color-mix(in srgb, var(--accent-color) 10%, transparent); + border: 1px solid color-mix(in srgb, var(--accent-color) 30%, transparent); + font-family: 'Roboto Mono', 'Google Sans Mono', monospace; + font-size: 10px; + box-sizing: border-box; + overflow: hidden; +} + +.source-pill-icon { + width: 12px; + height: 12px; + font-size: 12px; + line-height: 12px; + color: var(--accent-color); + flex-shrink: 0; +} + +.source-pill-file { + color: var(--mat-sys-on-surface, #1f1f1f); + font-weight: 700; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex-shrink: 0; + max-width: 100%; +} + +.source-pill-dir { + color: var(--mat-sys-on-surface-variant, #5f6368); + font-size: 9.5px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + flex: 1; +} + +.source-stack-list { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + max-width: 100%; +} + +.source-stack-frame { + display: flex; + align-items: center; + gap: 5px; + min-width: 0; + max-width: 100%; + height: 19px; + padding: 0 5px; + border-radius: 4px; + background: var(--mat-sys-surface-container-low, #f8f9fa); + border: 1px solid var(--mat-sys-outline-variant, #dadce0); + font-family: 'Roboto Mono', 'Google Sans Mono', monospace; + font-size: 10px; + box-sizing: border-box; + overflow: hidden; + + &.innermost-frame { + background: color-mix(in srgb, var(--accent-color) 10%, transparent); + border-color: color-mix(in srgb, var(--accent-color) 32%, transparent); + } +} + +.frame-index { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 13px; + height: 13px; + padding: 0 2px; + border-radius: 3px; + background: var(--mat-sys-surface-container-high, #e8eaed); + color: var(--mat-sys-on-surface-variant, #5f6368); + font-size: 9px; + font-weight: 700; + flex-shrink: 0; + + .innermost-frame & { + background: color-mix(in srgb, var(--accent-color) 20%, transparent); + color: var(--mat-sys-on-surface, #1f1f1f); + } +} + +.frame-file { + color: var(--mat-sys-on-surface, #1f1f1f); + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex-shrink: 0; + max-width: 100%; +} + +.frame-dir { + color: var(--mat-sys-on-surface-variant, #5f6368); + font-size: 9.5px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + flex: 1; +} + +.attr-chip { + display: inline-flex; + align-items: center; + gap: 4px; + max-width: 100%; + height: 19px; + padding: 0 6px; + border-radius: 4px; + background: var(--mat-sys-surface-container-low, #f8f9fa); + border: 1px solid var(--mat-sys-outline-variant, #dadce0); + font-family: 'Roboto Mono', 'Google Sans Mono', monospace; + font-size: 10px; + box-sizing: border-box; + overflow: hidden; +} + +.attr-chip-key { + color: var(--mat-sys-on-surface-variant, #5f6368); + white-space: nowrap; + flex-shrink: 0; +} + +.attr-chip-val { + color: var(--mat-sys-on-surface, #1f1f1f); + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; +} + +.kv-grid { + display: flex; + flex-direction: column; + gap: 4px; +} + +.kv-row { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 8px; + font-size: 11px; + padding: 2px 0; +} + +.kv-key { + color: var(--mat-sys-on-surface-variant, #5f6368); + flex-shrink: 0; +} + +.kv-value { + font-family: 'Roboto Mono', 'Google Sans Mono', monospace; + font-size: 10.5px; + color: var(--mat-sys-on-surface, #1f1f1f); + text-align: right; + overflow-wrap: anywhere; +} + +.bundle-strip { + display: grid; + grid-template-columns: repeat(8, minmax(0, 1fr)); + gap: 3px; + flex-shrink: 0; +} + +.bundle-cell { + --unit-color: #5f6368; + + display: flex; + align-items: center; + justify-content: center; + gap: 1px; + height: 18px; + border-radius: 4px; + background: var(--mat-sys-surface-container-low, #f1f3f4); + border: 1px solid var(--mat-sys-outline-variant, #dadce0); + color: var(--mat-sys-on-surface-variant, #5f6368); + font-size: 9.5px; + font-weight: 600; + white-space: nowrap; + opacity: 0.6; + cursor: default; + + &.active { + opacity: 1; + background: color-mix(in srgb, var(--unit-color) 18%, transparent); + border-color: color-mix(in srgb, var(--unit-color) 45%, transparent); + color: var(--mat-sys-on-surface, #1f1f1f); + font-weight: 700; + } +} + +.bundle-cell-count { + font-variant-numeric: tabular-nums; + font-size: 9px; +} + +.bundle-events-list, +.top-instructions-list, +.child-regions-list { + display: flex; + flex-direction: column; + gap: 2px; +} + +.bundle-event-row, +.top-instruction-row, +.child-region-row { + --row-unit-color: #1a73e8; + + position: relative; + display: flex; + align-items: center; + gap: 6px; + width: 100%; + padding: 1.5px 7px; + border-radius: 5px; + background: var(--mat-sys-surface-container-low, #f8f9fa); + border: 1px solid var(--mat-sys-outline-variant, #dadce0); + color: var(--mat-sys-on-surface, #1f1f1f); + text-align: left; + cursor: pointer; + box-sizing: border-box; + transition: + background-color 120ms ease, + border-color 120ms ease; + + &:hover { + background: color-mix(in srgb, var(--row-unit-color) 10%, transparent); + border-color: color-mix(in srgb, var(--row-unit-color) 38%, transparent); + } + + &.is-selected { + background: color-mix(in srgb, var(--row-unit-color) 15%, transparent); + border-color: color-mix(in srgb, var(--row-unit-color) 50%, transparent); + border-left: 3px solid var(--row-unit-color); + padding-left: 5px; + } +} + +.top-instruction-row { + --share-pct: 0%; + + background: linear-gradient( + 90deg, + color-mix(in srgb, var(--row-unit-color) 11%, transparent) var(--share-pct), + var(--mat-sys-surface-container-low, #f8f9fa) var(--share-pct) + ); +} + +.row-unit-tag { + --unit-color: #1a73e8; + + display: inline-flex; + align-items: center; + gap: 4px; + height: 16px; + padding: 0 5px; + border-radius: 4px; + background: color-mix(in srgb, var(--unit-color) 15%, transparent); + font-size: 9.5px; + font-weight: 700; + flex-shrink: 0; +} + +.row-mnemonic { + font-family: 'Roboto Mono', 'Google Sans Mono', monospace; + font-size: 10.5px; + line-height: 1.3; + font-weight: 600; + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.row-ordinal, +.row-count, +.row-share, +.row-bundle { + font-family: 'Roboto Mono', 'Google Sans Mono', monospace; + font-size: 10px; + color: var(--mat-sys-on-surface-variant, #5f6368); + font-variant-numeric: tabular-nums; + flex-shrink: 0; +} + +.row-count { + font-weight: 700; + color: var(--mat-sys-on-surface, #1f1f1f); +} + +.util-bars-list { + display: flex; + flex-direction: column; + gap: 4px; + + &.compact-util-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 3px 10px; + } +} + +.util-bar-row { + display: grid; + grid-template-columns: 44px 1fr 30px; + align-items: center; + gap: 5px; + font-size: 10px; + + &.highlighted { + font-weight: 700; + } +} + +.util-unit-label { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 10px; + color: var(--mat-sys-on-surface, #1f1f1f); +} + +.util-track, +.gauge-track, +.mix-bar-track { + position: relative; + height: 6px; + border-radius: 999px; + background: var(--mat-sys-surface-container-highest, #dee1e5); + overflow: hidden; +} + +.util-fill, +.gauge-fill, +.mix-bar-fill { + display: block; + height: 100%; + border-radius: 999px; + transition: width 160ms ease; +} + +.util-pct { + text-align: right; + font-family: 'Roboto Mono', 'Google Sans Mono', monospace; + font-size: 10px; + font-variant-numeric: tabular-nums; + color: var(--mat-sys-on-surface, #1f1f1f); +} + +.gauge-row { + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + gap: 8px; +} + +.gauge-value { + font-family: 'Roboto Mono', 'Google Sans Mono', monospace; + font-size: 11px; + font-weight: 700; + font-variant-numeric: tabular-nums; +} + +.position-track { + position: relative; + display: block; + height: 6px; + border-radius: 999px; + background: var(--mat-sys-surface-container-highest, #dee1e5); +} + +.position-fill { + display: block; + height: 100%; + border-radius: 999px; + background: color-mix(in srgb, var(--accent-color) 55%, transparent); +} + +.position-marker { + position: absolute; + top: 50%; + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--accent-color); + border: 2px solid var(--mat-sys-surface, #fff); + transform: translate(-50%, -50%); +} + +.position-caption { + font-size: 10.5px; + color: var(--mat-sys-on-surface-variant, #5f6368); + font-variant-numeric: tabular-nums; +} + +.mix-container { + display: flex; + flex-direction: column; + gap: 4px; +} + +.stacked-mix-bar { + display: flex; + width: 100%; + height: 8px; + border-radius: 999px; + overflow: hidden; + background: var(--mat-sys-surface-container-highest, #dee1e5); + gap: 1px; + flex-shrink: 0; +} + +.stacked-segment { + height: 100%; + min-width: 4px; + padding: 0; + border: none; + cursor: pointer; + transition: + opacity 120ms ease, + filter 120ms ease; + + &.is-hovered, + &.active-filter { + filter: brightness(1.08); + } + + &.is-dimmed { + opacity: 0.38; + } +} + +.mix-rows-list { + display: flex; + flex-direction: column; + gap: 1.5px; +} + +.mix-unit-row { + display: grid; + grid-template-columns: 88px 1fr 32px; + align-items: center; + gap: 6px; + width: 100%; + padding: 1.5px 6px; + border-radius: 5px; + background: transparent; + border: 1px solid transparent; + color: var(--mat-sys-on-surface, #1f1f1f); + cursor: pointer; + box-sizing: border-box; + text-align: left; + transition: + background-color 120ms ease, + border-color 120ms ease; + + &:hover, + &.is-hovered { + background: var(--mat-sys-surface-container-low, #f8f9fa); + border-color: var(--mat-sys-outline-variant, #dadce0); + } + + &.active-filter { + background: color-mix(in srgb, var(--accent-color) 12%, transparent); + border-color: color-mix(in srgb, var(--accent-color) 40%, transparent); + } +} + +.mix-unit-info { + display: inline-flex; + align-items: center; + gap: 5px; + min-width: 0; +} + +.mix-unit-name { + font-size: 10px; + font-weight: 700; +} + +.mix-unit-count { + font-size: 9.5px; + color: var(--mat-sys-on-surface-variant, #5f6368); + font-variant-numeric: tabular-nums; +} + +.mix-unit-pct { + text-align: right; + font-family: 'Roboto Mono', 'Google Sans Mono', monospace; + font-size: 10px; + font-weight: 600; + font-variant-numeric: tabular-nums; +} + +.hierarchy-block { + display: flex; + flex-direction: column; + gap: 2.5px; + padding-top: 4px; + border-top: 1px solid var(--mat-sys-outline-variant, #dadce0); +} + +.parent-region-row { + background: color-mix(in srgb, var(--accent-color) 7%, transparent); + border-color: color-mix(in srgb, var(--accent-color) 26%, transparent); +} + +.hierarchy-badge { + display: inline-flex; + align-items: center; + gap: 2px; + height: 15px; + padding: 0 5px; + border-radius: 4px; + font-size: 9px; + font-weight: 700; + flex-shrink: 0; + + &.parent-badge { + background: color-mix(in srgb, var(--accent-color) 16%, transparent); + color: var(--mat-sys-on-surface, #1f1f1f); + } + + &.child-badge { + background: var(--mat-sys-surface-container-high, #e8eaed); + color: var(--mat-sys-on-surface-variant, #444746); + } +} + +.hierarchy-badge-icon { + width: 11px; + height: 11px; + font-size: 11px; + line-height: 11px; +} + +.child-region-name { + font-family: 'Roboto Mono', 'Google Sans Mono', monospace; + font-size: 10.5px; + line-height: 1.3; + font-weight: 600; + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.child-region-meta { + font-size: 10px; + color: var(--mat-sys-on-surface-variant, #5f6368); + font-variant-numeric: tabular-nums; + flex-shrink: 0; +} + +.section-tabs { + display: inline-flex; + align-items: center; + gap: 4px; +} + +.tab-pill { + height: 20px; + padding: 0 7px; + border-radius: 5px; + border: 1px solid transparent; + background: transparent; + color: var(--mat-sys-on-surface-variant, #5f6368); + font-size: 10px; + font-weight: 700; + cursor: pointer; + + &.active { + background: var(--mat-sys-surface, #fff); + border-color: var(--mat-sys-outline-variant, #dadce0); + color: var(--mat-sys-on-surface, #1f1f1f); + } +} + +.filter-clear-pill { + display: inline-flex; + align-items: center; + gap: 3px; + height: 19px; + padding: 0 6px; + border-radius: 999px; + background: color-mix(in srgb, var(--accent-color) 14%, transparent); + border: 1px solid color-mix(in srgb, var(--accent-color) 35%, transparent); + color: var(--mat-sys-on-surface, #1f1f1f); + font-size: 10px; + font-weight: 600; + cursor: pointer; + + mat-icon { + width: 11px; + height: 11px; + font-size: 11px; + line-height: 11px; + } +} + +.empty-hint { + font-size: 11px; + color: var(--mat-sys-on-surface-variant, #5f6368); + padding: 6px 2px; +} diff --git a/frontend/app/components/static_kernel_viewer/kernel_selection_panel.ts b/frontend/app/components/static_kernel_viewer/kernel_selection_panel.ts new file mode 100644 index 000000000..af6a30652 --- /dev/null +++ b/frontend/app/components/static_kernel_viewer/kernel_selection_panel.ts @@ -0,0 +1,983 @@ +import {CommonModule} from '@angular/common'; +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + inject, + NgZone, + OnDestroy, + Output, +} from '@angular/core'; +import {MatButtonModule} from '@angular/material/button'; +import {MatIconModule} from '@angular/material/icon'; +import {MatTooltipModule} from '@angular/material/tooltip'; +import { + buildInstructionMix, + buildKernelEventTooltip, + buildUtilizationBars, + BUNDLE_FORMAT, + classifyRegister, + DEFAULT_ACCENT_COLOR, + DENSITY_FORMAT, + FUNCTIONAL_UNITS_BY_LANE, + readHoveredEvent, + readNumberRecord, + type BundleLaneCell, + type CounterGauge, + type FunctionalUnit, + type InstructionMixItem, + type InstructionOutput, + type OperandItem, + type RegionUtilizationSummary, + type SchedulePosition, + type UtilizationBar, +} from 'org_xprof/frontend/app/components/static_kernel_viewer/kernel_event_tooltip'; +import { + EVENT_SELECTED_EVENT_NAME, + EVENTS_SELECTED_EVENT_NAME, +} from 'org_xprof/frontend/app/components/trace_viewer_container/trace_viewer_container'; + +/** Co-scheduled instruction executing in the same VLIW bundle. */ +export interface BundleEventItem { + readonly eventIndex: number; + readonly name: string; + readonly trackName: string; + readonly unit?: FunctionalUnit; + readonly start: number; + readonly duration: number; + readonly selected: boolean; + readonly produces?: string; + readonly registerKind?: NonNullable; + readonly ordinalLabel?: string; +} + +/** Navigable region item (parent or child region in the schedule hierarchy). */ +export interface ChildRegionItem { + readonly eventIndex: number; + readonly name: string; + readonly bundles: string; + readonly durationLabel: string; +} + +/** Enclosing ancestor region crumb in the header breadcrumb bar. */ +export interface RegionBreadcrumbItem { + readonly name: string; + readonly eventIndex?: number; + readonly bundles?: string; + readonly durationLabel?: string; +} + +/** Aggregated instruction mnemonic frequency inside a selected region or range. */ +export interface TopInstructionItem { + readonly name: string; + readonly trackName: string; + readonly unit?: FunctionalUnit; + readonly count: number; + readonly sharePercent: number; + readonly formattedShare: string; + readonly totalBundlesLabel: string; + readonly firstEventIndex: number; +} + +/** Individual event inside a multi-event rectangle selection. */ +export interface SelectedRangeEventItem { + readonly eventIndex: number; + readonly name: string; + readonly trackName: string; + readonly unit?: FunctionalUnit; + readonly bundleLabel: string; + readonly durationLabel: string; +} + +/** Key-value attribute not already surfaced as a dedicated LLO badge. */ +export interface ExtraAttributeItem { + readonly key: string; + readonly value: string; +} + +/** Parsed source file and line/column location for a source or stack frame. */ +export interface SourceFrameItem { + /** Full raw location string, e.g. `learning/gemini/.../megablocks_gmm.py:627:5`. */ + readonly raw: string; + /** File basename with `:line[:col]` suffix, e.g. `megablocks_gmm.py:627:5`. */ + readonly shortLocation: string; + /** Parent directory path without trailing slash, e.g. `learning/gemini/.../kernels`. */ + readonly directory?: string; + /** 0-based index in the call stack (0 = outermost caller, last = innermost callee). */ + readonly index: number; + /** True when this frame is the innermost leaf frame where the op was emitted. */ + readonly isInnermost: boolean; +} + +/** View model for the Static Kernel Viewer bottom selection panel. */ +export interface KernelSelectionContent { + readonly mode: 'single' | 'range'; + readonly kind: 'instruction' | 'region' | 'counter' | 'process' | 'range'; + readonly name: string; + readonly isCode: boolean; + readonly unit?: FunctionalUnit; + readonly accentColor: string; + readonly badgeLabel: string; + readonly badgeIcon: string; + readonly bundlesLabel: string; + readonly bundles: string; + readonly length?: string; + readonly hexBundle?: string; + readonly ordinalLabel?: string; + readonly subtitleChips?: readonly string[]; + readonly description?: string; + readonly eventIndex?: number; + readonly prevEventIndex?: number; + readonly nextEventIndex?: number; + readonly output?: InstructionOutput; + readonly registerTypeLabel?: string; + readonly operands?: readonly OperandItem[]; + readonly consumers?: readonly string[]; + readonly spillsLabel?: string; + readonly hasSpills?: boolean; + readonly sourceInfo?: SourceFrameItem; + readonly sourceStack?: readonly SourceFrameItem[]; + readonly extraAttributes?: readonly ExtraAttributeItem[]; + readonly bundleStrip?: readonly BundleLaneCell[]; + readonly activeLanesSummary?: string; + readonly bundleEvents?: readonly BundleEventItem[]; + readonly utilizationBars?: readonly UtilizationBar[]; + readonly peakUtilizationLabel?: string; + readonly counterGauge?: CounterGauge; + readonly regions?: readonly string[]; + readonly regionBreadcrumbs?: readonly RegionBreadcrumbItem[]; + readonly regionRowLabel?: string; + readonly parentRegion?: ChildRegionItem; + readonly schedulePosition?: SchedulePosition; + readonly instructionMix?: readonly InstructionMixItem[]; + readonly totalInstructions: number; + readonly meanUtilization?: RegionUtilizationSummary; + readonly density?: string; + readonly childRegions?: readonly ChildRegionItem[]; + readonly topInstructions?: readonly TopInstructionItem[]; + readonly selectedEvents?: readonly SelectedRangeEventItem[]; +} + +/** Keys in `args` that are already rendered as dedicated LLO badges or rows. */ +const DEDICATED_ARG_KEYS = new Set([ + 'ordinal', + 'program_order', + 'instruction_ordinal', + 'bundle', + 'opcode', + 'unit', + 'start_bundle', + 'limit_bundle', + 'produces', + 'dest', + 'result', + 'result_register', + 'memory_space', + 'memorySpace', + 'throughput', + 'metadata', + 'data_format', + 'pseudo_kind', + 'treat_copy_as_opaque', + 'operands', + 'consumers', + 'vector_spills', + 'vectorSpills', + 'vector_fills', + 'vectorFills', + 'hlo_module', + 'hlo_op', + 'uid', + 'source', + 'source_stack', +]); + +const REGISTER_TYPE_LABELS: Readonly< + Record, string> +> = { + vreg: 'Vector Reg', + sreg: 'Scalar Reg', + preg: 'Predicate Reg', + vmreg: 'Vector Mask', + other: 'Register', +}; + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function formatBundleRange(start: number, duration: number): string { + const first = BUNDLE_FORMAT.format(Math.floor(start)); + if (duration <= 1) { + return first; + } + return `${first}–${BUNDLE_FORMAT.format(Math.ceil(start + duration))}`; +} + +function readBundleEvents( + raw: unknown, +): readonly BundleEventItem[] | undefined { + if (!Array.isArray(raw)) { + return undefined; + } + const items: BundleEventItem[] = []; + for (const entry of raw) { + if (!isRecord(entry)) continue; + const name = entry['name']; + const trackName = + typeof entry['trackName'] === 'string' ? entry['trackName'] : ''; + const start = typeof entry['startUs'] === 'number' ? entry['startUs'] : 0; + const duration = + typeof entry['durationUs'] === 'number' ? entry['durationUs'] : 1; + const eventIndex = + typeof entry['eventIndex'] === 'number' ? entry['eventIndex'] : -1; + if (typeof name !== 'string' || !name) continue; + const unit = FUNCTIONAL_UNITS_BY_LANE.get(trackName); + const produces = + typeof entry['produces'] === 'string' && entry['produces'].trim() !== '' + ? entry['produces'].trim() + : undefined; + const ordinalRaw = entry['ordinal']; + let ordinalLabel: string | undefined = undefined; + if (typeof ordinalRaw === 'number' && Number.isFinite(ordinalRaw)) { + ordinalLabel = `Ord #${BUNDLE_FORMAT.format(Math.round(ordinalRaw))}`; + } else if ( + typeof ordinalRaw === 'string' && + /^\d+$/.test(ordinalRaw.trim()) + ) { + ordinalLabel = `Ord #${BUNDLE_FORMAT.format(Number(ordinalRaw.trim()))}`; + } + items.push({ + eventIndex, + name, + trackName, + ...(unit ? {unit} : {}), + start, + duration, + selected: entry['selected'] === true, + ...(produces ? {produces, registerKind: classifyRegister(produces)} : {}), + ...(ordinalLabel ? {ordinalLabel} : {}), + }); + } + return items.length > 0 ? items : undefined; +} + +function readChildRegions( + raw: unknown, +): readonly ChildRegionItem[] | undefined { + if (!Array.isArray(raw)) { + return undefined; + } + const items: ChildRegionItem[] = []; + for (const entry of raw) { + if (!isRecord(entry)) continue; + const name = entry['name']; + const start = typeof entry['startUs'] === 'number' ? entry['startUs'] : 0; + const duration = + typeof entry['durationUs'] === 'number' ? entry['durationUs'] : 0; + const eventIndex = + typeof entry['eventIndex'] === 'number' ? entry['eventIndex'] : -1; + if (typeof name !== 'string' || !name) continue; + items.push({ + eventIndex, + name, + bundles: formatBundleRange(start, duration), + durationLabel: `${BUNDLE_FORMAT.format(Math.max(1, Math.round(duration)))} ${Math.round(duration) === 1 ? 'bundle' : 'bundles'}`, + }); + } + return items.length > 0 ? items : undefined; +} + +function buildRegionBreadcrumbs( + regions: readonly string[] | undefined, + ancestors: readonly ChildRegionItem[] | undefined, +): readonly RegionBreadcrumbItem[] | undefined { + if (ancestors && ancestors.length > 0) { + const items: RegionBreadcrumbItem[] = ancestors.map((item) => ({ + name: item.name, + ...(item.eventIndex >= 0 ? {eventIndex: item.eventIndex} : {}), + bundles: item.bundles, + durationLabel: item.durationLabel, + })); + if (regions && regions.length > items.length) { + for (let i = items.length; i < regions.length; i++) { + items.push({name: regions[i]}); + } + } + return items; + } + if (regions && regions.length > 0) { + return regions.map((name) => ({name})); + } + return undefined; +} + +function readTopInstructions( + raw: unknown, + totalInstructions: number, +): readonly TopInstructionItem[] | undefined { + if (!Array.isArray(raw)) { + return undefined; + } + const items: TopInstructionItem[] = []; + for (const entry of raw) { + if (!isRecord(entry)) continue; + const name = entry['name']; + const trackName = + typeof entry['trackName'] === 'string' ? entry['trackName'] : ''; + const count = + typeof entry['count'] === 'number' ? Math.round(entry['count']) : 0; + const totalBundles = + typeof entry['totalBundles'] === 'number' + ? entry['totalBundles'] + : typeof entry['wallTimeUs'] === 'number' + ? entry['wallTimeUs'] + : count; + const firstEventIndex = + typeof entry['firstEventIndex'] === 'number' + ? entry['firstEventIndex'] + : -1; + if (typeof name !== 'string' || !name || count <= 0) continue; + const unit = FUNCTIONAL_UNITS_BY_LANE.get(trackName); + const sharePercent = + totalInstructions > 0 + ? Math.max( + 0, + Math.min(100, Math.round((count / totalInstructions) * 100)), + ) + : 0; + items.push({ + name, + trackName, + ...(unit ? {unit} : {}), + count, + sharePercent, + formattedShare: `${sharePercent}%`, + totalBundlesLabel: `${BUNDLE_FORMAT.format(Math.round(totalBundles))} ${Math.round(totalBundles) === 1 ? 'bundle' : 'bundles'}`, + firstEventIndex, + }); + } + return items.length > 0 ? items : undefined; +} + +function readSelectedRangeEvents( + raw: unknown, +): readonly SelectedRangeEventItem[] | undefined { + if (!Array.isArray(raw)) { + return undefined; + } + const items: SelectedRangeEventItem[] = []; + for (const entry of raw) { + if (!isRecord(entry)) continue; + const name = entry['name']; + const trackName = + typeof entry['trackName'] === 'string' ? entry['trackName'] : ''; + const start = typeof entry['startUs'] === 'number' ? entry['startUs'] : 0; + const duration = + typeof entry['durationUs'] === 'number' ? entry['durationUs'] : 1; + const eventIndex = + typeof entry['eventIndex'] === 'number' ? entry['eventIndex'] : -1; + if (typeof name !== 'string' || !name) continue; + const unit = FUNCTIONAL_UNITS_BY_LANE.get(trackName); + items.push({ + eventIndex, + name, + trackName, + ...(unit ? {unit} : {}), + bundleLabel: formatBundleRange(start, duration), + durationLabel: `${BUNDLE_FORMAT.format(Math.max(1, Math.round(duration)))} ${Math.round(duration) === 1 ? 'bundle' : 'bundles'}`, + }); + } + return items.length > 0 ? items : undefined; +} + +function parseSourceFrame( + raw: string, + index = 0, + isInnermost = true, +): SourceFrameItem { + const trimmed = raw.trim(); + const lastSlash = trimmed.lastIndexOf('/'); + const directory = lastSlash > 0 ? trimmed.slice(0, lastSlash) : undefined; + const shortLocation = lastSlash >= 0 ? trimmed.slice(lastSlash + 1) : trimmed; + return { + raw: trimmed, + shortLocation: shortLocation || trimmed, + ...(directory ? {directory} : {}), + index, + isInnermost, + }; +} + +function parseSourceStack( + rawStack: string | undefined, +): readonly SourceFrameItem[] | undefined { + if (!rawStack) { + return undefined; + } + const lines = rawStack + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line !== ''); + if (lines.length === 0) { + return undefined; + } + return lines.map((line, index) => + parseSourceFrame(line, index, index === lines.length - 1), + ); +} + +/** + * Builds the bottom selection panel content from an `eventselected` detail + * payload. Returns `undefined` when deselected (`eventIndex === -1` and not a + * counter track selection). + */ +export function buildKernelSelectionFromEvent( + detail: unknown, +): KernelSelectionContent | undefined { + if (!isRecord(detail)) { + return undefined; + } + const hovered = readHoveredEvent(detail); + if (!hovered) { + return undefined; + } + const tooltip = buildKernelEventTooltip(hovered); + const eventIndex = + typeof detail['eventIndex'] === 'number' && detail['eventIndex'] >= 0 + ? detail['eventIndex'] + : undefined; + const prevEventIndex = + typeof detail['prevEventIndex'] === 'number' && + detail['prevEventIndex'] >= 0 + ? detail['prevEventIndex'] + : undefined; + const nextEventIndex = + typeof detail['nextEventIndex'] === 'number' && + detail['nextEventIndex'] >= 0 + ? detail['nextEventIndex'] + : undefined; + + const isRegion = tooltip.kind === 'region'; + const mode: KernelSelectionContent['mode'] = isRegion ? 'range' : 'single'; + const badgeLabel = isRegion + ? 'Region' + : tooltip.kind === 'counter' + ? 'Counter' + : (tooltip.unit?.label ?? 'Instruction'); + const badgeIcon = isRegion + ? 'account_tree' + : tooltip.kind === 'counter' + ? 'monitoring' + : 'memory'; + + const sourceStack = parseSourceStack(hovered.args?.['source_stack']); + const rawSource = hovered.args?.['source']?.trim(); + const sourceInfo = rawSource + ? parseSourceFrame(rawSource, 0, true) + : sourceStack && sourceStack.length > 0 + ? sourceStack[sourceStack.length - 1] + : undefined; + + const extraAttributes: ExtraAttributeItem[] = []; + if (hovered.args) { + for (const [key, value] of Object.entries(hovered.args)) { + if (!DEDICATED_ARG_KEYS.has(key) && value.trim() !== '') { + const normalizedValue = value.replace(/\s*\r?\n\s*/g, ' ↵ ').trim(); + extraAttributes.push({key, value: normalizedValue}); + } + } + } + + const bundleEvents = readBundleEvents(detail['bundleEvents']); + const activeLanes = tooltip.bundleStrip + ? tooltip.bundleStrip.filter((cell) => cell.active).length + : 0; + const totalOpsInBundle = tooltip.bundleStrip + ? tooltip.bundleStrip.reduce((sum, cell) => sum + cell.count, 0) + : (bundleEvents?.length ?? 0); + const activeLanesSummary = + totalOpsInBundle > 0 && tooltip.bundleStrip + ? `${totalOpsInBundle} ${totalOpsInBundle === 1 ? 'op' : 'ops'} · ${activeLanes}/${tooltip.bundleStrip.length} lanes active` + : undefined; + + const utilizationBars = + tooltip.utilizationBars ?? buildUtilizationBars(hovered.utilization); + const peakBar = utilizationBars?.[0]; + const peakUtilizationLabel = + peakBar && peakBar.percent > 0 + ? `Peak: ${peakBar.unit.label} ${peakBar.formattedPercent}` + : undefined; + + const mixResult = buildInstructionMix(hovered.bundleCounts); + const totalInstructions = + typeof detail['totalInstructions'] === 'number' && + detail['totalInstructions'] > 0 + ? Math.round(detail['totalInstructions']) + : mixResult.totalInstructions; + const regionAncestors = readChildRegions(detail['regionAncestors']); + const regionBreadcrumbs = buildRegionBreadcrumbs( + tooltip.regions, + regionAncestors, + ); + const parentRegion = + regionAncestors && regionAncestors.length > 0 + ? regionAncestors[regionAncestors.length - 1] + : undefined; + const childRegions = readChildRegions(detail['childRegions']); + const topInstructions = readTopInstructions( + detail['topInstructions'], + totalInstructions, + ); + + const subtitleChips = [...(tooltip.subtitleChips ?? [])]; + if (isRegion && totalInstructions > 0) { + subtitleChips.push( + `${BUNDLE_FORMAT.format(totalInstructions)} instructions`, + ); + } + + return { + mode, + kind: tooltip.kind, + name: tooltip.name, + isCode: tooltip.isCode, + ...(tooltip.unit ? {unit: tooltip.unit} : {}), + accentColor: tooltip.accentColor, + badgeLabel, + badgeIcon, + bundlesLabel: tooltip.bundlesLabel, + bundles: tooltip.bundles, + ...(tooltip.length ? {length: tooltip.length} : {}), + ...(tooltip.hexBundle ? {hexBundle: tooltip.hexBundle} : {}), + ...(tooltip.ordinalLabel ? {ordinalLabel: tooltip.ordinalLabel} : {}), + ...(subtitleChips.length > 0 ? {subtitleChips} : {}), + ...(tooltip.description ? {description: tooltip.description} : {}), + ...(eventIndex !== undefined ? {eventIndex} : {}), + ...(prevEventIndex !== undefined ? {prevEventIndex} : {}), + ...(nextEventIndex !== undefined ? {nextEventIndex} : {}), + ...(tooltip.output + ? { + output: tooltip.output, + ...(tooltip.output.registerKind + ? { + registerTypeLabel: + REGISTER_TYPE_LABELS[tooltip.output.registerKind], + } + : {}), + } + : {}), + ...(tooltip.operands ? {operands: tooltip.operands} : {}), + ...(tooltip.consumers ? {consumers: tooltip.consumers} : {}), + ...(tooltip.spillsLabel + ? {spillsLabel: tooltip.spillsLabel, hasSpills: tooltip.hasSpills} + : {}), + ...(sourceInfo ? {sourceInfo} : {}), + ...(sourceStack ? {sourceStack} : {}), + ...(extraAttributes.length > 0 ? {extraAttributes} : {}), + ...(tooltip.bundleStrip ? {bundleStrip: tooltip.bundleStrip} : {}), + ...(activeLanesSummary ? {activeLanesSummary} : {}), + ...(bundleEvents ? {bundleEvents} : {}), + ...(utilizationBars ? {utilizationBars} : {}), + ...(peakUtilizationLabel ? {peakUtilizationLabel} : {}), + ...(tooltip.counterGauge ? {counterGauge: tooltip.counterGauge} : {}), + ...(tooltip.regions + ? {regions: tooltip.regions, regionRowLabel: tooltip.regionRowLabel} + : {}), + ...(regionBreadcrumbs ? {regionBreadcrumbs} : {}), + ...(parentRegion && parentRegion.eventIndex >= 0 ? {parentRegion} : {}), + ...(tooltip.schedulePosition + ? {schedulePosition: tooltip.schedulePosition} + : {}), + ...(tooltip.instructionMix ? {instructionMix: tooltip.instructionMix} : {}), + totalInstructions, + ...(tooltip.meanUtilization + ? {meanUtilization: tooltip.meanUtilization} + : {}), + ...(tooltip.density ? {density: tooltip.density} : {}), + ...(childRegions ? {childRegions} : {}), + ...(topInstructions ? {topInstructions} : {}), + }; +} + +/** + * Builds the bottom selection panel content from an `events_selected` + * multi-event rectangle selection payload. Returns `undefined` when cleared. + */ +export function buildKernelSelectionFromRange( + detail: unknown, +): KernelSelectionContent | undefined { + if (!isRecord(detail)) { + return undefined; + } + + let parsedJson: Record | undefined = undefined; + if ( + typeof detail['events_selected_data'] === 'string' && + detail['events_selected_data'].trim() !== '' + ) { + try { + const parsed: unknown = JSON.parse(detail['events_selected_data']); + if (isRecord(parsed)) { + parsedJson = parsed; + } + } catch { + // Ignore malformed JSON fallback. + } + } + + const start = + typeof detail['selectionStart'] === 'number' + ? Math.max(0, Math.floor(detail['selectionStart'])) + : typeof parsedJson?.['selectionStartUs'] === 'number' + ? Math.max(0, Math.floor(parsedJson['selectionStartUs'])) + : undefined; + const extent = + typeof detail['selectionExtent'] === 'number' + ? Math.max(0, Math.round(detail['selectionExtent'])) + : typeof parsedJson?.['selectionExtentUs'] === 'number' + ? Math.max(0, Math.round(parsedJson['selectionExtentUs'])) + : undefined; + + const bundleCounts = readNumberRecord(detail['bundleCounts']); + const utilization = readNumberRecord(detail['utilization']); + const mixResult = buildInstructionMix(bundleCounts); + const utilizationBars = buildUtilizationBars(utilization); + + const selectedEvents = + readSelectedRangeEvents(detail['selectedEvents']) ?? + readSelectedRangeEvents(parsedJson?.['rawEvents']); + + const totalInstructions = + typeof detail['totalInstructions'] === 'number' && + detail['totalInstructions'] > 0 + ? Math.round(detail['totalInstructions']) + : mixResult.totalInstructions > 0 + ? mixResult.totalInstructions + : (selectedEvents?.length ?? 0); + + const totalSelectedEvents = + typeof detail['totalSelectedEvents'] === 'number' + ? Math.round(detail['totalSelectedEvents']) + : (selectedEvents?.length ?? totalInstructions); + + if ( + totalSelectedEvents <= 0 && + totalInstructions <= 0 && + !selectedEvents && + !parsedJson?.['metrics'] + ) { + return undefined; + } + + const topInstructions = + readTopInstructions(detail['topInstructions'], totalInstructions) ?? + readTopInstructions(parsedJson?.['metrics'], totalInstructions); + + const rangeStart = start ?? 0; + const rangeExtent = extent && extent > 0 ? extent : 1; + const bundles = formatBundleRange(rangeStart, rangeExtent); + const length = `${BUNDLE_FORMAT.format(rangeExtent)} ${rangeExtent === 1 ? 'bundle' : 'bundles'}`; + + const subtitleChips: string[] = [length]; + const totalBundles = + typeof detail['totalBundles'] === 'number' && detail['totalBundles'] > 0 + ? detail['totalBundles'] + : undefined; + if (totalBundles !== undefined) { + const share = Math.max( + 0, + Math.min(100, Math.round((rangeExtent / totalBundles) * 100)), + ); + subtitleChips.push(`${share}% of static schedule`); + } + if (totalInstructions > 0) { + subtitleChips.push( + `${BUNDLE_FORMAT.format(totalInstructions)} ${totalInstructions === 1 ? 'instruction' : 'instructions'}`, + ); + } + + let meanUtilization: RegionUtilizationSummary | undefined = undefined; + if (utilizationBars && utilizationBars.length > 0) { + const top = utilizationBars[0]; + meanUtilization = { + items: utilizationBars, + ...(top.percent >= 20 ? {boundUnit: top.unit.label} : {}), + }; + } + + const density = + totalInstructions > 0 && rangeExtent > 0 + ? `${DENSITY_FORMAT.format(totalInstructions / rangeExtent)} instr / bundle` + : undefined; + + const dominantUnit = meanUtilization?.boundUnit + ? FUNCTIONAL_UNITS_BY_LANE.get(meanUtilization.boundUnit) + : mixResult.mix?.[0]?.unit; + const accentColor = dominantUnit?.color ?? DEFAULT_ACCENT_COLOR; + + return { + mode: 'range', + kind: 'range', + name: `Bundles ${bundles}`, + isCode: false, + ...(dominantUnit ? {unit: dominantUnit} : {}), + accentColor, + badgeLabel: 'Range Selection', + badgeIcon: 'select_all', + bundlesLabel: 'Bundles', + bundles, + length, + subtitleChips, + ...(mixResult.mix ? {instructionMix: tooltipOrMix(mixResult.mix)} : {}), + totalInstructions, + ...(meanUtilization ? {meanUtilization} : {}), + ...(utilizationBars ? {utilizationBars} : {}), + ...(density ? {density} : {}), + ...(topInstructions ? {topInstructions} : {}), + ...(selectedEvents ? {selectedEvents} : {}), + }; +} + +function tooltipOrMix( + mix: readonly InstructionMixItem[], +): readonly InstructionMixItem[] { + return mix; +} + +/** + * Bottom selection details panel for the Static Kernel Viewer. + * + * Displays a 3-column hardware schedule inspection dashboard when the user + * clicks an instruction, region, or counter point, or drags a selection + * rectangle across multiple bundles. + */ +@Component({ + standalone: true, + selector: 'kernel-selection-panel', + templateUrl: './kernel_selection_panel.ng.html', + styleUrls: ['./kernel_selection_panel.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [CommonModule, MatButtonModule, MatIconModule, MatTooltipModule], +}) +export class KernelSelectionPanel implements AfterViewInit, OnDestroy { + /** Emits true when a selection is active, false when closed or deselected. */ + @Output() readonly openChange = new EventEmitter(); + /** Emits an `eventIndex` to select and reveal on the timeline. */ + @Output() readonly selectEvent = new EventEmitter(); + /** Emits a search query to highlight matching events on the timeline. */ + @Output() readonly searchQuery = new EventEmitter(); + + content: KernelSelectionContent | undefined = undefined; + /** Optional unit label filter applied to the Top Instructions / Events table. */ + selectedUnitFilter: string | undefined = undefined; + /** Currently hovered functional unit in the Instruction Mix stacked bar or list. */ + hoveredMixUnit: string | undefined = undefined; + /** Tab mode for the right-hand table in range selections (`top` or `events`). */ + rangeTableTab: 'top' | 'events' = 'top'; + /** Stack of previously inspected selections for in-panel back navigation. */ + historyStack: KernelSelectionContent[] = []; + copied = false; + + private copiedTimer: ReturnType | undefined = undefined; + private readonly changeDetectorRef = inject(ChangeDetectorRef); + private readonly ngZone = inject(NgZone); + + private readonly eventSelectedListener = (event: Event) => { + const detail = event instanceof CustomEvent ? event.detail : undefined; + const nextContent = buildKernelSelectionFromEvent(detail); + this.applyContent(nextContent); + }; + + private readonly eventsSelectedListener = (event: Event) => { + const detail = event instanceof CustomEvent ? event.detail : undefined; + const nextContent = buildKernelSelectionFromRange(detail); + this.applyContent(nextContent); + }; + + ngAfterViewInit(): void { + this.ngZone.runOutsideAngular(() => { + window.addEventListener( + EVENT_SELECTED_EVENT_NAME, + this.eventSelectedListener, + ); + window.addEventListener( + EVENTS_SELECTED_EVENT_NAME, + this.eventsSelectedListener, + ); + }); + } + + ngOnDestroy(): void { + clearTimeout(this.copiedTimer); + window.removeEventListener( + EVENT_SELECTED_EVENT_NAME, + this.eventSelectedListener, + ); + window.removeEventListener( + EVENTS_SELECTED_EVENT_NAME, + this.eventsSelectedListener, + ); + } + + /** Programmatically sets or clears the active selection content. */ + setSelection(nextContent: KernelSelectionContent | undefined): void { + this.applyContent(nextContent); + } + + /** Closes the selection panel and notifies the parent container. */ + close(): void { + this.historyStack = []; + this.applyContent(undefined); + } + + /** Toggles filtering the range instruction table by functional unit. */ + toggleUnitFilter(unitLabel: string): void { + this.selectedUnitFilter = + this.selectedUnitFilter === unitLabel ? undefined : unitLabel; + this.changeDetectorRef.detectChanges(); + } + + clearUnitFilter(): void { + if (this.selectedUnitFilter !== undefined) { + this.selectedUnitFilter = undefined; + this.changeDetectorRef.detectChanges(); + } + } + + setHoveredMixUnit(unitLabel: string | undefined): void { + if (this.hoveredMixUnit !== unitLabel) { + this.hoveredMixUnit = unitLabel; + this.changeDetectorRef.detectChanges(); + } + } + + setRangeTableTab(tab: 'top' | 'events'): void { + this.rangeTableTab = tab; + this.changeDetectorRef.detectChanges(); + } + + onSelectEventIndex(eventIndex: number | undefined): void { + if (eventIndex !== undefined && eventIndex >= 0) { + if (this.content && this.content.eventIndex !== eventIndex) { + this.historyStack.push(this.content); + if (this.historyStack.length > 24) { + this.historyStack.shift(); + } + this.changeDetectorRef.markForCheck(); + } + this.selectEvent.emit(eventIndex); + } + } + + /** Navigates back to the previously inspected selection in the panel history. */ + goBack(): void { + const previous = this.historyStack.pop(); + if (!previous) { + return; + } + this.applyContent(previous); + if (previous.eventIndex !== undefined && previous.eventIndex >= 0) { + this.selectEvent.emit(previous.eventIndex); + } + } + + get canGoBack(): boolean { + return this.historyStack.length > 0; + } + + get previousSelectionLabel(): string { + const last = this.historyStack[this.historyStack.length - 1]; + return last ? `Back to ${last.name}` : 'No previous selection in history'; + } + + onSearchText(query: string | undefined): void { + if (query && query.trim() !== '') { + this.searchQuery.emit(query.trim()); + } + } + + get filteredTopInstructions(): readonly TopInstructionItem[] { + const items = this.content?.topInstructions ?? []; + if (!this.selectedUnitFilter) { + return items; + } + return items.filter((item) => item.trackName === this.selectedUnitFilter); + } + + get filteredSelectedEvents(): readonly SelectedRangeEventItem[] { + const items = this.content?.selectedEvents ?? []; + if (!this.selectedUnitFilter) { + return items; + } + return items.filter((item) => item.trackName === this.selectedUnitFilter); + } + + copySummary(): void { + if (!this.content || !navigator.clipboard) { + return; + } + const c = this.content; + const lines: string[] = []; + lines.push( + `${c.badgeLabel}: ${c.name} (${c.bundlesLabel} ${c.bundles}${c.hexBundle ? ' ' + c.hexBundle : ''})`, + ); + if (c.ordinalLabel) { + lines.push(`Ordinal: ${c.ordinalLabel}`); + } + if (c.regions && c.regions.length > 0) { + lines.push(`${c.regionRowLabel ?? 'Region'}: ${c.regions.join(' › ')}`); + } + if (c.output?.register) { + lines.push(`Produces: ${c.output.register}`); + } + if (c.operands && c.operands.length > 0) { + lines.push( + `Operands: ${c.operands.map((op) => `[${op.index}] ${op.text}`).join(', ')}`, + ); + } + if (c.consumers && c.consumers.length > 0) { + lines.push(`Consumers: ${c.consumers.join(', ')}`); + } + if (c.sourceInfo) { + lines.push(`Source: ${c.sourceInfo.raw}`); + } + if (c.density) { + lines.push(`Density: ${c.density}`); + } + navigator.clipboard.writeText(lines.join('\n')).then( + () => { + this.copied = true; + clearTimeout(this.copiedTimer); + this.copiedTimer = setTimeout(() => { + this.copied = false; + this.changeDetectorRef.markForCheck(); + }, 1500); + this.changeDetectorRef.markForCheck(); + }, + () => {}, + ); + } + + private applyContent(nextContent: KernelSelectionContent | undefined): void { + const wasOpen = this.content !== undefined; + const isOpen = nextContent !== undefined; + if (!isOpen) { + this.historyStack = []; + } + this.content = nextContent; + this.selectedUnitFilter = undefined; + this.hoveredMixUnit = undefined; + this.rangeTableTab = + nextContent?.topInstructions && nextContent.topInstructions.length > 0 + ? 'top' + : 'events'; + this.copied = false; + if (wasOpen !== isOpen) { + this.openChange.emit(isOpen); + } + this.changeDetectorRef.detectChanges(); + } +} diff --git a/frontend/app/components/static_kernel_viewer/static_kernel_viewer.ng.html b/frontend/app/components/static_kernel_viewer/static_kernel_viewer.ng.html index ab9fd4fc2..08985db7e 100644 --- a/frontend/app/components/static_kernel_viewer/static_kernel_viewer.ng.html +++ b/frontend/app/components/static_kernel_viewer/static_kernel_viewer.ng.html @@ -248,12 +248,19 @@ + diff --git a/frontend/app/components/static_kernel_viewer/static_kernel_viewer.ts b/frontend/app/components/static_kernel_viewer/static_kernel_viewer.ts index b67d9426e..e8ea2d916 100644 --- a/frontend/app/components/static_kernel_viewer/static_kernel_viewer.ts +++ b/frontend/app/components/static_kernel_viewer/static_kernel_viewer.ts @@ -13,6 +13,7 @@ import { import {ActivatedRoute, Params} from '@angular/router'; import {Store} from '@ngrx/store'; import {DEFAULT_HOST} from 'org_xprof/frontend/app/common/constants/constants'; +import {KernelSelectionPanel} from 'org_xprof/frontend/app/components/static_kernel_viewer/kernel_selection_panel'; import { LOADING_STATUS_UPDATE_EVENT_NAME, shutdownTraceViewerV2, @@ -628,8 +629,10 @@ export class StaticKernelViewer implements OnInit, AfterViewInit, OnDestroy { enableEventTooltip = false; traceViewerModule: TraceViewerV2Module | null = null; + isSelectionPanelOpen = false; @ViewChild('searchInput') searchInput?: ElementRef; + @ViewChild(KernelSelectionPanel) selectionPanel?: KernelSelectionPanel; private readonly collapsedModules = new Set(); private listKey = ''; @@ -1221,8 +1224,28 @@ export class StaticKernelViewer implements OnInit, AfterViewInit, OnDestroy { return this.tabs.find((tab) => tab.entry === entry); } + onSelectionPanelOpenChange(isOpen: boolean): void { + if (this.isSelectionPanelOpen === isOpen) { + return; + } + this.isSelectionPanelOpen = isOpen; + this.changeDetectorRef.markForCheck(); + this.notifyResize(); + } + + onSelectTimelineEvent(eventIndex: number): void { + this.traceViewerModule?.application + ?.instance?.() + ?.selectEvent?.(eventIndex); + } + + onSearchTimelineQuery(query: string): void { + this.traceViewerModule?.application?.instance?.()?.setSearchQuery?.(query); + } + /** Loads the trace of `entry` into the timeline. */ private showEntry(entry: KernelEntry): void { + this.selectionPanel?.close(); this.collapsedModules.delete(entry.module); this.updateUrlQueryParams(); const queryParamsMap = new Map([ @@ -1510,6 +1533,9 @@ export class StaticKernelViewer implements OnInit, AfterViewInit, OnDestroy { event.preventDefault(); this.focusSearch(); this.changeDetectorRef.markForCheck(); + } else if (event.key === 'Escape' && this.isSelectionPanelOpen) { + this.selectionPanel?.close(); + this.changeDetectorRef.markForCheck(); } } diff --git a/frontend/app/components/static_kernel_viewer/static_kernel_viewer_module.ts b/frontend/app/components/static_kernel_viewer/static_kernel_viewer_module.ts index d09555585..6762ab391 100644 --- a/frontend/app/components/static_kernel_viewer/static_kernel_viewer_module.ts +++ b/frontend/app/components/static_kernel_viewer/static_kernel_viewer_module.ts @@ -5,6 +5,7 @@ import {MatIconModule} from '@angular/material/icon'; import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; import {MatTooltipModule} from '@angular/material/tooltip'; import {KernelEventTooltip} from 'org_xprof/frontend/app/components/static_kernel_viewer/kernel_event_tooltip'; +import {KernelSelectionPanel} from 'org_xprof/frontend/app/components/static_kernel_viewer/kernel_selection_panel'; import {TraceViewerContainer} from 'org_xprof/frontend/app/components/trace_viewer_container/trace_viewer_container'; import {StaticKernelViewer} from './static_kernel_viewer'; @@ -13,6 +14,7 @@ import {StaticKernelViewer} from './static_kernel_viewer'; imports: [ CommonModule, KernelEventTooltip, + KernelSelectionPanel, MatButtonModule, MatIconModule, MatProgressSpinnerModule, diff --git a/frontend/app/components/trace_viewer_container/trace_viewer_container.ng.html b/frontend/app/components/trace_viewer_container/trace_viewer_container.ng.html index 433f9c512..ec899fed1 100644 --- a/frontend/app/components/trace_viewer_container/trace_viewer_container.ng.html +++ b/frontend/app/components/trace_viewer_container/trace_viewer_container.ng.html @@ -171,8 +171,11 @@ - -
+ +
+ +
+
diff --git a/frontend/app/components/trace_viewer_container/trace_viewer_container.scss b/frontend/app/components/trace_viewer_container/trace_viewer_container.scss index bd6e34fa8..29b7453ff 100644 --- a/frontend/app/components/trace_viewer_container/trace_viewer_container.scss +++ b/frontend/app/components/trace_viewer_container/trace_viewer_container.scss @@ -132,9 +132,7 @@ canvas.emscripten { --tt-good: #81c995; --tt-warn: #fdd663; --tt-bad: #f28b82; - --tt-shadow: - 0 16px 40px rgba(0, 0, 0, 0.45), - 0 4px 12px rgba(0, 0, 0, 0.3); + --tt-shadow: 0 16px 40px rgba(0, 0, 0, 0.45), 0 4px 12px rgba(0, 0, 0, 0.3); --tt-label-font: 'Google Sans Text', 'Google Sans', Roboto, sans-serif; --tt-code-font: 'Roboto Mono', monospace; @@ -774,8 +772,7 @@ canvas.emscripten { --tt-warn: #e37400; --tt-bad: #d93025; --tt-shadow: - 0 16px 36px rgba(60, 64, 67, 0.2), - 0 3px 8px rgba(60, 64, 67, 0.12); + 0 16px 36px rgba(60, 64, 67, 0.2), 0 3px 8px rgba(60, 64, 67, 0.12); .view-toggle .view-toggle-btn.active { box-shadow: 0 1px 2px rgba(60, 64, 67, 0.2); @@ -1157,6 +1154,22 @@ button.shortcuts-guide-btn { flex-grow: 1; } +.custom-drawer-container { + height: calc(100% - 16px); + margin-top: 8px; + margin-bottom: 8px; + box-sizing: border-box; + border-radius: 16px; + overflow: hidden; + display: flex; + flex-direction: column; + background-color: transparent; + + &[hidden] { + display: none; + } +} + .drawer { height: calc(100% - 16px); margin-top: 8px; diff --git a/frontend/app/components/trace_viewer_container/trace_viewer_container.ts b/frontend/app/components/trace_viewer_container/trace_viewer_container.ts index 0257b72dc..1d89eb6d3 100644 --- a/frontend/app/components/trace_viewer_container/trace_viewer_container.ts +++ b/frontend/app/components/trace_viewer_container/trace_viewer_container.ts @@ -585,6 +585,12 @@ export class TraceViewerContainer this.rawEventsDataSource.data = data ?? []; } @Input() tool?: string; + @Input() customDrawerOpen = false; + + get isDrawerVisible(): boolean { + return Boolean(this.selectedEvent) || this.customDrawerOpen; + } + rawEventsDataSource = new MatTableDataSource(); @Output() readonly drillDownEvent = new EventEmitter(); @@ -840,9 +846,11 @@ export class TraceViewerContainer ngOnChanges(changes: SimpleChanges) { if (changes['selectedEvent']) { - this.updateSplitSizes(); this.selectedEventJson = this.buildSelectedEventJson(); } + if (changes['selectedEvent'] || changes['customDrawerOpen']) { + this.updateSplitSizes(); + } } /** @@ -1093,12 +1101,12 @@ export class TraceViewerContainer this.drawerSizePercent = DEFAULT_DRAWER_SIZE_PERCENT; } - // If an event is selected, the timeline height is reduced to accommodate - // the detail view (drawer). Otherwise, the timeline takes the full height. - this.timelineHeightPercent = this.selectedEvent - ? 100 - this.drawerSizePercent - : 100; - this.detailHeightPercent = this.selectedEvent ? this.drawerSizePercent : 0; + // If an event is selected or a custom drawer is open, the timeline height + // is reduced to accommodate the detail view (drawer). Otherwise, the + // timeline takes the full height. + const isOpen = this.isDrawerVisible; + this.timelineHeightPercent = isOpen ? 100 - this.drawerSizePercent : 100; + this.detailHeightPercent = isOpen ? this.drawerSizePercent : 0; } /** @@ -1246,7 +1254,7 @@ export class TraceViewerContainer * `event.sizes` is `IOutputAreaSizes` from `angular-split`. */ onDragEnd({sizes}: {sizes: Array}): void { - if (this.selectedEvent && sizes.length > 1) { + if (this.isDrawerVisible && sizes.length > 1) { // This assumes the drawer is the second area (index 1). This is safe as // long as the template structure remains consistent (Canvas then Drawer). const size = sizes[1];