diff --git a/frontend/app/components/static_kernel_viewer/BUILD b/frontend/app/components/static_kernel_viewer/BUILD
index 5db7f35a2..804803814 100644
--- a/frontend/app/components/static_kernel_viewer/BUILD
+++ b/frontend/app/components/static_kernel_viewer/BUILD
@@ -17,6 +17,29 @@ sass_binary(
src = "kernel_selection_panel.scss",
)
+sass_binary(
+ name = "kernel_minimap_css",
+ src = "kernel_minimap.scss",
+)
+
+xprof_ng_module(
+ name = "kernel_minimap",
+ srcs = [
+ "kernel_minimap.ts",
+ ],
+ assets = [
+ ":kernel_minimap_css",
+ "kernel_minimap.ng.html",
+ ],
+ deps = [
+ "//:node_modules/@angular/common",
+ "//:node_modules/@angular/core",
+ "@org_xprof//frontend/app/common/angular:angular_material_button",
+ "@org_xprof//frontend/app/common/angular:angular_material_icon",
+ "@org_xprof//frontend/app/common/angular:angular_material_tooltip",
+ ],
+)
+
xprof_ng_module(
name = "static_kernel_viewer",
srcs = [
@@ -34,6 +57,7 @@ xprof_ng_module(
"static_kernel_viewer.ng.html",
],
deps = [
+ ":kernel_minimap",
"//:node_modules/@angular/common",
"//:node_modules/@angular/core",
"//:node_modules/@angular/router",
@@ -56,10 +80,12 @@ xprof_ng_module(
testonly = True,
srcs = [
"kernel_event_tooltip_test.ts",
+ "kernel_minimap_test.ts",
"kernel_selection_panel_test.ts",
"static_kernel_viewer_test.ts",
],
deps = [
+ ":kernel_minimap",
":static_kernel_viewer",
"//javascript/angular2/testing/catalyst/fake_async",
# ksf,
diff --git a/frontend/app/components/static_kernel_viewer/kernel_minimap.ng.html b/frontend/app/components/static_kernel_viewer/kernel_minimap.ng.html
new file mode 100644
index 000000000..583ddd1eb
--- /dev/null
+++ b/frontend/app/components/static_kernel_viewer/kernel_minimap.ng.html
@@ -0,0 +1,245 @@
+
+
+
+
+
+
+
+ {{ tick.label }}
+
+
bundles
+
+
+
+
+
+
+
+
0"
+ class="utilization-fill"
+ [style.height.%]="bin.density * 100"
+ >
+
+
+
+
0" class="minimap-pallas">
+
+
+
+
0" class="minimap-regions">
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ s.isTimeAxis ? h.bundleLabel : 'Bundle ' + h.bundleLabel }}
+
+
+ {{ u.label }}
+
+
+ +{{ su.label }}
+
+ Idle
+
+ {{ pp }}
+
+ {{ h.region }}
+
+
+
+
diff --git a/frontend/app/components/static_kernel_viewer/kernel_minimap.scss b/frontend/app/components/static_kernel_viewer/kernel_minimap.scss
new file mode 100644
index 000000000..4f6c711e4
--- /dev/null
+++ b/frontend/app/components/static_kernel_viewer/kernel_minimap.scss
@@ -0,0 +1,597 @@
+:host {
+ display: block;
+ flex-shrink: 0;
+ width: 100%;
+ box-sizing: border-box;
+ user-select: none;
+ font-family:
+ Roboto,
+ 'Google Sans Text',
+ 'Google Sans',
+ -apple-system,
+ BlinkMacSystemFont,
+ sans-serif;
+ color: var(--minimap-fg, var(--mat-sys-on-surface, #222));
+}
+
+.minimap-bar {
+ display: flex;
+ align-items: stretch;
+ width: 100%;
+ height: 68px;
+ background: var(--minimap-bg, var(--mat-sys-surface, #fff));
+ border-bottom: 1px solid
+ var(--minimap-ruler-line, var(--mat-sys-outline-variant, #ccc));
+ color: var(--minimap-fg, var(--mat-sys-on-surface, #222));
+ box-sizing: border-box;
+
+ &.has-both-scopes {
+ height: 82px;
+
+ .minimap-track {
+ min-height: 65px;
+ }
+
+ .minimap-pallas {
+ top: 8px;
+ height: 16px;
+ }
+
+ .minimap-regions {
+ top: 25px;
+ height: 15px;
+ }
+
+ .minimap-bins {
+ inset: 41px 0 1px;
+ }
+ }
+}
+
+.minimap-header {
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: stretch;
+ border-right: 1px solid #666;
+ background: var(--minimap-bg, var(--mat-sys-surface, #fff));
+ box-sizing: border-box;
+ overflow: hidden;
+}
+
+.header-top {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 6px;
+ height: 16px;
+ padding: 0 8px 0 10px;
+ background: var(--minimap-midtone, var(--mat-sys-surface-container, #ddd));
+ border-bottom: 1px solid
+ var(--minimap-ruler-line, var(--mat-sys-outline-variant, #ccc));
+ box-sizing: border-box;
+}
+
+.header-actions {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ flex-shrink: 0;
+}
+
+.fit-button {
+ display: inline-flex;
+ align-items: center;
+ gap: 2px;
+ height: 13px;
+ padding: 0 5px;
+ border-radius: 2px;
+ border: 1px solid
+ var(--minimap-ruler-line, var(--mat-sys-outline-variant, #ccc));
+ background: var(--minimap-bg, var(--mat-sys-surface, #fff));
+ color: var(--minimap-fg, var(--mat-sys-on-surface, #222));
+ font-family: inherit;
+ font-size: 9px;
+ font-weight: 600;
+ cursor: pointer;
+ transition:
+ background-color 120ms ease,
+ border-color 120ms ease;
+
+ mat-icon {
+ width: 10px;
+ height: 10px;
+ font-size: 10px;
+ line-height: 10px;
+ }
+
+ &:hover {
+ background: var(
+ --minimap-collapsed-header,
+ var(--mat-sys-surface-container-high, #ccc)
+ );
+ border-color: var(--minimap-subtitle, var(--mat-sys-outline, #555));
+ }
+}
+
+.header-range {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 7px 8px 0 10px;
+ font-size: 11px;
+ line-height: 1.2;
+ font-variant-numeric: tabular-nums;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ background: linear-gradient(
+ to bottom,
+ var(--minimap-expanded-header, #aaa) 0,
+ var(--minimap-expanded-header, #aaa) 7px,
+ transparent 7px
+ );
+}
+
+.header-chevron {
+ font-size: 9px;
+ color: var(--minimap-fg, var(--mat-sys-on-surface, #222));
+ margin-right: 2px;
+}
+
+.visible-bundles {
+ font-weight: 600;
+ color: var(--minimap-fg, var(--mat-sys-on-surface, #222));
+}
+
+.range-sep,
+.total-bundles,
+.range-unit {
+ color: var(--minimap-subtitle, var(--mat-sys-outline, #555));
+ font-size: 10.5px;
+}
+
+.minimap-viewport {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: stretch;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+.minimap-ruler {
+ position: relative;
+ width: 100%;
+ height: 16px;
+ flex-shrink: 0;
+ background-color: var(
+ --minimap-midtone,
+ var(--mat-sys-surface-container, #ddd)
+ );
+ background-image: repeating-linear-gradient(
+ to right,
+ var(--minimap-ruler-line, var(--mat-sys-outline-variant, #ccc)) 0,
+ var(--minimap-ruler-line, var(--mat-sys-outline-variant, #ccc)) 1px,
+ transparent 1px,
+ transparent 2%
+ );
+ background-size: 100% 3px;
+ background-position: left bottom;
+ background-repeat: no-repeat;
+ border-bottom: 1px solid
+ var(--minimap-ruler-line, var(--mat-sys-outline-variant, #ccc));
+ box-sizing: border-box;
+ pointer-events: none;
+ overflow: hidden;
+}
+
+.ruler-tick {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ display: flex;
+ align-items: center;
+
+ &.right-edge {
+ transform: translateX(-100%);
+ flex-direction: row-reverse;
+
+ .tick-label {
+ padding-left: 0;
+ padding-right: 4px;
+ }
+ }
+}
+
+.tick-mark {
+ width: 1px;
+ height: 100%;
+ background: var(--minimap-ruler-line, var(--mat-sys-outline-variant, #ccc));
+ flex-shrink: 0;
+}
+
+.tick-label {
+ padding-left: 4px;
+ font-size: 9.5px;
+ line-height: 1;
+ color: var(--minimap-ruler-text, var(--mat-sys-outline, #222));
+ font-variant-numeric: tabular-nums;
+ white-space: nowrap;
+}
+
+.ruler-unit-label {
+ position: absolute;
+ right: 4px;
+ top: 3px;
+ font-size: 9.5px;
+ line-height: 1;
+ color: var(--minimap-ruler-text, var(--mat-sys-outline, #222));
+}
+
+.minimap-track {
+ position: relative;
+ flex: 1;
+ width: 100%;
+ min-height: 51px;
+ background-color: var(--minimap-bg, var(--mat-sys-surface, #fff));
+ box-sizing: border-box;
+ cursor: crosshair;
+
+ &.dragging-pan {
+ cursor: grabbing;
+ }
+
+ &.dragging-resize {
+ cursor: ew-resize;
+ }
+
+ &.dragging-scrub {
+ cursor: col-resize;
+ }
+}
+
+.minimap-grid {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ z-index: 0;
+}
+
+.minimap-grid-line {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 1px;
+ background: var(--minimap-midtone, var(--mat-sys-surface-container, #ddd));
+}
+
+.minimap-utilization-strip {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 7px;
+ display: flex;
+ align-items: stretch;
+ background: var(--minimap-expanded-header, #aaa);
+ border-bottom: 1px solid var(--minimap-bg, var(--mat-sys-surface, #fff));
+ box-sizing: border-box;
+ pointer-events: none;
+ z-index: 1;
+}
+
+.utilization-col {
+ flex: 1 1 0;
+ min-width: 0;
+ display: flex;
+ align-items: flex-end;
+}
+
+.utilization-fill {
+ width: 100%;
+ background: var(--minimap-flame-header, #ffa1a1);
+}
+
+.minimap-pallas,
+.minimap-regions {
+ position: absolute;
+ top: 8px;
+ left: 0;
+ right: 0;
+ height: 20px;
+ border-bottom: 1px solid
+ var(--minimap-midtone, var(--mat-sys-surface-container, #ddd));
+ box-sizing: border-box;
+ pointer-events: none;
+ z-index: 2;
+ overflow: hidden;
+}
+
+.region-marker {
+ --region-color: #96c1ff;
+ --region-text-color: var(--minimap-on-surface, #222);
+
+ position: absolute;
+ padding: 0 1px 1px 0;
+ border: none;
+ border-radius: 0;
+ background: var(--region-color);
+ background-clip: content-box;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ overflow: hidden;
+ pointer-events: auto;
+ cursor: pointer;
+
+ .region-label {
+ padding: 0 3px;
+ font-size: 8px;
+ line-height: 1;
+ font-weight: 500;
+ color: var(--region-text-color, var(--minimap-on-surface, #222));
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: clip;
+ }
+
+ &:hover {
+ outline: 1px solid #00f;
+ outline-offset: -1px;
+ z-index: 3;
+ }
+}
+
+.minimap-bins {
+ position: absolute;
+ inset: 29px 0 1px;
+ display: flex;
+ align-items: stretch;
+ overflow: hidden;
+ pointer-events: none;
+ z-index: 1;
+}
+
+.minimap-bin {
+ flex: 1 1 0;
+ min-width: 0;
+ position: relative;
+
+ &.active::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: var(--bin-top, 0%);
+ height: var(--bin-height, 100%);
+ border-bottom: 1px solid var(--minimap-bg, var(--mat-sys-surface, #fff));
+ box-sizing: border-box;
+ background: var(--bin-color);
+ opacity: var(--bin-opacity, 0.96);
+ }
+
+ &.active.gap-right::before {
+ right: 1px;
+ }
+
+ &.has-secondary::after {
+ content: '';
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: var(--bin-secondary-top, 50%);
+ height: var(--bin-secondary-height, 20%);
+ border-bottom: 1px solid var(--minimap-bg, var(--mat-sys-surface, #fff));
+ box-sizing: border-box;
+ background: var(--bin-secondary-color);
+ opacity: 0.94;
+ }
+
+ &.has-secondary.secondary-gap-right::after {
+ right: 1px;
+ }
+}
+
+.minimap-curtain {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ background: color-mix(
+ in srgb,
+ var(--minimap-bg, var(--mat-sys-surface, #fff)) 32%,
+ transparent
+ );
+ pointer-events: none;
+ z-index: 3;
+
+ &.left-curtain {
+ left: 0;
+ }
+}
+
+.minimap-lens {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ min-width: 8px;
+ border: 1.5px solid var(--mat-sys-primary, #3782fa);
+ border-radius: 0;
+ background: color-mix(
+ in srgb,
+ var(--mat-sys-primary, #3782fa) 10%,
+ transparent
+ );
+ box-sizing: border-box;
+ cursor: grab;
+ z-index: 4;
+ transition:
+ box-shadow 120ms ease,
+ background-color 120ms ease;
+
+ &:hover {
+ background: color-mix(
+ in srgb,
+ var(--mat-sys-primary, #3782fa) 15%,
+ transparent
+ );
+ }
+
+ &.dragging {
+ cursor: grabbing;
+ background: color-mix(
+ in srgb,
+ var(--mat-sys-primary, #3782fa) 18%,
+ transparent
+ );
+ }
+}
+
+.lens-handle {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: ew-resize;
+ z-index: 5;
+
+ &.left-handle {
+ left: -4px;
+ }
+
+ &.right-handle {
+ right: -4px;
+ }
+
+ .handle-grip {
+ width: 3px;
+ height: 14px;
+ border-radius: 1px;
+ background: var(--mat-sys-primary, #3782fa);
+ box-shadow: 0 0 0 1px var(--minimap-bg, var(--mat-sys-surface, #fff));
+ transition: transform 120ms ease;
+ }
+
+ &:hover .handle-grip {
+ transform: scaleY(1.2);
+ }
+}
+
+.minimap-hover-playhead {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ width: 1px;
+ transform: translateX(-50%);
+ background: var(--minimap-fg, var(--mat-sys-on-surface, #222));
+ opacity: 0.65;
+ pointer-events: none;
+ z-index: 5;
+}
+
+.minimap-hover-pill {
+ position: absolute;
+ top: calc(100% + 4px);
+ transform: translateX(-50%);
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ height: 22px;
+ padding: 0 8px;
+ border-radius: 4px;
+ background: var(--minimap-bg, var(--mat-sys-surface-container-lowest, #fff));
+ border: 1px solid
+ var(--minimap-ruler-line, var(--mat-sys-outline-variant, #ccc));
+ box-shadow: 0 2px 6px
+ color-mix(in srgb, var(--mat-sys-shadow, #000) 16%, transparent);
+ font-size: 10.5px;
+ line-height: 1;
+ white-space: nowrap;
+ pointer-events: none;
+ z-index: 10;
+
+ &.align-left {
+ transform: translateX(0);
+ }
+
+ &.align-right {
+ transform: translateX(-100%);
+ }
+}
+
+.hover-bundle {
+ font-family: 'Roboto Mono', 'Google Sans Mono', monospace;
+ font-weight: 600;
+ color: var(--minimap-fg, var(--mat-sys-on-surface, #222));
+ font-variant-numeric: tabular-nums;
+}
+
+.hover-unit-chip {
+ --unit-color: #96c1ff;
+
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ height: 15px;
+ padding: 0 5px;
+ border-radius: 2px;
+ background: var(--unit-color);
+ color: var(--minimap-on-surface, #222);
+ font-size: 9.5px;
+ font-weight: 700;
+
+ .unit-dot {
+ width: 5px;
+ height: 5px;
+ border-radius: 1px;
+ background: var(--minimap-on-surface, #222);
+ opacity: 0.55;
+ }
+
+ &.secondary {
+ font-weight: 600;
+ opacity: 0.92;
+ }
+}
+
+.hover-idle {
+ font-size: 10px;
+ color: var(--minimap-subtitle, var(--mat-sys-on-surface-variant, #555));
+ font-style: italic;
+}
+
+.hover-pallas-chip {
+ --pallas-color: #b1d284;
+
+ display: inline-flex;
+ align-items: center;
+ height: 15px;
+ padding: 0 5px;
+ border-radius: 2px;
+ background: var(--pallas-color);
+ color: var(--minimap-on-surface, #222);
+ font-family: 'Roboto Mono', 'Google Sans Mono', monospace;
+ font-size: 9.5px;
+ font-weight: 600;
+ max-width: 140px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.hover-region {
+ font-family: 'Roboto Mono', 'Google Sans Mono', monospace;
+ font-size: 10px;
+ color: var(--minimap-subtitle, var(--mat-sys-on-surface-variant, #555));
+ max-width: 160px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ border-left: 1px solid
+ var(--minimap-ruler-line, var(--mat-sys-outline-variant, #ccc));
+ padding-left: 6px;
+}
diff --git a/frontend/app/components/static_kernel_viewer/kernel_minimap.ts b/frontend/app/components/static_kernel_viewer/kernel_minimap.ts
new file mode 100644
index 000000000..a5ab2b779
--- /dev/null
+++ b/frontend/app/components/static_kernel_viewer/kernel_minimap.ts
@@ -0,0 +1,1469 @@
+import {CommonModule} from '@angular/common';
+import {
+ AfterViewInit,
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ ElementRef,
+ EventEmitter,
+ inject,
+ NgZone,
+ OnDestroy,
+ Output,
+ ViewChild,
+} from '@angular/core';
+import {MatButtonModule} from '@angular/material/button';
+import {MatIconModule} from '@angular/material/icon';
+import {MatTooltipModule} from '@angular/material/tooltip';
+
+/** A functional unit of a TPU core, which has its own lane in the timeline. */
+export interface FunctionalUnit {
+ readonly label: string;
+ readonly description: string;
+ readonly color: string;
+}
+
+const DEFAULT_ACCENT_COLOR = '#96c1ff';
+
+/**
+ * Trace Viewer V2 default canvas event palette (`kCatapultPalette` in
+ * `trace_viewer_v2/color/palettes.h`, used as `kDefaultPalette` by the canvas).
+ */
+export const TRACE_VIEWER_PALETTE: readonly string[] = [
+ '#ffa1a1',
+ '#96c1ff',
+ '#b1d284',
+ '#ff80ff',
+ '#80ddcc',
+ '#e4b886',
+ '#cc9eff',
+ '#98dc95',
+ '#ff92c1',
+ '#85d1ff',
+ '#c3ca80',
+ '#ff82ff',
+ '#86dfb3',
+ '#f9ab94',
+ '#a7b5ff',
+ '#a7d789',
+ '#ff85ec',
+ '#80dade',
+ '#d7c081',
+ '#e691ff',
+ '#91de9f',
+ '#ff9bac',
+ '#8ec8ff',
+];
+
+/** Active canvas theme colors synchronized from Trace Viewer V2 `ColorPalette`. */
+export interface MinimapTheme {
+ readonly paletteName: string;
+ readonly background: string;
+ readonly foreground: string;
+ readonly midtone: string;
+ readonly flameHeader: string;
+ readonly collapsedHeader: string;
+ readonly expandedHeader: string;
+ readonly subtitle: string;
+ readonly rulerText: string;
+ readonly rulerLine: string;
+ readonly selection: string;
+ readonly onSurface: string;
+ readonly inverseOnSurface: string;
+ readonly traceColors: readonly string[];
+}
+
+/** Default Catapult canvas theme matching `application.cc` (`kDefaultPalette`). */
+export const DEFAULT_MINIMAP_THEME: MinimapTheme = {
+ paletteName: 'Catapult',
+ background: '#ffffff',
+ foreground: '#222222',
+ midtone: '#dddddd',
+ flameHeader: '#ffa1a1',
+ collapsedHeader: '#cccccc',
+ expandedHeader: '#aaaaaa',
+ subtitle: '#555555',
+ rulerText: '#222222',
+ rulerLine: '#cccccc',
+ selection: '#dddddd',
+ onSurface: '#222222',
+ inverseOnSurface: '#ffffff',
+ traceColors: TRACE_VIEWER_PALETTE,
+};
+
+const FUNCTIONAL_UNITS: readonly FunctionalUnit[] = [
+ {label: 'MXU', description: 'Matrix multiply unit', color: '#cc9eff'},
+ {
+ label: 'XLU',
+ description: 'Cross-lane unit — transpose / permute / reduce',
+ color: '#e4b886',
+ },
+ {
+ label: 'SALU',
+ description: 'Scalar ALU — control flow, addressing, DMA issue',
+ color: '#b1d284',
+ },
+ {label: 'VPU', description: 'Vector ALU', color: '#96c1ff'},
+ {
+ label: 'EUP',
+ description: 'Extended unary pipeline — exp, rcp, tanh',
+ color: '#80ddcc',
+ },
+ {label: 'VLD', description: 'Vector load — VMEM → vregs', color: '#85d1ff'},
+ {label: 'VST', description: 'Vector store — vregs → VMEM', color: '#ff92c1'},
+ {label: 'DMA', description: 'DMA engine — HBM ↔ VMEM', color: '#f9ab94'},
+ {
+ label: 'Other',
+ description: 'No dedicated functional unit',
+ color: '#d7c081',
+ },
+];
+
+const FUNCTIONAL_UNITS_BY_LANE: ReadonlyMap = new Map(
+ FUNCTIONAL_UNITS.map((unit) => [unit.label, unit] as const),
+);
+
+const FUNCTIONAL_UNIT_INDEX_BY_LANE: ReadonlyMap = new Map(
+ FUNCTIONAL_UNITS.map((unit, idx) => [unit.label, idx] as const),
+);
+
+const BUNDLE_FORMAT = new Intl.NumberFormat(undefined, {
+ maximumFractionDigits: 0,
+});
+
+/**
+ * Custom event name dispatched from Trace Viewer V2 WASM when the schedule
+ * minimap summary or visible bundle range changes.
+ */
+export const MINIMAP_UPDATED_EVENT_NAME = 'minimap_updated';
+
+/** Minimum visible span in bundles when zooming or resizing the lens. */
+const MIN_VISIBLE_BUNDLES = 2;
+
+/** Minimum visible span in microseconds in time-axis mode (1 ns). */
+const MIN_VISIBLE_TIME_US = 1e-3;
+
+/** Minimum percentage width of the rendered viewport lens so it stays grabbable. */
+const MIN_LENS_WIDTH_PERCENT = 1.2;
+
+/** Pixel movement threshold to distinguish a click-to-center from a brush-zoom drag. */
+const BRUSH_DRAG_THRESHOLD_PX = 5;
+
+/** Default track label column width in pixels (`kDefaultLabelWidth` in C++). */
+const DEFAULT_LABEL_WIDTH_PX = 250;
+
+/** Formats a duration or relative timestamp in microseconds with optional compact units. */
+export function formatMinimapTimeUs(us: number, includeUnit = true): string {
+ if (!Number.isFinite(us) || Math.abs(us) < 1e-9) {
+ return '0';
+ }
+ const abs = Math.abs(us);
+ if (abs >= 1e6) {
+ const num = (us / 1e6).toFixed(2).replace(/\.?0+$/, '');
+ return includeUnit ? `${num} s` : num;
+ }
+ if (abs >= 1e3) {
+ const num = (us / 1e3).toFixed(2).replace(/\.?0+$/, '');
+ return includeUnit ? `${num} ms` : num;
+ }
+ if (abs >= 1) {
+ const num = us.toFixed(2).replace(/\.?0+$/, '');
+ return includeUnit ? `${num} us` : num;
+ }
+ if (abs >= 1e-3) {
+ const num = (us * 1e3).toFixed(1).replace(/\.0$/, '');
+ return includeUnit ? `${num} ns` : num;
+ }
+ const num = `${Math.round(us * 1e6)}`;
+ return includeUnit ? `${num} ps` : num;
+}
+
+/** A single bucket in the static schedule minimap ribbon. */
+export interface MinimapBin {
+ readonly index: number;
+ readonly unit?: FunctionalUnit;
+ readonly secondaryUnit?: FunctionalUnit;
+ readonly color: string;
+ readonly secondaryColor?: string;
+ readonly opacity: number;
+ readonly density: number;
+ readonly region: string;
+ readonly pallasPrimitive: string;
+ readonly namedScope: string;
+ readonly active: boolean;
+ readonly topPercent: number;
+ readonly heightPercent: number;
+ readonly secondaryTopPercent?: number;
+ readonly secondaryHeightPercent?: number;
+ readonly gapRight: boolean;
+ readonly secondaryGapRight: boolean;
+}
+
+/** A major region or Pallas primitive flame bar along the schedule minimap. */
+export interface MinimapRegionMarker {
+ readonly eventIndex: number;
+ readonly name: string;
+ readonly startUs: number;
+ readonly durationUs: number;
+ readonly depth: number;
+ readonly row: number;
+ readonly topPercent: number;
+ readonly heightPercent: number;
+ readonly leftPercent: number;
+ readonly widthPercent: number;
+ readonly color: string;
+ readonly textColor: string;
+ readonly bundlesLabel: string;
+ readonly source?: string;
+}
+
+/** A tick mark on the full-schedule bundle ruler above the minimap tracks. */
+export interface MinimapRulerTick {
+ readonly bundle: number;
+ readonly label: string;
+ readonly leftPercent: number;
+ readonly isRightEdge?: boolean;
+}
+
+/** Floating hover pill state when inspecting the minimap ribbon. */
+export interface MinimapHoverInfo {
+ readonly leftPercent: number;
+ readonly align: 'left' | 'center' | 'right';
+ readonly bundleLabel: string;
+ readonly unit?: FunctionalUnit;
+ readonly secondaryUnit?: FunctionalUnit;
+ readonly pallasPrimitive?: string;
+ readonly pallasColor?: string;
+ readonly region?: string;
+}
+
+/** View model for the Static Kernel Viewer and Trace Viewer schedule minimap. */
+export interface KernelMinimapState {
+ readonly dataStartUs: number;
+ readonly dataEndUs: number;
+ readonly visibleStartUs: number;
+ readonly visibleEndUs: number;
+ readonly labelWidthPx: number;
+ readonly isTimeAxis: boolean;
+ readonly visibleRangeLabel: string;
+ readonly totalBundlesLabel: string;
+ readonly zoomLabel: string;
+ readonly isZoomed: boolean;
+ readonly lensLeftPercent: number;
+ readonly lensWidthPercent: number;
+ readonly rightCurtainLeftPercent: number;
+ readonly rightCurtainWidthPercent: number;
+ readonly theme: MinimapTheme;
+ readonly bins: readonly MinimapBin[];
+ readonly regions: readonly MinimapRegionMarker[];
+ readonly pallasPrimitives: readonly MinimapRegionMarker[];
+ readonly hasPallasPrimitives: boolean;
+ readonly hasBothScopeRows: boolean;
+ readonly ticks: readonly MinimapRulerTick[];
+ readonly showStandaloneUnitLabel: boolean;
+}
+
+type DragMode = 'pan' | 'resize-left' | 'resize-right' | 'scrub';
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
+}
+
+function clamp(value: number, min: number, max: number): number {
+ return Math.min(Math.max(value, min), max);
+}
+
+/** Computes clean 1-2-5 bundle or time tick marks across `[startUs, endUs]`. */
+export function buildMinimapRulerTicks(
+ startUs: number,
+ endUs: number,
+ isTimeAxis = false,
+): MinimapRulerTick[] {
+ const duration = endUs - startUs;
+ if (!(duration > 0)) {
+ return [];
+ }
+ const targetTickCount = 5;
+ const rawStep = duration / targetTickCount;
+ const minRawStep = isTimeAxis ? 1e-6 : 1;
+ const magnitude = Math.pow(
+ 10,
+ Math.floor(Math.log10(Math.max(minRawStep, rawStep))),
+ );
+ const residual = rawStep / magnitude;
+ let step = magnitude;
+ if (residual > 5) {
+ step = 10 * magnitude;
+ } else if (residual > 2.5) {
+ step = 5 * magnitude;
+ } else if (residual > 1.5) {
+ step = 2 * magnitude;
+ }
+ if (!isTimeAxis) {
+ step = Math.max(1, Math.round(step));
+ }
+
+ if (isTimeAxis) {
+ const ticks: MinimapRulerTick[] = [];
+ for (let offset = step; offset <= duration; offset += step) {
+ const leftPercent = (offset / duration) * 100;
+ if (leftPercent < 6 || leftPercent > 99) {
+ continue;
+ }
+ const isRightEdge = leftPercent > 86;
+ ticks.push({
+ bundle: startUs + offset,
+ label: formatMinimapTimeUs(offset),
+ leftPercent,
+ isRightEdge,
+ });
+ }
+ return ticks;
+ }
+
+ const firstTick = Math.ceil(startUs / step) * step;
+ const ticks: MinimapRulerTick[] = [];
+ for (let bundle = firstTick; bundle <= endUs; bundle += step) {
+ const leftPercent = ((bundle - startUs) / duration) * 100;
+ if (leftPercent < 6 || leftPercent > 99) {
+ continue;
+ }
+ const isRightEdge = leftPercent > 86;
+ const formatted = BUNDLE_FORMAT.format(Math.round(bundle));
+ ticks.push({
+ bundle,
+ label: isRightEdge ? `${formatted} bundles` : formatted,
+ leftPercent,
+ isRightEdge,
+ });
+ }
+ return ticks;
+}
+
+function readThemeString(
+ raw: Record,
+ key: string,
+ fallback: string,
+): string {
+ const val = raw[key];
+ return typeof val === 'string' && val.trim().length > 0
+ ? val.trim()
+ : fallback;
+}
+
+function linearSrgbChannel(value255: number): number {
+ const c = value255 / 255;
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
+}
+
+function hexRelativeLuminance(hex: string): number {
+ const cleaned = hex.replace(/^#/, '');
+ if (cleaned.length !== 6) {
+ return 0.5;
+ }
+ const r = Number(`0x${cleaned.slice(0, 2)}`);
+ const g = Number(`0x${cleaned.slice(2, 4)}`);
+ const b = Number(`0x${cleaned.slice(4, 6)}`);
+ if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) {
+ return 0.5;
+ }
+ return (
+ 0.2126 * linearSrgbChannel(r) +
+ 0.7152 * linearSrgbChannel(g) +
+ 0.0722 * linearSrgbChannel(b)
+ );
+}
+
+function getTextColorForContrast(
+ bgHex: string,
+ onSurfaceHex: string,
+ inverseOnSurfaceHex: string,
+): string {
+ const bgLum = hexRelativeLuminance(bgHex);
+ const onSurfaceLum = hexRelativeLuminance(onSurfaceHex);
+ const contrastOnSurface =
+ (Math.max(bgLum, onSurfaceLum) + 0.05) /
+ (Math.min(bgLum, onSurfaceLum) + 0.05);
+ return contrastOnSurface >= 4.5 ? onSurfaceHex : inverseOnSurfaceHex;
+}
+
+/** Parses optional `theme` object from `minimap_updated` event detail. */
+export function parseMinimapTheme(rawTheme: unknown): MinimapTheme {
+ if (!isRecord(rawTheme)) {
+ return DEFAULT_MINIMAP_THEME;
+ }
+ const rawTraceColors = rawTheme['traceColors'];
+ const traceColors = Array.isArray(rawTraceColors)
+ ? rawTraceColors.filter(
+ (c): c is string => typeof c === 'string' && c.trim().length > 0,
+ )
+ : [];
+ return {
+ paletteName: readThemeString(
+ rawTheme,
+ 'paletteName',
+ DEFAULT_MINIMAP_THEME.paletteName,
+ ),
+ background: readThemeString(
+ rawTheme,
+ 'background',
+ DEFAULT_MINIMAP_THEME.background,
+ ),
+ foreground: readThemeString(
+ rawTheme,
+ 'foreground',
+ DEFAULT_MINIMAP_THEME.foreground,
+ ),
+ midtone: readThemeString(
+ rawTheme,
+ 'midtone',
+ DEFAULT_MINIMAP_THEME.midtone,
+ ),
+ flameHeader: readThemeString(
+ rawTheme,
+ 'flameHeader',
+ DEFAULT_MINIMAP_THEME.flameHeader,
+ ),
+ collapsedHeader: readThemeString(
+ rawTheme,
+ 'collapsedHeader',
+ DEFAULT_MINIMAP_THEME.collapsedHeader,
+ ),
+ expandedHeader: readThemeString(
+ rawTheme,
+ 'expandedHeader',
+ DEFAULT_MINIMAP_THEME.expandedHeader,
+ ),
+ subtitle: readThemeString(
+ rawTheme,
+ 'subtitle',
+ DEFAULT_MINIMAP_THEME.subtitle,
+ ),
+ rulerText: readThemeString(
+ rawTheme,
+ 'rulerText',
+ DEFAULT_MINIMAP_THEME.rulerText,
+ ),
+ rulerLine: readThemeString(
+ rawTheme,
+ 'rulerLine',
+ DEFAULT_MINIMAP_THEME.rulerLine,
+ ),
+ selection: readThemeString(
+ rawTheme,
+ 'selection',
+ DEFAULT_MINIMAP_THEME.selection,
+ ),
+ onSurface: readThemeString(
+ rawTheme,
+ 'onSurface',
+ DEFAULT_MINIMAP_THEME.onSurface,
+ ),
+ inverseOnSurface: readThemeString(
+ rawTheme,
+ 'inverseOnSurface',
+ DEFAULT_MINIMAP_THEME.inverseOnSurface,
+ ),
+ traceColors:
+ traceColors.length > 0 ? traceColors : DEFAULT_MINIMAP_THEME.traceColors,
+ };
+}
+
+function resolveFunctionalUnit(
+ unitKey: string,
+ explicitColor: string | undefined,
+ theme: MinimapTheme,
+ fallbackIndex = 0,
+): FunctionalUnit | undefined {
+ if (!unitKey) {
+ return undefined;
+ }
+ const baseUnit = FUNCTIONAL_UNITS_BY_LANE.get(unitKey);
+ if (!baseUnit) {
+ const palette =
+ theme.traceColors.length > 0 ? theme.traceColors : TRACE_VIEWER_PALETTE;
+ return {
+ label: unitKey,
+ description: unitKey,
+ color:
+ explicitColor ??
+ palette[fallbackIndex % palette.length] ??
+ DEFAULT_ACCENT_COLOR,
+ };
+ }
+ if (explicitColor) {
+ return {...baseUnit, color: explicitColor};
+ }
+ if (theme.paletteName !== 'Catapult' && theme.traceColors.length > 0) {
+ const unitIdx = FUNCTIONAL_UNIT_INDEX_BY_LANE.get(unitKey) ?? 0;
+ return {
+ ...baseUnit,
+ color: theme.traceColors[unitIdx % theme.traceColors.length],
+ };
+ }
+ return baseUnit;
+}
+
+interface ParsedBinRecord {
+ readonly unit?: FunctionalUnit;
+ readonly secondaryUnit?: FunctionalUnit;
+ readonly density: number;
+ readonly region: string;
+ readonly pallasPrimitive: string;
+ readonly namedScope: string;
+ readonly active: boolean;
+}
+
+const FUNCTIONAL_UNIT_ROW_INDEX: Readonly> = {
+ 'MXU': 0,
+ 'XLU': 0,
+ 'VPU': 1,
+ 'SALU': 1,
+ 'EUP': 1,
+ 'VLD': 2,
+ 'VST': 2,
+ 'DMA': 3,
+ 'Other': 3,
+};
+
+const FUNCTIONAL_UNIT_LANE_COUNT = 4;
+
+/** Parses raw `bins` array from `minimap_updated` event detail. */
+function parseMinimapBins(
+ rawBins: unknown,
+ theme: MinimapTheme = DEFAULT_MINIMAP_THEME,
+): MinimapBin[] {
+ if (!Array.isArray(rawBins)) {
+ return [];
+ }
+ const customUnitOrder = new Map();
+ const getCustomUnitIndex = (key: string): number => {
+ if (!key) return 0;
+ const existing = customUnitOrder.get(key);
+ if (existing !== undefined) return existing;
+ const nextIdx = customUnitOrder.size;
+ customUnitOrder.set(key, nextIdx);
+ return nextIdx;
+ };
+ const getRowForUnit = (label: string): number => {
+ const fixedRow = FUNCTIONAL_UNIT_ROW_INDEX[label];
+ if (fixedRow !== undefined) {
+ return fixedRow;
+ }
+ return getCustomUnitIndex(label) % FUNCTIONAL_UNIT_LANE_COUNT;
+ };
+
+ const parsed: ParsedBinRecord[] = [];
+ for (let i = 0; i < rawBins.length; i++) {
+ const item = rawBins[i];
+ if (!isRecord(item)) {
+ continue;
+ }
+ const unitKey = typeof item['unit'] === 'string' ? item['unit'] : '';
+ const secondaryKey =
+ typeof item['secondaryUnit'] === 'string' ? item['secondaryUnit'] : '';
+ const unitColor =
+ typeof item['unitColor'] === 'string' && item['unitColor']
+ ? item['unitColor']
+ : undefined;
+ const secondaryUnitColor =
+ typeof item['secondaryUnitColor'] === 'string' &&
+ item['secondaryUnitColor']
+ ? item['secondaryUnitColor']
+ : undefined;
+ const density =
+ typeof item['density'] === 'number' && Number.isFinite(item['density'])
+ ? clamp(item['density'], 0, 1)
+ : 0;
+ const region = typeof item['region'] === 'string' ? item['region'] : '';
+ const pallasPrimitive =
+ typeof item['pallasPrimitive'] === 'string'
+ ? item['pallasPrimitive']
+ : '';
+ const namedScope =
+ typeof item['namedScope'] === 'string' ? item['namedScope'] : '';
+ const unit = resolveFunctionalUnit(
+ unitKey,
+ unitColor,
+ theme,
+ getCustomUnitIndex(unitKey),
+ );
+ const secondaryUnit = resolveFunctionalUnit(
+ secondaryKey,
+ secondaryUnitColor,
+ theme,
+ getCustomUnitIndex(secondaryKey),
+ );
+ const active = Boolean(unitKey) || density > 0;
+ parsed.push({
+ unit,
+ secondaryUnit,
+ density,
+ region,
+ pallasPrimitive,
+ namedScope,
+ active,
+ });
+ }
+
+ const laneHeightPercent = 100 / FUNCTIONAL_UNIT_LANE_COUNT;
+ const bins: MinimapBin[] = [];
+ for (let i = 0; i < parsed.length; i++) {
+ const current = parsed[i];
+ const next = i + 1 < parsed.length ? parsed[i + 1] : undefined;
+ const {
+ unit,
+ secondaryUnit,
+ density,
+ region,
+ pallasPrimitive,
+ namedScope,
+ active,
+ } = current;
+ const color =
+ unit?.color ??
+ (active ? (theme.traceColors[0] ?? DEFAULT_ACCENT_COLOR) : 'transparent');
+ const opacity = active ? Number((0.9 + 0.1 * density).toFixed(2)) : 0;
+ const primaryRow = unit ? getRowForUnit(unit.label) : 0;
+ const topPercent = primaryRow * laneHeightPercent;
+ const heightPercent = active ? laneHeightPercent : 0;
+
+ let secondaryTopPercent: number | undefined;
+ let secondaryHeightPercent: number | undefined;
+ if (secondaryUnit) {
+ let secondaryRow = getRowForUnit(secondaryUnit.label);
+ if (secondaryRow === primaryRow) {
+ secondaryRow = (primaryRow + 1) % FUNCTIONAL_UNIT_LANE_COUNT;
+ }
+ secondaryTopPercent = secondaryRow * laneHeightPercent;
+ secondaryHeightPercent = laneHeightPercent;
+ }
+
+ const nextHasPrimary =
+ Boolean(unit) &&
+ Boolean(next) &&
+ next?.region === region &&
+ next?.unit?.label === unit?.label;
+ const nextHasSecondary =
+ Boolean(secondaryUnit) &&
+ Boolean(next) &&
+ next?.region === region &&
+ next?.secondaryUnit?.label === secondaryUnit?.label;
+
+ bins.push({
+ index: i,
+ unit,
+ secondaryUnit,
+ color,
+ secondaryColor: secondaryUnit?.color,
+ opacity,
+ density,
+ region,
+ pallasPrimitive,
+ namedScope,
+ active,
+ topPercent,
+ heightPercent,
+ secondaryTopPercent,
+ secondaryHeightPercent,
+ gapRight: !nextHasPrimary,
+ secondaryGapRight: !nextHasSecondary,
+ });
+ }
+ return bins;
+}
+
+/** Parses raw `regions` or `pallasPrimitives` array from `minimap_updated` event detail. */
+function parseMinimapRegions(
+ rawRegions: unknown,
+ dataStartUs: number,
+ dataEndUs: number,
+ theme: MinimapTheme = DEFAULT_MINIMAP_THEME,
+ isTimeAxis = false,
+): MinimapRegionMarker[] {
+ const duration = dataEndUs - dataStartUs;
+ if (!Array.isArray(rawRegions) || !(duration > 0)) {
+ return [];
+ }
+ interface RawRegion {
+ readonly eventIndex: number;
+ readonly name: string;
+ readonly startUs: number;
+ readonly durationUs: number;
+ readonly depth: number;
+ readonly leftPercent: number;
+ readonly widthPercent: number;
+ readonly bundlesLabel: string;
+ readonly color?: string;
+ readonly textColor?: string;
+ readonly source?: string;
+ }
+ const candidates: RawRegion[] = [];
+ for (const item of rawRegions) {
+ if (!isRecord(item)) {
+ continue;
+ }
+ const eventIndex =
+ typeof item['eventIndex'] === 'number' ? item['eventIndex'] : -1;
+ const name = typeof item['name'] === 'string' ? item['name'] : '';
+ const startUs = typeof item['startUs'] === 'number' ? item['startUs'] : NaN;
+ const durationUs =
+ typeof item['durationUs'] === 'number' ? item['durationUs'] : 0;
+ const depth = typeof item['depth'] === 'number' ? item['depth'] : 0;
+ const color =
+ typeof item['color'] === 'string' && item['color']
+ ? item['color']
+ : undefined;
+ const textColor =
+ typeof item['textColor'] === 'string' && item['textColor']
+ ? item['textColor']
+ : undefined;
+ const source =
+ typeof item['source'] === 'string' && item['source']
+ ? item['source']
+ : undefined;
+ if (eventIndex < 0 || !name || !Number.isFinite(startUs)) {
+ continue;
+ }
+ const leftPercent = clamp(
+ ((startUs - dataStartUs) / duration) * 100,
+ 0,
+ 100,
+ );
+ if (leftPercent >= 99.5) {
+ continue;
+ }
+ const widthPercent = clamp(
+ (durationUs / duration) * 100,
+ 0.8,
+ 100 - leftPercent,
+ );
+ const startBundle = Math.round(startUs);
+ const endBundle = Math.round(startUs + durationUs);
+ const bundlesLabel = isTimeAxis
+ ? formatMinimapTimeUs(durationUs)
+ : durationUs > 1
+ ? `${BUNDLE_FORMAT.format(startBundle)}–${BUNDLE_FORMAT.format(endBundle)}`
+ : BUNDLE_FORMAT.format(startBundle);
+ candidates.push({
+ eventIndex,
+ name,
+ startUs,
+ durationUs,
+ depth,
+ leftPercent,
+ widthPercent,
+ bundlesLabel,
+ color,
+ textColor,
+ source,
+ });
+ }
+
+ // Pack regions into up to 2 flame rows so nested sub-regions sit directly
+ // beneath their enclosing region, matching the viewer's flame chart levels.
+ const maxRows = 2;
+ const rowEndPercent = [-1, -1];
+ const assignedRows: number[] = [];
+ let usedRows = 1;
+ for (const candidate of candidates) {
+ let row = 0;
+ while (
+ row < maxRows - 1 &&
+ candidate.leftPercent < rowEndPercent[row] - 0.2
+ ) {
+ row++;
+ }
+ rowEndPercent[row] = Math.max(
+ rowEndPercent[row],
+ candidate.leftPercent + candidate.widthPercent,
+ );
+ assignedRows.push(row);
+ usedRows = Math.max(usedRows, row + 1);
+ }
+
+ const rowHeightPercent = Number((100 / usedRows).toFixed(2));
+ const palette =
+ theme.traceColors.length > 0 ? theme.traceColors : TRACE_VIEWER_PALETTE;
+ const regions: MinimapRegionMarker[] = [];
+ for (let i = 0; i < candidates.length; i++) {
+ const c = candidates[i];
+ const row = assignedRows[i];
+ const color = c.color ?? palette[i % palette.length];
+ const textColor =
+ c.textColor ??
+ getTextColorForContrast(color, theme.onSurface, theme.inverseOnSurface);
+ regions.push({
+ eventIndex: c.eventIndex,
+ name: c.name,
+ startUs: c.startUs,
+ durationUs: c.durationUs,
+ depth: c.depth,
+ row,
+ topPercent: Number(((row * 100) / usedRows).toFixed(2)),
+ heightPercent: rowHeightPercent,
+ leftPercent: c.leftPercent,
+ widthPercent: c.widthPercent,
+ color,
+ textColor,
+ bundlesLabel: c.bundlesLabel,
+ source: c.source,
+ });
+ }
+ return regions;
+}
+
+/**
+ * Horizontal schedule minimap for the Static Kernel Viewer and Trace Viewer.
+ *
+ * Renders a full-schedule VLIW functional unit or track ribbon above the
+ * timeline canvas, aligned pixel-for-pixel with the track label column and
+ * timeline ruler. Supports dragging the viewport lens to pan, dragging lens
+ * handles to resize/zoom, clicking or brushing outside the lens to jump/zoom,
+ * mouse-wheel zooming, and double-clicking or clicking Fit to reset to the full
+ * schedule.
+ */
+@Component({
+ standalone: true,
+ selector: 'kernel-minimap',
+ templateUrl: './kernel_minimap.ng.html',
+ styleUrls: ['./kernel_minimap.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [CommonModule, MatButtonModule, MatIconModule, MatTooltipModule],
+})
+export class KernelMinimap implements AfterViewInit, OnDestroy {
+ /** Emits when the user pans, resizes, or zooms the visible bundle range. */
+ @Output()
+ readonly visibleRangeChange = new EventEmitter<{
+ startUs: number;
+ endUs: number;
+ }>();
+
+ /** Emits when the user clicks a region marker to select that region. */
+ @Output() readonly selectEvent = new EventEmitter();
+
+ @ViewChild('trackElement') trackElement?: ElementRef;
+
+ state?: KernelMinimapState;
+ hoverInfo?: MinimapHoverInfo;
+ isDragging = false;
+ dragMode?: DragMode;
+
+ private dataStartUs = 0;
+ private dataEndUs = 0;
+ private visibleStartUs = 0;
+ private visibleEndUs = 0;
+ private labelWidthPx = DEFAULT_LABEL_WIDTH_PX;
+ private isTimeAxis = false;
+ private cachedTheme: MinimapTheme = DEFAULT_MINIMAP_THEME;
+ private cachedBins: readonly MinimapBin[] = [];
+ private cachedRegions: readonly MinimapRegionMarker[] = [];
+ private cachedPallasPrimitives: readonly MinimapRegionMarker[] = [];
+ private cachedTicks: readonly MinimapRulerTick[] = [];
+
+ private dragStartClientX = 0;
+ private dragTrackLeftPx = 0;
+ private dragTrackWidthPx = 1;
+ private dragInitialStartUs = 0;
+ private dragInitialEndUs = 0;
+ private scrubAnchorUs = 0;
+
+ private readonly changeDetectorRef = inject(ChangeDetectorRef);
+ private readonly ngZone = inject(NgZone);
+
+ private readonly minimapUpdatedListener = (event: Event) => {
+ this.onMinimapUpdated(event);
+ };
+ private readonly windowMouseMoveListener = (event: MouseEvent) => {
+ this.onWindowMouseMove(event);
+ };
+ private readonly windowMouseUpListener = (event: MouseEvent) => {
+ this.onWindowMouseUp(event);
+ };
+
+ ngAfterViewInit(): void {
+ this.ngZone.runOutsideAngular(() => {
+ window.addEventListener(
+ MINIMAP_UPDATED_EVENT_NAME,
+ this.minimapUpdatedListener,
+ );
+ });
+ }
+
+ ngOnDestroy(): void {
+ window.removeEventListener(
+ MINIMAP_UPDATED_EVENT_NAME,
+ this.minimapUpdatedListener,
+ );
+ this.stopDragging();
+ }
+
+ /** Resets the visible range to cover the full static kernel schedule. */
+ resetZoom(event?: MouseEvent): void {
+ event?.stopPropagation();
+ if (!(this.dataEndUs > this.dataStartUs)) {
+ return;
+ }
+ this.applyAndEmitVisibleRange(this.dataStartUs, this.dataEndUs);
+ }
+
+ private getMinVisibleSpan(): number {
+ return this.isTimeAxis ? MIN_VISIBLE_TIME_US : MIN_VISIBLE_BUNDLES;
+ }
+
+ /** Zooms in or out by a step factor centered on the current viewport. */
+ stepZoom(factor: number, event?: MouseEvent): void {
+ event?.stopPropagation();
+ const totalDuration = this.dataEndUs - this.dataStartUs;
+ const currentDuration = this.visibleEndUs - this.visibleStartUs;
+ if (!(totalDuration > 0) || !(currentDuration > 0)) {
+ return;
+ }
+ const center = (this.visibleStartUs + this.visibleEndUs) * 0.5;
+ const newDuration = clamp(
+ currentDuration * factor,
+ Math.min(this.getMinVisibleSpan(), totalDuration),
+ totalDuration,
+ );
+ let newStart = center - newDuration * 0.5;
+ let newEnd = newStart + newDuration;
+ if (newStart < this.dataStartUs) {
+ newStart = this.dataStartUs;
+ newEnd = newStart + newDuration;
+ }
+ if (newEnd > this.dataEndUs) {
+ newEnd = this.dataEndUs;
+ newStart = Math.max(this.dataStartUs, newEnd - newDuration);
+ }
+ this.applyAndEmitVisibleRange(newStart, newEnd);
+ }
+
+ onLensMouseDown(event: MouseEvent): void {
+ if (event.button !== 0) {
+ return;
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ this.startDragging('pan', event);
+ }
+
+ onLeftHandleMouseDown(event: MouseEvent): void {
+ if (event.button !== 0) {
+ return;
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ this.startDragging('resize-left', event);
+ }
+
+ onRightHandleMouseDown(event: MouseEvent): void {
+ if (event.button !== 0) {
+ return;
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ this.startDragging('resize-right', event);
+ }
+
+ onTrackMouseDown(event: MouseEvent): void {
+ if (event.button !== 0 || !(this.dataEndUs > this.dataStartUs)) {
+ return;
+ }
+ event.preventDefault();
+ this.startDragging('scrub', event);
+ this.scrubAnchorUs = this.clientXToBundle(event.clientX);
+ }
+
+ onTrackDoubleClick(event: MouseEvent): void {
+ event.preventDefault();
+ event.stopPropagation();
+ this.resetZoom();
+ }
+
+ onTrackWheel(event: WheelEvent): void {
+ const totalDuration = this.dataEndUs - this.dataStartUs;
+ const currentDuration = this.visibleEndUs - this.visibleStartUs;
+ if (!(totalDuration > 0) || !(currentDuration > 0)) {
+ return;
+ }
+ event.preventDefault();
+ event.stopPropagation();
+
+ const rect = this.trackElement?.nativeElement.getBoundingClientRect();
+ const trackWidth = Math.max(1, rect?.width ?? 1);
+ const pointerRatio = rect
+ ? clamp((event.clientX - rect.left) / trackWidth, 0, 1)
+ : 0.5;
+
+ // Horizontal wheel or Shift+wheel pans the viewport.
+ if (
+ Math.abs(event.deltaX) > Math.abs(event.deltaY) ||
+ (event.shiftKey && event.deltaY !== 0)
+ ) {
+ const deltaPx =
+ Math.abs(event.deltaX) > Math.abs(event.deltaY)
+ ? event.deltaX
+ : event.deltaY;
+ const deltaBundles = (deltaPx / trackWidth) * currentDuration * 0.5;
+ const shift = clamp(
+ deltaBundles,
+ this.dataStartUs - this.visibleStartUs,
+ this.dataEndUs - this.visibleEndUs,
+ );
+ this.applyAndEmitVisibleRange(
+ this.visibleStartUs + shift,
+ this.visibleEndUs + shift,
+ );
+ return;
+ }
+
+ if (event.deltaY === 0) {
+ return;
+ }
+ const zoomFactor = event.deltaY < 0 ? 0.8 : 1.25;
+ const newDuration = clamp(
+ currentDuration * zoomFactor,
+ Math.min(this.getMinVisibleSpan(), totalDuration),
+ totalDuration,
+ );
+ const anchorBundle = this.dataStartUs + pointerRatio * totalDuration;
+ // Keep the hovered point stationary relative to the new window.
+ const ratioWithinLens = clamp(
+ (anchorBundle - this.visibleStartUs) / currentDuration,
+ 0,
+ 1,
+ );
+ let newStart = anchorBundle - ratioWithinLens * newDuration;
+ let newEnd = newStart + newDuration;
+ if (newStart < this.dataStartUs) {
+ newStart = this.dataStartUs;
+ newEnd = newStart + newDuration;
+ }
+ if (newEnd > this.dataEndUs) {
+ newEnd = this.dataEndUs;
+ newStart = Math.max(this.dataStartUs, newEnd - newDuration);
+ }
+ this.applyAndEmitVisibleRange(newStart, newEnd);
+ }
+
+ onTrackMouseMove(event: MouseEvent): void {
+ if (this.isDragging || !this.state) {
+ return;
+ }
+ this.updateHoverInfo(event.clientX);
+ }
+
+ onTrackMouseLeave(): void {
+ if (this.hoverInfo !== undefined) {
+ this.hoverInfo = undefined;
+ this.changeDetectorRef.markForCheck();
+ }
+ }
+
+ onRegionMarkerClick(marker: MinimapRegionMarker, event: MouseEvent): void {
+ event.preventDefault();
+ event.stopPropagation();
+ this.selectEvent.emit(marker.eventIndex);
+ }
+
+ trackByBinIndex(index: number, bin: MinimapBin): number {
+ return bin.index;
+ }
+
+ trackByRegionIndex(index: number, marker: MinimapRegionMarker): number {
+ return marker.eventIndex;
+ }
+
+ trackByTickBundle(index: number, tick: MinimapRulerTick): number {
+ return tick.bundle;
+ }
+
+ private onMinimapUpdated(event: Event): void {
+ if (!(event instanceof CustomEvent) || !isRecord(event.detail)) {
+ return;
+ }
+ const detail = event.detail;
+ const dataStartUs =
+ typeof detail['dataStartUs'] === 'number' ? detail['dataStartUs'] : NaN;
+ const dataEndUs =
+ typeof detail['dataEndUs'] === 'number' ? detail['dataEndUs'] : NaN;
+ const visibleStartUs =
+ typeof detail['visibleStartUs'] === 'number'
+ ? detail['visibleStartUs']
+ : NaN;
+ const visibleEndUs =
+ typeof detail['visibleEndUs'] === 'number' ? detail['visibleEndUs'] : NaN;
+
+ if (
+ !Number.isFinite(dataStartUs) ||
+ !Number.isFinite(dataEndUs) ||
+ !(dataEndUs > dataStartUs) ||
+ !Number.isFinite(visibleStartUs) ||
+ !Number.isFinite(visibleEndUs)
+ ) {
+ if (this.state !== undefined) {
+ this.ngZone.run(() => {
+ this.state = undefined;
+ this.hoverInfo = undefined;
+ this.changeDetectorRef.markForCheck();
+ });
+ }
+ return;
+ }
+
+ const nextIsTimeAxis =
+ typeof detail['timeAxisUnit'] === 'string'
+ ? detail['timeAxisUnit'] === 'time'
+ : this.isTimeAxis;
+ const unitChanged = nextIsTimeAxis !== this.isTimeAxis;
+ this.isTimeAxis = nextIsTimeAxis;
+
+ const boundsChanged =
+ dataStartUs !== this.dataStartUs ||
+ dataEndUs !== this.dataEndUs ||
+ unitChanged;
+ this.dataStartUs = dataStartUs;
+ this.dataEndUs = dataEndUs;
+ this.visibleStartUs = clamp(visibleStartUs, dataStartUs, dataEndUs);
+ this.visibleEndUs = clamp(
+ Math.max(this.visibleStartUs, visibleEndUs),
+ dataStartUs,
+ dataEndUs,
+ );
+ if (
+ typeof detail['labelWidthPx'] === 'number' &&
+ detail['labelWidthPx'] > 0
+ ) {
+ this.labelWidthPx = Math.round(detail['labelWidthPx']);
+ }
+
+ if (isRecord(detail['theme'])) {
+ this.cachedTheme = parseMinimapTheme(detail['theme']);
+ }
+
+ if (Array.isArray(detail['bins'])) {
+ this.cachedBins = parseMinimapBins(detail['bins'], this.cachedTheme);
+ }
+ if (Array.isArray(detail['regions'])) {
+ this.cachedRegions = parseMinimapRegions(
+ detail['regions'],
+ dataStartUs,
+ dataEndUs,
+ this.cachedTheme,
+ this.isTimeAxis,
+ );
+ } else if (boundsChanged && this.cachedRegions.length > 0) {
+ this.cachedRegions = parseMinimapRegions(
+ this.cachedRegions,
+ dataStartUs,
+ dataEndUs,
+ this.cachedTheme,
+ this.isTimeAxis,
+ );
+ }
+ if (Array.isArray(detail['pallasPrimitives'])) {
+ this.cachedPallasPrimitives = parseMinimapRegions(
+ detail['pallasPrimitives'],
+ dataStartUs,
+ dataEndUs,
+ this.cachedTheme,
+ this.isTimeAxis,
+ );
+ } else if (boundsChanged && this.cachedPallasPrimitives.length > 0) {
+ this.cachedPallasPrimitives = parseMinimapRegions(
+ this.cachedPallasPrimitives,
+ dataStartUs,
+ dataEndUs,
+ this.cachedTheme,
+ this.isTimeAxis,
+ );
+ }
+ if (boundsChanged || this.cachedTicks.length === 0) {
+ this.cachedTicks = buildMinimapRulerTicks(
+ dataStartUs,
+ dataEndUs,
+ this.isTimeAxis,
+ );
+ }
+
+ this.ngZone.run(() => {
+ this.rebuildViewModel();
+ this.changeDetectorRef.markForCheck();
+ });
+ }
+
+ private startDragging(mode: DragMode, event: MouseEvent): void {
+ const rect = this.trackElement?.nativeElement.getBoundingClientRect();
+ this.dragTrackLeftPx = rect?.left ?? 0;
+ this.dragTrackWidthPx = Math.max(1, rect?.width ?? 1);
+ this.dragStartClientX = event.clientX;
+ this.dragInitialStartUs = this.visibleStartUs;
+ this.dragInitialEndUs = this.visibleEndUs;
+ this.isDragging = true;
+ this.dragMode = mode;
+ this.hoverInfo = undefined;
+
+ this.ngZone.runOutsideAngular(() => {
+ window.addEventListener('mousemove', this.windowMouseMoveListener);
+ window.addEventListener('mouseup', this.windowMouseUpListener);
+ });
+ this.changeDetectorRef.markForCheck();
+ }
+
+ private onWindowMouseMove(event: MouseEvent): void {
+ if (!this.isDragging || !this.dragMode) {
+ return;
+ }
+ const totalDuration = this.dataEndUs - this.dataStartUs;
+ if (!(totalDuration > 0)) {
+ return;
+ }
+ const deltaPx = event.clientX - this.dragStartClientX;
+ const deltaBundles = (deltaPx / this.dragTrackWidthPx) * totalDuration;
+ const minSpan = Math.min(this.getMinVisibleSpan(), totalDuration);
+
+ if (this.dragMode === 'pan') {
+ const span = this.dragInitialEndUs - this.dragInitialStartUs;
+ const clampedShift = clamp(
+ deltaBundles,
+ this.dataStartUs - this.dragInitialStartUs,
+ this.dataEndUs - this.dragInitialEndUs,
+ );
+ this.ngZone.run(() => {
+ this.applyAndEmitVisibleRange(
+ this.dragInitialStartUs + clampedShift,
+ this.dragInitialStartUs + clampedShift + span,
+ );
+ });
+ } else if (this.dragMode === 'resize-left') {
+ const newStart = clamp(
+ this.dragInitialStartUs + deltaBundles,
+ this.dataStartUs,
+ this.dragInitialEndUs - minSpan,
+ );
+ this.ngZone.run(() => {
+ this.applyAndEmitVisibleRange(newStart, this.dragInitialEndUs);
+ });
+ } else if (this.dragMode === 'resize-right') {
+ const newEnd = clamp(
+ this.dragInitialEndUs + deltaBundles,
+ this.dragInitialStartUs + minSpan,
+ this.dataEndUs,
+ );
+ this.ngZone.run(() => {
+ this.applyAndEmitVisibleRange(this.dragInitialStartUs, newEnd);
+ });
+ } else if (this.dragMode === 'scrub') {
+ if (Math.abs(deltaPx) >= BRUSH_DRAG_THRESHOLD_PX) {
+ const currentUs = this.clientXToBundle(event.clientX);
+ const startUs = clamp(
+ Math.min(this.scrubAnchorUs, currentUs),
+ this.dataStartUs,
+ this.dataEndUs - minSpan,
+ );
+ const endUs = clamp(
+ Math.max(this.scrubAnchorUs, currentUs),
+ startUs + minSpan,
+ this.dataEndUs,
+ );
+ this.ngZone.run(() => {
+ this.applyAndEmitVisibleRange(startUs, endUs);
+ });
+ }
+ }
+ }
+
+ private onWindowMouseUp(event: MouseEvent): void {
+ if (!this.isDragging) {
+ return;
+ }
+ const mode = this.dragMode;
+ const deltaPx = Math.abs(event.clientX - this.dragStartClientX);
+ this.stopDragging();
+
+ this.ngZone.run(() => {
+ if (mode === 'scrub' && deltaPx < BRUSH_DRAG_THRESHOLD_PX) {
+ // Clicking outside the lens centers the current viewport window at the
+ // clicked bundle.
+ const clickedBundle = this.clientXToBundle(event.clientX);
+ const span = this.dragInitialEndUs - this.dragInitialStartUs;
+ const totalDuration = this.dataEndUs - this.dataStartUs;
+ if (span > 0 && totalDuration > 0) {
+ const newStart = clamp(
+ clickedBundle - span * 0.5,
+ this.dataStartUs,
+ Math.max(this.dataStartUs, this.dataEndUs - span),
+ );
+ this.applyAndEmitVisibleRange(
+ newStart,
+ Math.min(this.dataEndUs, newStart + span),
+ );
+ }
+ }
+ this.changeDetectorRef.markForCheck();
+ });
+ }
+
+ private stopDragging(): void {
+ this.isDragging = false;
+ this.dragMode = undefined;
+ window.removeEventListener('mousemove', this.windowMouseMoveListener);
+ window.removeEventListener('mouseup', this.windowMouseUpListener);
+ }
+
+ private clientXToBundle(clientX: number): number {
+ const ratio = clamp(
+ (clientX - this.dragTrackLeftPx) / this.dragTrackWidthPx,
+ 0,
+ 1,
+ );
+ return this.dataStartUs + ratio * (this.dataEndUs - this.dataStartUs);
+ }
+
+ private updateHoverInfo(clientX: number): void {
+ const rect = this.trackElement?.nativeElement.getBoundingClientRect();
+ const totalDuration = this.dataEndUs - this.dataStartUs;
+ if (!rect || !(rect.width > 0) || !(totalDuration > 0)) {
+ return;
+ }
+ const ratio = clamp((clientX - rect.left) / rect.width, 0, 1);
+ const leftPercent = ratio * 100;
+ const hoveredUs = this.dataStartUs + ratio * totalDuration;
+ const bundle = Math.round(hoveredUs);
+ const binIndex =
+ this.cachedBins.length > 0
+ ? clamp(
+ Math.floor(ratio * this.cachedBins.length),
+ 0,
+ this.cachedBins.length - 1,
+ )
+ : -1;
+ const bin = binIndex >= 0 ? this.cachedBins[binIndex] : undefined;
+ const align: MinimapHoverInfo['align'] =
+ leftPercent < 14 ? 'left' : leftPercent > 86 ? 'right' : 'center';
+ const pallasPrimitive = bin?.pallasPrimitive || undefined;
+ const pallasColor = pallasPrimitive
+ ? (this.cachedPallasPrimitives.find(
+ (m) =>
+ m.name === pallasPrimitive &&
+ hoveredUs >= m.startUs &&
+ hoveredUs <= m.startUs + m.durationUs,
+ )?.color ??
+ this.cachedPallasPrimitives.find((m) => m.name === pallasPrimitive)
+ ?.color ??
+ this.cachedTheme.traceColors[2] ??
+ '#b1d284')
+ : undefined;
+
+ this.hoverInfo = {
+ leftPercent,
+ align,
+ bundleLabel: this.isTimeAxis
+ ? formatMinimapTimeUs(hoveredUs - this.dataStartUs)
+ : BUNDLE_FORMAT.format(bundle),
+ unit: bin?.unit,
+ secondaryUnit: bin?.secondaryUnit,
+ region: bin?.region || undefined,
+ pallasPrimitive,
+ pallasColor,
+ };
+ this.changeDetectorRef.markForCheck();
+ }
+
+ private applyAndEmitVisibleRange(startUs: number, endUs: number): void {
+ const clampedStart = clamp(startUs, this.dataStartUs, this.dataEndUs);
+ const clampedEnd = clamp(
+ Math.max(clampedStart, endUs),
+ this.dataStartUs,
+ this.dataEndUs,
+ );
+ this.visibleStartUs = clampedStart;
+ this.visibleEndUs = clampedEnd;
+ this.rebuildViewModel();
+ this.visibleRangeChange.emit({startUs: clampedStart, endUs: clampedEnd});
+ this.changeDetectorRef.markForCheck();
+ }
+
+ private rebuildViewModel(): void {
+ const totalDuration = this.dataEndUs - this.dataStartUs;
+ if (!(totalDuration > 0)) {
+ this.state = undefined;
+ return;
+ }
+ const visibleDuration = Math.max(
+ 0,
+ this.visibleEndUs - this.visibleStartUs,
+ );
+ const rawLeftPercent = clamp(
+ ((this.visibleStartUs - this.dataStartUs) / totalDuration) * 100,
+ 0,
+ 100,
+ );
+ const rawWidthPercent = clamp(
+ (visibleDuration / totalDuration) * 100,
+ 0,
+ 100,
+ );
+ const lensWidthPercent = Math.max(MIN_LENS_WIDTH_PERCENT, rawWidthPercent);
+ const lensLeftPercent = clamp(
+ rawLeftPercent,
+ 0,
+ Math.max(0, 100 - lensWidthPercent),
+ );
+ const rightCurtainLeftPercent = clamp(
+ lensLeftPercent + lensWidthPercent,
+ 0,
+ 100,
+ );
+ const rightCurtainWidthPercent = Math.max(0, 100 - rightCurtainLeftPercent);
+
+ const zoomFactor =
+ visibleDuration > 0 ? totalDuration / visibleDuration : 1;
+ const isZoomed = zoomFactor > 1.02;
+ const zoomLabel =
+ zoomFactor >= 10
+ ? `${Math.round(zoomFactor)}×`
+ : `${zoomFactor.toFixed(1).replace(/\.0$/, '')}×`;
+
+ const visibleRangeLabel = this.isTimeAxis
+ ? `${formatMinimapTimeUs(
+ this.visibleStartUs - this.dataStartUs,
+ false,
+ )}–${formatMinimapTimeUs(this.visibleEndUs - this.dataStartUs, false)}`
+ : `${BUNDLE_FORMAT.format(
+ Math.round(this.visibleStartUs),
+ )}–${BUNDLE_FORMAT.format(Math.round(this.visibleEndUs))}`;
+ const totalBundlesLabel = this.isTimeAxis
+ ? formatMinimapTimeUs(totalDuration, false)
+ : BUNDLE_FORMAT.format(Math.round(totalDuration));
+
+ const showStandaloneUnitLabel =
+ !this.isTimeAxis && !this.cachedTicks.some((tick) => tick.isRightEdge);
+
+ this.state = {
+ dataStartUs: this.dataStartUs,
+ dataEndUs: this.dataEndUs,
+ visibleStartUs: this.visibleStartUs,
+ visibleEndUs: this.visibleEndUs,
+ labelWidthPx: this.labelWidthPx,
+ isTimeAxis: this.isTimeAxis,
+ visibleRangeLabel,
+ totalBundlesLabel,
+ zoomLabel,
+ isZoomed,
+ lensLeftPercent,
+ lensWidthPercent,
+ rightCurtainLeftPercent,
+ rightCurtainWidthPercent,
+ theme: this.cachedTheme,
+ bins: this.cachedBins,
+ regions: this.cachedRegions,
+ pallasPrimitives: this.cachedPallasPrimitives,
+ hasPallasPrimitives: this.cachedPallasPrimitives.length > 0,
+ hasBothScopeRows:
+ this.cachedPallasPrimitives.length > 0 && this.cachedRegions.length > 0,
+ ticks: this.cachedTicks,
+ showStandaloneUnitLabel,
+ };
+ }
+}
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 08985db7e..d3c65484d 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
@@ -251,6 +251,11 @@
[customDrawerOpen]="isSelectionPanelOpen"
(initializeWasm)="onInitializeWasm()"
>
+
+
+
+
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 1d89eb6d3..dd4d5b3a5 100644
--- a/frontend/app/components/trace_viewer_container/trace_viewer_container.ts
+++ b/frontend/app/components/trace_viewer_container/trace_viewer_container.ts
@@ -8,6 +8,7 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
+ ContentChild,
CUSTOM_ELEMENTS_SCHEMA,
ElementRef,
EventEmitter,
@@ -39,6 +40,7 @@ import {ActivatedRoute} from '@angular/router';
import {AngularSplitModule} from 'angular-split';
import {NgxJsonViewerModule} from 'ngx-json-viewer';
+import {KernelMinimap} from 'org_xprof/frontend/app/components/static_kernel_viewer/kernel_minimap';
import {TimelinePlayer} from 'org_xprof/frontend/app/components/timeline_player/timeline_player';
import {getDefaultFeatureFlag} from 'org_xprof/frontend/app/components/trace_viewer_v2/feature_flags';
import {
@@ -353,6 +355,7 @@ declare interface TfTraceViewer {
imports: [
AngularSplitModule,
CommonModule,
+ KernelMinimap,
MatIconModule,
MatProgressBarModule,
PipesModule,
@@ -396,6 +399,22 @@ export class TraceViewerContainer
/** Whether the timeline player applies */
enableTimelinePlayer = false;
+ /** Whether the timeline overview minimap bar is enabled via feature flag. */
+ enableMinimap = false;
+
+ /** Projected minimap from hosts like StaticKernelViewer (avoids rendering a duplicate). */
+ @ContentChild(KernelMinimap) projectedMinimap?: KernelMinimap;
+
+ onMinimapVisibleRangeChange(range: {startUs: number; endUs: number}): void {
+ this.traceViewerModule?.application
+ ?.instance()
+ ?.setVisibleRange?.(range.startUs, range.endUs);
+ }
+
+ onMinimapSelectEvent(eventIndex: number): void {
+ this.traceViewerModule?.application?.instance()?.selectEvent?.(eventIndex);
+ }
+
private handleTimelineRedrawRequest = () => {
if (!this.traceViewerModule) return;
this.traceViewerModule.application.instance().scheduleForcedRedraw();
@@ -753,6 +772,7 @@ export class TraceViewerContainer
clearDeprecatedStorageKeys();
this.enableTimelinePlayer = this.readFeatureFlag('enable_timeline_player');
+ this.enableMinimap = this.readFeatureFlag('enable_minimap');
this.handleTimelineRedrawRequest =
this.handleTimelineRedrawRequest.bind(this);
diff --git a/frontend/app/components/trace_viewer_v2/main.ts b/frontend/app/components/trace_viewer_v2/main.ts
index 318467222..a4062c298 100644
--- a/frontend/app/components/trace_viewer_v2/main.ts
+++ b/frontend/app/components/trace_viewer_v2/main.ts
@@ -239,6 +239,7 @@ declare global {
setSearchQuery(query: string): void;
selectEvent(eventIndex: number): void;
setMouseMode(mode: number): void;
+ setVisibleRange?(startUs: number, endUs: number): void;
setVisibleFlowCategory(categoryId: number): void;
setVisibleFlowCategories(categoryIds: number[]): void;
scheduleForcedRedraw(): void;