-body {
- font-family:
- "Source Sans Pro", "Lucida Grande", "Lucida Sans Unicode", "Geneva",
- "Verdana", sans-serif;
+/* ==========================================================================
+ Flamegraph Viewer - CSS
+ Python-branded profiler with dark/light theme support
+ ========================================================================== */
+
+/* --------------------------------------------------------------------------
+ CSS Variables & Theme System
+ -------------------------------------------------------------------------- */
+
+:root {
+ /* Typography */
+ --font-sans: "Source Sans Pro", "Lucida Grande", "Lucida Sans Unicode",
+ "Geneva", "Verdana", sans-serif;
+ --font-mono: 'SF Mono', 'Monaco', 'Consolas', 'Liberation Mono', monospace;
+
+ /* Python brand colors (theme-independent) */
+ --python-blue: #3776ab;
+ --python-blue-light: #4584bb;
+ --python-blue-lighter: #5592cc;
+ --python-gold: #ffd43b;
+ --python-gold-dark: #ffcd02;
+ --python-gold-light: #ffdc5c;
+
+ /* Heat palette - defined per theme below */
+
+ /* Layout */
+ --sidebar-width: 280px;
+ --sidebar-collapsed: 44px;
+ --topbar-height: 52px;
+ --statusbar-height: 32px;
+
+ /* Transitions */
+ --transition-fast: 0.15s ease;
+ --transition-normal: 0.25s ease;
+}
+
+/* Light theme (default) - Python yellow-to-blue heat palette */
+:root, [data-theme="light"] {
+ --bg-primary: #ffffff;
+ --bg-secondary: #f8f9fa;
+ --bg-tertiary: #e9ecef;
+ --border: #e9ecef;
+ --border-subtle: #f0f2f5;
+
+ --text-primary: #2e3338;
+ --text-secondary: #5a6c7d;
+ --text-muted: #8b949e;
+
+ --accent: #3776ab;
+ --accent-hover: #2d5aa0;
+ --accent-glow: rgba(55, 118, 171, 0.15);
+
+ --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1);
+ --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.15);
+
+ --header-gradient: linear-gradient(135deg, #3776ab 0%, #4584bb 100%);
+
+ /* Light mode heat palette - blue to yellow to orange to red (cold to hot) */
+ --heat-1: #d6e9f8;
+ --heat-2: #a8d0ef;
+ --heat-3: #7ba3d1;
+ --heat-4: #ffe6a8;
+ --heat-5: #ffd43b;
+ --heat-6: #ffb84d;
+ --heat-7: #ff9966;
+ --heat-8: #ff6347;
+}
+
+/* Dark theme - teal-to-orange heat palette */
+[data-theme="dark"] {
+ --bg-primary: #0d1117;
+ --bg-secondary: #161b22;
+ --bg-tertiary: #21262d;
+ --border: #30363d;
+ --border-subtle: #21262d;
+
+ --text-primary: #e6edf3;
+ --text-secondary: #8b949e;
+ --text-muted: #6e7681;
+
+ --accent: #58a6ff;
+ --accent-hover: #79b8ff;
+ --accent-glow: rgba(88, 166, 255, 0.15);
+
+ --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
+ --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5);
+
+ --header-gradient: linear-gradient(135deg, #21262d 0%, #30363d 100%);
+
+ /* Dark mode heat palette - dark blue to teal to yellow to orange (cold to hot) */
+ --heat-1: #1e3a5f;
+ --heat-2: #2d5580;
+ --heat-3: #4a7ba7;
+ --heat-4: #5a9fa8;
+ --heat-5: #7ec488;
+ --heat-6: #c4de6a;
+ --heat-7: #f4d44d;
+ --heat-8: #ff6b35;
+}
+
+/* --------------------------------------------------------------------------
+ Base Styles
+ -------------------------------------------------------------------------- */
+
+*, *::before, *::after {
+ box-sizing: border-box;
+}
+
+html, body {
margin: 0;
padding: 0;
- background: #ffffff;
- color: #2e3338;
- line-height: 1.6;
+ height: 100%;
+ overflow: hidden;
}
-.header {
- background: linear-gradient(135deg, #3776ab 0%, #4584bb 100%);
- color: white;
- padding: 32px 0;
- box-shadow: 0 2px 10px rgba(55, 118, 171, 0.2);
+body {
+ font-family: var(--font-sans);
+ font-size: 14px;
+ line-height: 1.6;
+ color: var(--text-primary);
+ background: var(--bg-primary);
+ transition: background var(--transition-normal), color var(--transition-normal);
}
-.header-content {
- max-width: 1200px;
- margin: 0 auto;
- padding: 0 24px;
+/* --------------------------------------------------------------------------
+ Layout Structure
+ -------------------------------------------------------------------------- */
+
+.app-layout {
display: flex;
flex-direction: column;
+ height: 100vh;
+}
+
+.main-content {
+ display: flex;
+ flex: 1;
+ min-height: 0;
+}
+
+/* --------------------------------------------------------------------------
+ Top Bar
+ -------------------------------------------------------------------------- */
+
+.top-bar {
+ height: 56px;
+ background: var(--header-gradient);
+ display: flex;
align-items: center;
- justify-content: center;
- text-align: center;
- gap: 20px;
+ padding: 0 16px;
+ gap: 16px;
+ flex-shrink: 0;
+ box-shadow: 0 2px 10px rgba(55, 118, 171, 0.25);
+ border-bottom: 2px solid var(--python-gold);
}
-.python-logo {
- width: auto;
- height: 72px;
- margin-bottom: 12px; /* tighter spacing to avoid visual gap */
+/* Brand / Logo */
+.brand {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ color: white;
+ text-decoration: none;
flex-shrink: 0;
+}
+
+.brand-logo {
display: flex;
align-items: center;
justify-content: center;
+ width: 28px;
+ height: 28px;
+ flex-shrink: 0;
+}
+
+/* Style the inlined SVG/img inside brand-logo */
+.brand-logo svg,
+.brand-logo img {
+ width: 28px;
+ height: 28px;
+ display: block;
+ object-fit: contain;
+ filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2));
}
-.python-logo img {
- height: 72px;
- width: auto;
- display: block; /* avoid baseline alignment issues */
- vertical-align: middle;
- /* subtle shadow that does not affect layout */
- filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.1));
+.brand-info {
+ display: flex;
+ flex-direction: column;
+ line-height: 1.15;
}
-.header-text h1 {
- margin: 0;
- font-size: 2.5em;
- font-weight: 600;
- color: white;
- text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+.brand-text {
+ font-weight: 700;
+ font-size: 16px;
+ letter-spacing: -0.3px;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
}
-.header-text .subtitle {
- margin: 8px 0 0 0;
- font-size: 1.1em;
- color: rgba(255, 255, 255, 0.9);
- font-weight: 300;
+.brand-subtitle {
+ font-weight: 500;
+ font-size: 10px;
+ opacity: 0.9;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
}
-.header-search {
- width: 100%;
- max-width: 500px;
+.brand-divider {
+ width: 1px;
+ height: 16px;
+ background: rgba(255, 255, 255, 0.3);
+}
+
+/* Search */
+.search-wrapper {
+ flex: 1;
+ max-width: 360px;
+ position: relative;
}
-.header-search #search-input {
+.search-input {
width: 100%;
- padding: 12px 20px;
- border: 2px solid rgba(255, 255, 255, 0.2);
- border-radius: 25px;
- font-size: 16px;
- font-family: inherit;
- background: rgba(255, 255, 255, 0.95);
+ padding: 8px 36px 8px 14px;
+ font-family: var(--font-sans);
+ font-size: 13px;
color: #2e3338;
- transition: all 0.3s ease;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
- backdrop-filter: blur(10px);
+ background: rgba(255, 255, 255, 0.95);
+ border: 2px solid rgba(255, 255, 255, 0.3);
+ border-radius: 20px;
+ outline: none;
+ transition: all var(--transition-fast);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
-.header-search #search-input:focus {
- outline: none;
+.search-input::placeholder {
+ color: #6c757d;
+}
+
+.search-input:focus {
border-color: rgba(255, 255, 255, 0.8);
- background: rgba(255, 255, 255, 1);
- box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
- transform: translateY(-2px);
+ background: white;
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
+}
+
+/* Dark theme search input */
+[data-theme="dark"] .search-input {
+ color: #e6edf3;
+ background: rgba(33, 38, 45, 0.95);
+ border: 2px solid rgba(88, 166, 255, 0.3);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
-.header-search #search-input::placeholder {
+[data-theme="dark"] .search-input::placeholder {
+ color: #8b949e;
+}
+
+[data-theme="dark"] .search-input:focus {
+ border-color: rgba(88, 166, 255, 0.6);
+ background: rgba(33, 38, 45, 1);
+ box-shadow: 0 4px 16px rgba(88, 166, 255, 0.2);
+}
+
+.search-input.has-matches {
+ border-color: rgba(40, 167, 69, 0.8);
+ box-shadow: 0 4px 16px rgba(40, 167, 69, 0.2);
+}
+
+.search-input.no-matches {
+ border-color: rgba(220, 53, 69, 0.8);
+ box-shadow: 0 4px 16px rgba(220, 53, 69, 0.2);
+}
+
+.search-clear {
+ position: absolute;
+ right: 10px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 20px;
+ height: 20px;
+ padding: 0;
+ display: none;
+ align-items: center;
+ justify-content: center;
+ font-size: 14px;
+ line-height: 1;
color: #6c757d;
+ background: transparent;
+ border: none;
+ border-radius: 50%;
+ cursor: pointer;
+ transition: color var(--transition-fast);
}
-.stats-section {
- background: #ffffff;
- padding: 24px 0;
- border-bottom: 1px solid #e9ecef;
+.search-clear:hover {
+ color: #2e3338;
}
-.stats-container {
- max-width: 1200px;
- margin: 0 auto;
- padding: 0 24px;
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 20px;
+[data-theme="dark"] .search-clear {
+ color: #8b949e;
}
-/* Compact Thread Stats Bar - Colorful Square Design */
-.thread-stats-bar {
- background: rgba(255, 255, 255, 0.95);
- padding: 12px 24px;
+[data-theme="dark"] .search-clear:hover {
+ color: #e6edf3;
+}
+
+.search-wrapper.has-value .search-clear {
+ display: flex;
+}
+
+/* Toolbar */
+.toolbar {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-left: auto;
+}
+
+.toolbar-btn {
display: flex;
align-items: center;
justify-content: center;
- gap: 16px;
- font-size: 13px;
- box-shadow: 0 2px 8px rgba(55, 118, 171, 0.2);
+ width: 32px;
+ height: 32px;
+ padding: 0;
+ font-size: 15px;
+ color: white;
+ background: rgba(255, 255, 255, 0.12);
+ border: 1px solid rgba(255, 255, 255, 0.18);
+ border-radius: 6px;
+ cursor: pointer;
+ transition: all var(--transition-fast);
}
-.thread-stat-item {
- display: inline-flex;
+.toolbar-btn:hover {
+ background: rgba(255, 255, 255, 0.22);
+ border-color: rgba(255, 255, 255, 0.35);
+}
+
+.toolbar-btn:active {
+ transform: scale(0.95);
+}
+
+/* --------------------------------------------------------------------------
+ Sidebar
+ -------------------------------------------------------------------------- */
+
+.sidebar {
+ width: var(--sidebar-width);
+ background: var(--bg-secondary);
+ border-right: 1px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ flex-shrink: 0;
+ overflow: hidden;
+ position: relative;
+}
+
+.sidebar.collapsed {
+ width: var(--sidebar-collapsed) !important;
+ transition: width var(--transition-normal);
+}
+
+.sidebar-toggle {
+ position: absolute;
+ top: 12px;
+ right: 10px;
+ width: 26px;
+ height: 26px;
+ display: flex;
align-items: center;
- gap: 8px;
- background: white;
- padding: 6px 14px;
- border-radius: 4px;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
- transition: all 0.3s ease;
- border: 2px solid;
- min-width: 115px;
justify-content: center;
- animation: fadeIn 0.5s ease-out backwards;
+ color: var(--text-muted);
+ background: var(--bg-primary);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ cursor: pointer;
+ transition: all var(--transition-fast);
+ z-index: 10;
}
-.thread-stat-item:nth-child(1) { animation-delay: 0s; }
-.thread-stat-item:nth-child(3) { animation-delay: 0.1s; }
-.thread-stat-item:nth-child(5) { animation-delay: 0.2s; }
-.thread-stat-item:nth-child(7) { animation-delay: 0.3s; }
+.sidebar-toggle svg {
+ transition: transform var(--transition-fast);
+}
-@keyframes fadeIn {
- from {
- opacity: 0;
- }
- to {
- opacity: 1;
- }
+.sidebar-toggle:hover {
+ color: var(--accent);
+ border-color: var(--accent);
+ background: var(--accent-glow);
}
-@keyframes slideUp {
- from {
- opacity: 0;
- transform: translateY(15px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
+.sidebar.collapsed .sidebar-toggle {
+ right: 9px;
}
-@keyframes gentlePulse {
- 0%, 100% { box-shadow: 0 2px 8px rgba(55, 118, 171, 0.15); }
- 50% { box-shadow: 0 2px 16px rgba(55, 118, 171, 0.4); }
+.sidebar.collapsed .sidebar-toggle svg {
+ transform: rotate(180deg);
}
-/* Color-coded borders and subtle glow on hover */
-#gil-held-stat {
- --stat-color: 40, 167, 69;
- border-color: rgb(var(--stat-color));
- background: linear-gradient(135deg, rgba(var(--stat-color), 0.06) 0%, #ffffff 100%);
+.sidebar-content {
+ flex: 1;
+ overflow-y: auto;
+ padding: 44px 14px 14px;
}
-#gil-released-stat {
- --stat-color: 220, 53, 69;
- border-color: rgb(var(--stat-color));
- background: linear-gradient(135deg, rgba(var(--stat-color), 0.06) 0%, #ffffff 100%);
+.sidebar.collapsed .sidebar-content {
+ display: none;
}
-#gil-waiting-stat {
- --stat-color: 255, 193, 7;
- border-color: rgb(var(--stat-color));
- background: linear-gradient(135deg, rgba(var(--stat-color), 0.06) 0%, #ffffff 100%);
+.sidebar-resize-handle {
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 6px;
+ height: 100%;
+ cursor: col-resize;
+ background: transparent;
+ transition: background var(--transition-fast);
+ z-index: 11;
}
-#gc-stat {
- --stat-color: 111, 66, 193;
- border-color: rgb(var(--stat-color));
- background: linear-gradient(135deg, rgba(var(--stat-color), 0.06) 0%, #ffffff 100%);
+.sidebar-resize-handle:hover,
+.sidebar-resize-handle.resizing {
+ background: var(--python-gold);
}
-#gil-held-stat:hover,
-#gil-released-stat:hover,
-#gil-waiting-stat:hover,
-#gc-stat:hover {
- box-shadow: 0 0 12px rgba(var(--stat-color), 0.4), 0 1px 3px rgba(0, 0, 0, 0.08);
+.sidebar-resize-handle::before {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 2px;
+ height: 40px;
+ background: var(--border);
+ border-radius: 1px;
+ opacity: 0;
+ transition: opacity var(--transition-fast);
}
-.thread-stat-item .stat-label {
- color: #5a6c7d;
- font-weight: 600;
- font-size: 11px;
- letter-spacing: 0.3px;
+.sidebar-resize-handle:hover::before {
+ opacity: 1;
}
-.thread-stat-item .stat-value {
- color: #2e3338;
- font-weight: 800;
- font-size: 14px;
- font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
+.sidebar.collapsed .sidebar-resize-handle {
+ display: none;
}
-.thread-stat-separator {
- color: rgba(0, 0, 0, 0.15);
- font-weight: 300;
- font-size: 16px;
- position: relative;
- z-index: 1;
+body.resizing-sidebar {
+ cursor: col-resize;
+ user-select: none;
}
-/* Responsive - stack on small screens */
-@media (max-width: 768px) {
- .thread-stats-bar {
- flex-wrap: wrap;
- gap: 8px;
- font-size: 11px;
- padding: 10px 16px;
- }
+/* Sidebar Logo */
+.sidebar-logo {
+ display: flex;
+ justify-content: center;
+ margin-bottom: 16px;
+}
- .thread-stat-item {
- padding: 4px 10px;
- }
+.sidebar-logo-img {
+ width: 90px;
+ height: 90px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
- .thread-stat-item .stat-label {
- font-size: 11px;
- }
+.sidebar-logo-img svg,
+.sidebar-logo-img img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+}
- .thread-stat-item .stat-value {
- font-size: 12px;
- }
+/* Sidebar sections */
+.sidebar-section {
+ margin-bottom: 20px;
+}
- .thread-stat-separator {
- display: none;
- }
+.sidebar-section:last-child {
+ margin-bottom: 0;
}
-.stat-card {
- background: #ffffff;
- border: 1px solid #e9ecef;
- border-radius: 12px;
- padding: 20px;
+.section-title {
+ font-size: 10px;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.8px;
+ color: var(--accent);
+ margin: 0;
+ flex: 1;
+}
+
+/* Collapsible sections */
+.collapsible .section-header {
display: flex;
- align-items: flex-start;
- gap: 16px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
- transition: all 0.2s ease;
- min-height: 120px;
+ align-items: center;
+ width: 100%;
+ padding: 0 0 8px 0;
+ margin-bottom: 10px;
+ background: none;
+ border: none;
+ border-bottom: 2px solid var(--python-gold);
+ cursor: pointer;
+ transition: all var(--transition-fast);
+}
+
+.collapsible .section-header:hover {
+ opacity: 0.8;
+}
+
+.section-chevron {
+ color: var(--text-muted);
+ transition: transform var(--transition-fast);
+}
+
+.collapsible.collapsed .section-chevron {
+ transform: rotate(-90deg);
+}
+
+.section-content {
+ overflow: hidden;
+ transition: max-height var(--transition-normal), opacity var(--transition-normal);
+ max-height: 1000px;
+ opacity: 1;
+}
+
+.collapsible.collapsed .section-content {
+ max-height: 0;
+ opacity: 0;
+ margin-bottom: -10px;
+}
+
+/* --------------------------------------------------------------------------
+ Profile Summary Cards
+ -------------------------------------------------------------------------- */
+
+.summary-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 6px;
+}
+
+.summary-card {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 10px;
+ background: var(--bg-primary);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ transition: all var(--transition-fast);
animation: slideUp 0.4s ease-out backwards;
+ animation-delay: calc(var(--i, 0) * 0.05s);
+ overflow: hidden;
}
-.stat-card:nth-child(1) { animation-delay: 0.1s; }
-.stat-card:nth-child(2) { animation-delay: 0.2s; }
-.stat-card:nth-child(3) { animation-delay: 0.3s; }
+.summary-card:nth-child(1) { --i: 0; }
+.summary-card:nth-child(2) { --i: 1; }
+.summary-card:nth-child(3) { --i: 2; }
+.summary-card:nth-child(4) { --i: 3; }
-.stat-card:hover {
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
- transform: translateY(-2px);
+.summary-card:hover {
+ border-color: var(--accent);
+ background: var(--accent-glow);
}
-.stat-icon {
- font-size: 32px;
- width: 56px;
- height: 56px;
+.summary-icon {
+ font-size: 16px;
+ width: 28px;
+ height: 28px;
display: flex;
align-items: center;
justify-content: center;
- background: linear-gradient(135deg, #3776ab 0%, #4584bb 100%);
- border-radius: 50%;
+ background: var(--bg-tertiary);
+ border-radius: 6px;
flex-shrink: 0;
- box-shadow: 0 2px 8px rgba(55, 118, 171, 0.3);
}
-.stat-content {
+.summary-data {
+ min-width: 0;
flex: 1;
+ overflow: hidden;
}
-.stat-label {
- font-size: 14px;
- color: #5a6c7d;
- font-weight: 500;
- margin-bottom: 4px;
+.summary-value {
+ font-family: var(--font-mono);
+ font-size: 13px;
+ font-weight: 700;
+ color: var(--accent);
+ line-height: 1.2;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.summary-label {
+ font-size: 8px;
+ font-weight: 600;
+ color: var(--text-muted);
text-transform: uppercase;
- letter-spacing: 0.5px;
+ letter-spacing: 0.2px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
-.stat-value {
- font-size: 16px;
+/* Efficiency Bar */
+.efficiency-section {
+ margin-top: 10px;
+ padding-top: 10px;
+ border-top: 1px solid var(--border);
+}
+
+.efficiency-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 5px;
+}
+
+.efficiency-label {
+ font-size: 9px;
+ font-weight: 600;
+ color: var(--text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.2px;
+}
+
+.efficiency-value {
+ font-family: var(--font-mono);
+ font-size: 11px;
font-weight: 700;
- color: #2e3338;
- line-height: 1.3;
- margin-bottom: 4px;
- word-break: break-word;
- overflow-wrap: break-word;
+ color: var(--accent);
}
-.stat-file {
- font-size: 12px;
- color: #8b949e;
- font-weight: 400;
- margin-bottom: 2px;
- font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
- word-break: break-word;
- overflow-wrap: break-word;
+.efficiency-bar {
+ height: 6px;
+ background: var(--bg-tertiary);
+ border-radius: 3px;
+ overflow: hidden;
}
-.stat-detail {
- font-size: 12px;
- color: #5a6c7d;
- font-weight: 400;
- line-height: 1.4;
- font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
- word-break: break-word;
- overflow-wrap: break-word;
+.efficiency-fill {
+ height: 100%;
+ background: linear-gradient(90deg, #28a745 0%, #20c997 50%, #17a2b8 100%);
+ border-radius: 3px;
+ transition: width 0.6s ease-out;
+ position: relative;
+ overflow: hidden;
}
-.controls {
- background: #f8f9fa;
- border-bottom: 1px solid #e9ecef;
- padding: 20px 0;
- text-align: center;
+.efficiency-fill::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(
+ 90deg,
+ transparent 0%,
+ rgba(255, 255, 255, 0.4) 50%,
+ transparent 100%
+ );
+ animation: shimmer 2s ease-in-out infinite;
+}
+
+@keyframes shimmer {
+ 0% { left: -100%; }
+ 100% { left: 100%; }
+}
+
+/* --------------------------------------------------------------------------
+ Thread Stats Grid (in Sidebar)
+ -------------------------------------------------------------------------- */
+
+.thread-stats-section {
+ display: block;
}
-.controls-content {
- max-width: 1200px;
- margin: 0 auto;
- padding: 0 24px;
+.stats-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 8px;
+}
+
+.stat-tile {
+ background: var(--bg-primary);
+ border-radius: 8px;
+ padding: 10px;
text-align: center;
+ border: 2px solid var(--border);
+ transition: all var(--transition-fast);
+ animation: fadeIn 0.4s ease-out backwards;
+ animation-delay: calc(var(--i, 0) * 0.05s);
}
+.stat-tile:nth-child(1) { --i: 0; }
+.stat-tile:nth-child(2) { --i: 1; }
+.stat-tile:nth-child(3) { --i: 2; }
+.stat-tile:nth-child(4) { --i: 3; }
-.controls button {
- background: #3776ab;
- color: white;
- border: none;
- padding: 12px 24px;
- margin: 0 8px;
- border-radius: 6px;
- cursor: pointer;
+.stat-tile:hover {
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-sm);
+}
+
+.stat-tile-value {
+ font-family: var(--font-mono);
+ font-size: 16px;
+ font-weight: 700;
+ color: var(--text-primary);
+ line-height: 1.2;
+}
+
+.stat-tile-label {
+ font-size: 9px;
font-weight: 600;
- font-size: 14px;
- font-family: inherit;
- transition: all 0.2s ease;
- box-shadow: 0 2px 4px rgba(55, 118, 171, 0.2);
+ text-transform: uppercase;
+ letter-spacing: 0.3px;
+ color: var(--text-muted);
+ margin-top: 2px;
}
-.controls button:hover {
- background: #2d5aa0;
- transform: translateY(-1px);
- box-shadow: 0 4px 8px rgba(55, 118, 171, 0.3);
+/* Stat tile color variants */
+.stat-tile--green { --tile-color: 40, 167, 69; --tile-text: #28a745; }
+.stat-tile--red { --tile-color: 220, 53, 69; --tile-text: #dc3545; }
+.stat-tile--yellow { --tile-color: 255, 193, 7; --tile-text: #d39e00; }
+.stat-tile--purple { --tile-color: 111, 66, 193; --tile-text: #6f42c1; }
+
+.stat-tile[class*="--"] {
+ border-color: rgba(var(--tile-color), 0.4);
+ background: linear-gradient(135deg, rgba(var(--tile-color), 0.08) 0%, var(--bg-primary) 100%);
}
+.stat-tile[class*="--"] .stat-tile-value { color: var(--tile-text); }
+
+/* --------------------------------------------------------------------------
+ Hotspot Cards
+ -------------------------------------------------------------------------- */
-.controls button:active {
- transform: translateY(1px);
- box-shadow: 0 1px 2px rgba(55, 118, 171, 0.2);
+.hotspot {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ padding: 10px;
+ margin-bottom: 8px;
+ background: var(--bg-primary);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ cursor: pointer;
+ transition: all var(--transition-fast);
+ opacity: 0;
+ transform: translateY(8px);
+ box-shadow: var(--shadow-sm);
}
-.controls button.secondary {
- background: #ffd43b;
- color: #2e3338;
+.hotspot.visible {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+.hotspot:hover {
+ border-color: var(--accent);
+ box-shadow: var(--shadow-md);
+ transform: translateY(-2px);
}
-.controls button.secondary:hover {
- background: #ffcd02;
+.hotspot.active {
+ border-color: var(--python-gold);
+ background: var(--accent-glow);
+ box-shadow: 0 0 0 3px var(--accent-glow);
}
-.controls button.secondary:active {
- background: #e6b800;
+.hotspot:last-child {
+ margin-bottom: 0;
}
-.thread-filter-wrapper {
- display: none;
+.hotspot-rank {
+ width: 26px;
+ height: 26px;
+ border-radius: 50%;
+ display: flex;
align-items: center;
- margin-left: 16px;
- background: white;
- border-radius: 6px;
- padding: 4px 8px 4px 12px;
- border: 2px solid #3776ab;
- transition: all 0.2s ease;
+ justify-content: center;
+ font-weight: 700;
+ font-size: 12px;
+ flex-shrink: 0;
+ background: linear-gradient(135deg, var(--python-blue) 0%, var(--python-blue-light) 100%);
+ color: white;
+ box-shadow: 0 2px 4px rgba(55, 118, 171, 0.3);
}
-.thread-filter-wrapper:hover {
- border-color: #2d5aa0;
- box-shadow: 0 2px 6px rgba(55, 118, 171, 0.2);
+.hotspot-rank--1 { background: linear-gradient(135deg, #d4af37, #f4d03f); color: #5a4a00; }
+.hotspot-rank--2 { background: linear-gradient(135deg, #a8a8a8, #c0c0c0); color: #4a4a4a; }
+.hotspot-rank--3 { background: linear-gradient(135deg, #cd7f32, #e6a55a); color: #5a3d00; }
+
+.hotspot-info {
+ flex: 1;
+ min-width: 0;
}
-.thread-filter-label {
- color: #3776ab;
- font-size: 14px;
+.hotspot-func {
+ font-family: var(--font-mono);
+ font-size: 11px;
font-weight: 600;
- margin-right: 8px;
+ color: var(--text-primary);
+ line-height: 1.3;
+ word-break: break-word;
+ margin-bottom: 2px;
+}
+
+.hotspot-file {
+ font-family: var(--font-mono);
+ font-size: 10px;
+ color: var(--text-muted);
+ margin-bottom: 3px;
+ word-break: break-all;
+}
+
+.hotspot-stats {
+ font-family: var(--font-mono);
+ font-size: 10px;
+ color: var(--text-secondary);
+}
+
+.hotspot-percent {
+ color: var(--accent);
+ font-weight: 600;
+}
+
+/* --------------------------------------------------------------------------
+ Legend
+ -------------------------------------------------------------------------- */
+
+.legend-section {
+ margin-top: auto;
+ padding-top: 12px;
+}
+
+.legend {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.legend-item {
display: flex;
align-items: center;
+ gap: 8px;
+ padding: 5px 8px;
+ background: var(--bg-primary);
+ border-radius: 4px;
+ border: 1px solid var(--border-subtle);
+ font-size: 11px;
}
-.thread-filter-select {
- background: transparent;
- color: #2e3338;
- border: none;
- padding: 4px 24px 4px 4px;
- font-size: 14px;
+.legend-color {
+ width: 20px;
+ height: 10px;
+ border-radius: 2px;
+ flex-shrink: 0;
+ border: 1px solid rgba(0, 0, 0, 0.08);
+}
+
+.legend-label {
+ color: var(--text-primary);
+ font-weight: 500;
+ flex: 1;
+}
+
+.legend-range {
+ font-family: var(--font-mono);
+ font-size: 9px;
+ color: var(--text-muted);
+}
+
+/* --------------------------------------------------------------------------
+ Thread Filter
+ -------------------------------------------------------------------------- */
+
+.filter-section {
+ padding-top: 12px;
+ border-top: 1px solid var(--border);
+}
+
+.filter-label {
+ display: block;
+ font-size: 10px;
font-weight: 600;
+ color: var(--text-muted);
+ margin-bottom: 6px;
+}
+
+.filter-select {
+ width: 100%;
+ padding: 7px 28px 7px 10px;
+ font-family: var(--font-mono);
+ font-size: 11px;
+ color: var(--text-primary);
+ background: var(--bg-primary);
+ border: 2px solid var(--accent);
+ border-radius: 6px;
cursor: pointer;
- min-width: 120px;
- font-family: inherit;
+ outline: none;
appearance: none;
- -webkit-appearance: none;
- -moz-appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%233776ab' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
- background-position: right 4px center;
- background-size: 16px;
+ background-position: right 6px center;
+ background-size: 14px;
+ transition: all var(--transition-fast);
}
-.thread-filter-select:focus {
- outline: none;
+.filter-select:hover {
+ border-color: var(--accent-hover);
+ box-shadow: 0 2px 6px var(--accent-glow);
}
-.thread-filter-select:hover {
- color: #3776ab;
+.filter-select:focus {
+ border-color: var(--accent);
+ box-shadow: 0 0 0 3px var(--accent-glow);
}
-.thread-filter-select option {
- padding: 8px;
- background: white;
- color: #2e3338;
- font-weight: normal;
+/* --------------------------------------------------------------------------
+ Chart Area
+ -------------------------------------------------------------------------- */
+
+.chart-area {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ background: var(--bg-primary);
+ position: relative;
}
#chart {
width: 100%;
- height: calc(100vh - 160px);
- overflow: hidden;
- background: #ffffff;
- padding: 0 40px;
+ height: 100%;
+ padding: 16px;
+ overflow: auto;
}
+/* D3 Flamegraph overrides */
.d3-flame-graph rect {
- /* Prefer selector specificity instead of !important */
stroke: rgba(55, 118, 171, 0.3);
stroke-width: 1px;
cursor: pointer;
- transition: all 0.1s ease;
+ transition: filter 0.1s ease;
}
.d3-flame-graph rect:hover {
- stroke: #3776ab;
+ stroke: var(--python-blue);
stroke-width: 2px;
- filter: brightness(1.05);
+ filter: brightness(1.08);
}
.d3-flame-graph text {
- /* Ensure labels use our font without !important */
- font-family: "Source Sans Pro", sans-serif;
+ font-family: var(--font-sans);
font-size: 12px;
font-weight: 500;
- fill: #2e3338;
+ fill: var(--text-primary);
pointer-events: none;
}
-.info-panel {
- position: fixed;
- bottom: 24px;
- left: 84px; /* Leave space for the button */
- background: white;
- padding: 24px;
- border-radius: 8px;
- border: 1px solid #e9ecef;
- font-size: 14px;
- max-width: 280px;
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
- z-index: 1000;
- display: none;
+/* Search highlight */
+.d3-flame-graph rect.search-match {
+ stroke: #ff6b35 !important;
+ stroke-width: 2px !important;
+ stroke-dasharray: 4 2;
}
-.info-panel h3 {
- margin: 0 0 16px 0;
- color: #3776ab;
- font-weight: 600;
- font-size: 16px;
- border-bottom: 2px solid #ffd43b;
- padding-bottom: 8px;
+.d3-flame-graph rect.search-dim {
+ opacity: 0.25;
}
-.info-panel p {
- margin: 12px 0;
- color: #5a6c7d;
- line-height: 1.5;
-}
+/* --------------------------------------------------------------------------
+ Status Bar
+ -------------------------------------------------------------------------- */
-.info-panel strong {
- color: #3776ab;
+.status-bar {
+ height: var(--statusbar-height);
+ background: var(--bg-secondary);
+ border-top: 1px solid var(--border);
+ display: flex;
+ align-items: center;
+ padding: 0 16px;
+ gap: 16px;
+ font-family: var(--font-mono);
+ font-size: 11px;
+ color: var(--text-secondary);
+ flex-shrink: 0;
}
-#show-info-btn {
- position: fixed;
- bottom: 32px;
- left: 32px;
- z-index: 1100;
- width: 44px;
- height: 44px;
- border-radius: 50%;
- background: #3776ab;
- color: white;
- border: none;
- font-size: 24px;
- box-shadow: 0 2px 8px rgba(55, 118, 171, 0.15);
- cursor: pointer;
+.status-item {
display: flex;
align-items: center;
- justify-content: center;
- transition: background 0.2s, transform 0.2s;
- animation: gentlePulse 3s ease-in-out infinite;
+ gap: 5px;
}
-#show-info-btn:hover {
- background: #2d5aa0;
- animation: none;
- transform: scale(1.05);
+.status-item::before {
+ content: '';
+ width: 4px;
+ height: 4px;
+ background: var(--python-gold);
+ border-radius: 50%;
}
-#close-info-btn {
- position: absolute;
- top: 8px;
- right: 12px;
- background: none;
- border: none;
- font-size: 20px;
- cursor: pointer;
- color: #3776ab;
+.status-item:first-child::before {
+ display: none;
}
-@media (max-width: 600px) {
- .python-logo { height: 48px; }
- .python-logo img { height: 48px; }
- #show-info-btn {
- left: 8px;
- bottom: 8px;
- }
- .info-panel {
- left: 60px; /* Still leave space for button */
- bottom: 8px;
- max-width: 90vw;
- }
+.status-label {
+ color: var(--text-muted);
}
-.legend-panel {
- position: fixed;
- top: 24px;
- left: 24px;
- background: white;
- padding: 24px;
+.status-value {
+ color: var(--text-primary);
+ font-weight: 500;
+}
+
+.status-value.accent {
+ color: var(--accent);
+ font-weight: 600;
+}
+
+/* --------------------------------------------------------------------------
+ Tooltip
+ -------------------------------------------------------------------------- */
+
+.python-tooltip {
+ position: absolute;
+ z-index: 1000;
+ pointer-events: none;
+ background: var(--bg-primary);
+ border: 1px solid var(--border);
border-radius: 8px;
- border: 1px solid #e9ecef;
+ padding: 14px;
+ max-width: 480px;
+ box-shadow: var(--shadow-lg);
+ font-family: var(--font-sans);
+ font-size: 13px;
+ color: var(--text-primary);
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+ line-height: 1.5;
+}
+
+.tooltip-header {
+ margin-bottom: 10px;
+}
+
+.tooltip-title {
font-size: 14px;
- max-width: 320px;
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
- display: none;
- z-index: 1001;
+ font-weight: 600;
+ color: var(--accent);
+ line-height: 1.3;
+ word-break: break-word;
+ margin-bottom: 4px;
}
-.legend-panel h3 {
- margin: 0 0 20px 0;
- color: #3776ab;
+.tooltip-location {
+ font-family: var(--font-mono);
+ font-size: 11px;
+ color: var(--text-muted);
+ background: var(--bg-tertiary);
+ padding: 4px 8px;
+ border-radius: 4px;
+ word-break: break-all;
+}
+
+.tooltip-stats {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: 4px 14px;
+ font-size: 12px;
+}
+
+.tooltip-stat-label {
+ color: var(--text-secondary);
+ font-weight: 500;
+}
+
+.tooltip-stat-value {
+ color: var(--text-primary);
font-weight: 600;
- font-size: 18px;
- text-align: center;
- border-bottom: 2px solid #ffd43b;
- padding-bottom: 8px;
}
-.legend-item {
- display: flex;
- align-items: center;
- margin: 12px 0;
- padding: 10px;
- border-radius: 6px;
- background: #f8f9fa;
- border: 1px solid #e9ecef;
+.tooltip-stat-value.accent {
+ color: var(--accent);
}
-.legend-color {
- width: 28px;
- height: 18px;
- border-radius: 4px;
- margin-right: 16px;
- border: 1px solid rgba(0, 0, 0, 0.1);
- flex-shrink: 0;
+.tooltip-source {
+ margin-top: 10px;
+ padding-top: 10px;
+ border-top: 1px solid var(--border);
}
-.legend-label {
- color: #2e3338;
+.tooltip-source-title {
+ font-size: 11px;
font-weight: 600;
- flex: 1;
+ color: var(--accent);
+ margin-bottom: 6px;
}
-.legend-description {
- color: #5a6c7d;
- font-size: 12px;
- margin-top: 2px;
- font-weight: 400;
+.tooltip-source-code {
+ font-family: var(--font-mono);
+ font-size: 10px;
+ line-height: 1.5;
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 8px;
+ max-height: 140px;
+ overflow-y: auto;
+ white-space: pre-wrap;
+ word-break: break-all;
}
-.chart-container {
- background: #ffffff;
- margin: 0;
- padding: 12px 0;
+.tooltip-source-line {
+ color: var(--text-secondary);
+ padding: 1px 0;
}
-/* Tooltip overflow fixes */
-.python-tooltip {
- max-width: 500px !important;
- word-wrap: break-word !important;
- overflow-wrap: break-word !important;
- box-sizing: border-box !important;
-}
-
-/* Responsive tooltip adjustments */
-@media (max-width: 768px) {
- .python-tooltip {
- max-width: calc(100vw - 40px) !important;
- max-height: calc(100vh - 80px) !important;
- overflow-y: auto !important;
- }
+.tooltip-source-line.current {
+ color: var(--accent);
+ font-weight: 600;
}
-@media (max-width: 480px) {
- .python-tooltip {
- max-width: calc(100vw - 20px) !important;
- font-size: 12px !important;
+.tooltip-hint {
+ margin-top: 10px;
+ padding-top: 8px;
+ border-top: 1px solid var(--border);
+ font-size: 11px;
+ color: var(--text-muted);
+ text-align: center;
+}
+
+/* --------------------------------------------------------------------------
+ Animations
+ -------------------------------------------------------------------------- */
+
+@keyframes fadeIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+@keyframes slideUp {
+ from {
+ opacity: 0;
+ transform: translateY(12px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
}
}
-/* Accessibility: visible focus states */
+/* --------------------------------------------------------------------------
+ Focus States (Accessibility)
+ -------------------------------------------------------------------------- */
+
button:focus-visible,
select:focus-visible,
input:focus-visible {
- outline: 2px solid #ffd43b;
+ outline: 2px solid var(--python-gold);
outline-offset: 2px;
}
-/* Smooth panel transitions */
-.legend-panel,
-.info-panel {
- transition: opacity 0.2s ease, transform 0.2s ease;
+/* --------------------------------------------------------------------------
+ Responsive
+ -------------------------------------------------------------------------- */
+
+@media (max-width: 900px) {
+ .sidebar {
+ position: fixed;
+ left: 0;
+ top: var(--topbar-height);
+ bottom: var(--statusbar-height);
+ z-index: 100;
+ box-shadow: var(--shadow-lg);
+ }
+
+ .sidebar.collapsed {
+ width: var(--sidebar-collapsed);
+ }
+
+ .brand-subtitle {
+ display: none;
+ }
+
+ .search-wrapper {
+ max-width: 220px;
+ }
}
-.legend-panel[style*="block"],
-.info-panel[style*="block"] {
- animation: slideUp 0.2s ease-out;
+@media (max-width: 600px) {
+ .toolbar-btn:not(.theme-toggle) {
+ display: none;
+ }
+
+ .search-wrapper {
+ max-width: 160px;
+ }
+
+ .brand-info {
+ display: none;
+ }
+
+ .stats-grid {
+ grid-template-columns: 1fr;
+ }
}
let originalData = null;
let currentThreadFilter = 'all';
-// Function to resolve string indices to actual strings
+// Heat colors are now defined in CSS variables (--heat-1 through --heat-8)
+// and automatically switch with theme changes - no JS color arrays needed!
+
+// ============================================================================
+// String Resolution
+// ============================================================================
+
function resolveString(index) {
- if (typeof index === 'number' && index >= 0 && index < stringTable.length) {
- return stringTable[index];
- }
- // Fallback for non-indexed strings or invalid indices
- return String(index);
+ if (index === null || index === undefined) {
+ return null;
+ }
+ if (typeof index === 'number' && index >= 0 && index < stringTable.length) {
+ return stringTable[index];
+ }
+ return String(index);
}
-// Function to recursively resolve all string indices in flamegraph data
function resolveStringIndices(node) {
- if (!node) return node;
+ if (!node) return node;
- // Create a copy to avoid mutating the original
- const resolved = { ...node };
+ const resolved = { ...node };
- // Resolve string fields
- if (typeof resolved.name === 'number') {
- resolved.name = resolveString(resolved.name);
- }
- if (typeof resolved.filename === 'number') {
- resolved.filename = resolveString(resolved.filename);
+ if (typeof resolved.name === 'number') {
+ resolved.name = resolveString(resolved.name);
+ }
+ if (typeof resolved.filename === 'number') {
+ resolved.filename = resolveString(resolved.filename);
+ }
+ if (typeof resolved.funcname === 'number') {
+ resolved.funcname = resolveString(resolved.funcname);
+ }
+
+ if (Array.isArray(resolved.source)) {
+ resolved.source = resolved.source.map(index =>
+ typeof index === 'number' ? resolveString(index) : index
+ );
+ }
+
+ if (Array.isArray(resolved.children)) {
+ resolved.children = resolved.children.map(child => resolveStringIndices(child));
+ }
+
+ return resolved;
+}
+
+// ============================================================================
+// Theme & UI Controls
+// ============================================================================
+
+function toggleTheme() {
+ const html = document.documentElement;
+ const current = html.getAttribute('data-theme') || 'light';
+ const next = current === 'light' ? 'dark' : 'light';
+ html.setAttribute('data-theme', next);
+ localStorage.setItem('flamegraph-theme', next);
+
+ // Update theme button icon
+ const btn = document.getElementById('theme-btn');
+ if (btn) {
+ btn.innerHTML = next === 'dark' ? '☼' : '☾'; // sun or moon
+ }
+
+ // Re-render flamegraph with new theme colors
+ if (window.flamegraphData && originalData) {
+ const tooltip = createPythonTooltip(originalData);
+ const chart = createFlamegraph(tooltip, originalData.value);
+ renderFlamegraph(chart, window.flamegraphData);
+ }
+}
+
+function toggleSidebar() {
+ const sidebar = document.getElementById('sidebar');
+ if (sidebar) {
+ const isCollapsing = !sidebar.classList.contains('collapsed');
+
+ if (isCollapsing) {
+ // Save current width before collapsing
+ const currentWidth = sidebar.offsetWidth;
+ sidebar.dataset.expandedWidth = currentWidth;
+ localStorage.setItem('flamegraph-sidebar-width', currentWidth);
+ } else {
+ // Restore width when expanding
+ const savedWidth = sidebar.dataset.expandedWidth || localStorage.getItem('flamegraph-sidebar-width');
+ if (savedWidth) {
+ sidebar.style.width = savedWidth + 'px';
+ }
}
- if (typeof resolved.funcname === 'number') {
- resolved.funcname = resolveString(resolved.funcname);
+
+ sidebar.classList.toggle('collapsed');
+ localStorage.setItem('flamegraph-sidebar', sidebar.classList.contains('collapsed') ? 'collapsed' : 'expanded');
+
+ // Resize chart after sidebar animation
+ setTimeout(() => {
+ resizeChart();
+ }, 300);
+ }
+}
+
+function resizeChart() {
+ if (window.flamegraphChart && window.flamegraphData) {
+ const chartArea = document.querySelector('.chart-area');
+ if (chartArea) {
+ window.flamegraphChart.width(chartArea.clientWidth - 32);
+ d3.select("#chart").datum(window.flamegraphData).call(window.flamegraphChart);
}
+ }
+}
- // Resolve source lines if present
- if (Array.isArray(resolved.source)) {
- resolved.source = resolved.source.map(index =>
- typeof index === 'number' ? resolveString(index) : index
- );
+function toggleSection(sectionId) {
+ const section = document.getElementById(sectionId);
+ if (section) {
+ section.classList.toggle('collapsed');
+ // Save state
+ const collapsedSections = JSON.parse(localStorage.getItem('flamegraph-collapsed-sections') || '{}');
+ collapsedSections[sectionId] = section.classList.contains('collapsed');
+ localStorage.setItem('flamegraph-collapsed-sections', JSON.stringify(collapsedSections));
+ }
+}
+
+function restoreUIState() {
+ // Restore theme
+ const savedTheme = localStorage.getItem('flamegraph-theme');
+ if (savedTheme) {
+ document.documentElement.setAttribute('data-theme', savedTheme);
+ const btn = document.getElementById('theme-btn');
+ if (btn) {
+ btn.innerHTML = savedTheme === 'dark' ? '☼' : '☾';
}
+ }
- // Recursively resolve children
- if (Array.isArray(resolved.children)) {
- resolved.children = resolved.children.map(child => resolveStringIndices(child));
+ // Restore sidebar state
+ const savedSidebar = localStorage.getItem('flamegraph-sidebar');
+ if (savedSidebar === 'collapsed') {
+ const sidebar = document.getElementById('sidebar');
+ if (sidebar) sidebar.classList.add('collapsed');
+ }
+
+ // Restore sidebar width
+ const savedWidth = localStorage.getItem('flamegraph-sidebar-width');
+ if (savedWidth) {
+ const sidebar = document.getElementById('sidebar');
+ if (sidebar) {
+ sidebar.style.width = savedWidth + 'px';
}
+ }
- return resolved;
+ // Restore collapsed sections
+ const collapsedSections = JSON.parse(localStorage.getItem('flamegraph-collapsed-sections') || '{}');
+ for (const [sectionId, isCollapsed] of Object.entries(collapsedSections)) {
+ if (isCollapsed) {
+ const section = document.getElementById(sectionId);
+ if (section) section.classList.add('collapsed');
+ }
+ }
}
-// Python color palette - cold to hot
-const pythonColors = [
- "#fff4bf", // Coldest - light yellow (<1%)
- "#ffec9e", // Cold - yellow (1-3%)
- "#ffe47d", // Cool - golden yellow (3-6%)
- "#ffdc5c", // Medium - golden (6-12%)
- "#ffd43b", // Warm - Python gold (12-18%)
- "#5592cc", // Hot - light blue (18-35%)
- "#4584bb", // Very hot - medium blue (35-60%)
- "#3776ab", // Hottest - Python blue (≥60%)
-];
-
-function ensureLibraryLoaded() {
- if (typeof flamegraph === "undefined") {
- console.error("d3-flame-graph library not loaded");
- document.getElementById("chart").innerHTML =
- '<h2 style="text-align: center; color: #d32f2f;">Error: d3-flame-graph library failed to load</h2>';
- throw new Error("d3-flame-graph library failed to load");
+// ============================================================================
+// Status Bar
+// ============================================================================
+
+function updateStatusBar(nodeData, rootValue) {
+ const funcname = resolveString(nodeData.funcname) || resolveString(nodeData.name) || "--";
+ const filename = resolveString(nodeData.filename) || "";
+ const lineno = nodeData.lineno;
+ const timeMs = (nodeData.value / 1000).toFixed(2);
+ const percent = rootValue > 0 ? ((nodeData.value / rootValue) * 100).toFixed(1) : "0.0";
+
+ const locationEl = document.getElementById('status-location');
+ const funcItem = document.getElementById('status-func-item');
+ const timeItem = document.getElementById('status-time-item');
+ const percentItem = document.getElementById('status-percent-item');
+
+ if (locationEl) locationEl.style.display = filename && filename !== "~" ? 'flex' : 'none';
+ if (funcItem) funcItem.style.display = 'flex';
+ if (timeItem) timeItem.style.display = 'flex';
+ if (percentItem) percentItem.style.display = 'flex';
+
+ const fileEl = document.getElementById('status-file');
+ if (fileEl && filename && filename !== "~") {
+ const basename = filename.split('/').pop();
+ fileEl.textContent = lineno ? `${basename}:${lineno}` : basename;
}
+
+ const funcEl = document.getElementById('status-func');
+ if (funcEl) funcEl.textContent = funcname.length > 40 ? funcname.substring(0, 37) + '...' : funcname;
+
+ const timeEl = document.getElementById('status-time');
+ if (timeEl) timeEl.textContent = `${timeMs} ms`;
+
+ const percentEl = document.getElementById('status-percent');
+ if (percentEl) percentEl.textContent = `${percent}%`;
+}
+
+function clearStatusBar() {
+ const ids = ['status-location', 'status-func-item', 'status-time-item', 'status-percent-item'];
+ ids.forEach(id => {
+ const el = document.getElementById(id);
+ if (el) el.style.display = 'none';
+ });
}
+// ============================================================================
+// Tooltip
+// ============================================================================
+
function createPythonTooltip(data) {
const pythonTooltip = flamegraph.tooltip.defaultFlamegraphTooltip();
+
pythonTooltip.show = function (d, element) {
if (!this._tooltip) {
- this._tooltip = d3
- .select("body")
+ this._tooltip = d3.select("body")
.append("div")
.attr("class", "python-tooltip")
- .style("position", "absolute")
- .style("padding", "20px")
- .style("background", "white")
- .style("color", "#2e3338")
- .style("border-radius", "8px")
- .style("font-size", "14px")
- .style("border", "1px solid #e9ecef")
- .style("box-shadow", "0 8px 30px rgba(0, 0, 0, 0.15)")
- .style("z-index", "1000")
- .style("pointer-events", "none")
- .style("font-weight", "400")
- .style("line-height", "1.5")
- .style("max-width", "500px")
- .style("word-wrap", "break-word")
- .style("overflow-wrap", "break-word")
- .style("font-family", "'Source Sans Pro', sans-serif")
.style("opacity", 0);
}
const childCount = d.children ? d.children.length : 0;
const source = d.data.source;
- // Create source code section if available
+ const funcname = resolveString(d.data.funcname) || resolveString(d.data.name);
+ const filename = resolveString(d.data.filename) || "";
+ const isSpecialFrame = filename === "~";
+
+ // Build source section
let sourceSection = "";
if (source && Array.isArray(source) && source.length > 0) {
const sourceLines = source
- .map(
- (line) =>
- `<div style="font-family: 'SF Mono', 'Monaco', 'Consolas', ` +
- `monospace; font-size: 12px; color: ${
- line.startsWith("→") ? "#3776ab" : "#5a6c7d"
- }; white-space: pre-wrap; word-break: break-all; overflow-wrap: break-word; line-height: 1.4; padding: 2px 0;">${line
- .replace(/&/g, "&")
- .replace(/</g, "<")
- .replace(/>/g, ">")}</div>`,
- )
+ .map((line) => {
+ const isCurrent = line.startsWith("→");
+ const escaped = line.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
+ return `<div class="tooltip-source-line${isCurrent ? ' current' : ''}">${escaped}</div>`;
+ })
.join("");
sourceSection = `
- <div style="margin-top: 16px; padding-top: 12px;
- border-top: 1px solid #e9ecef;">
- <div style="color: #3776ab; font-size: 13px;
- margin-bottom: 8px; font-weight: 600;">
- Source Code:
- </div>
- <div style="background: #f8f9fa; border: 1px solid #e9ecef;
- border-radius: 6px; padding: 12px; max-height: 150px;
- overflow-y: auto; overflow-x: hidden;">
- ${sourceLines}
- </div>
- </div>`;
- } else if (source) {
- // Show debug info if source exists but isn't an array
- sourceSection = `
- <div style="margin-top: 16px; padding-top: 12px;
- border-top: 1px solid #e9ecef;">
- <div style="color: #d32f2f; font-size: 13px;
- margin-bottom: 8px; font-weight: 600;">
- [Debug] - Source data type: ${typeof source}
- </div>
- <div style="background: #f8f9fa; border: 1px solid #e9ecef;
- border-radius: 6px; padding: 12px; max-height: 150px;
- overflow-y: auto; overflow-x: hidden; font-family: monospace; font-size: 11px; word-break: break-all; overflow-wrap: break-word;">
- ${JSON.stringify(source, null, 2)}
- </div>
+ <div class="tooltip-source">
+ <div class="tooltip-source-title">Source Code:</div>
+ <div class="tooltip-source-code">${sourceLines}</div>
</div>`;
}
- // Resolve strings for display
- const funcname = resolveString(d.data.funcname) || resolveString(d.data.name);
- const filename = resolveString(d.data.filename) || "";
-
- // Don't show file location for special frames like <GC> and <native>
- const isSpecialFrame = filename === "~";
const fileLocationHTML = isSpecialFrame ? "" : `
- <div style="color: #5a6c7d; font-size: 13px; margin-bottom: 12px;
- font-family: monospace; background: #f8f9fa;
- padding: 4px 8px; border-radius: 4px; word-break: break-all; overflow-wrap: break-word;">
- ${filename}${d.data.lineno ? ":" + d.data.lineno : ""}
- </div>`;
+ <div class="tooltip-location">${filename}${d.data.lineno ? ":" + d.data.lineno : ""}</div>`;
const tooltipHTML = `
- <div>
- <div style="color: #3776ab; font-weight: 600; font-size: 16px;
- margin-bottom: 8px; line-height: 1.3; word-break: break-word; overflow-wrap: break-word;">
- ${funcname}
- </div>
+ <div class="tooltip-header">
+ <div class="tooltip-title">${funcname}</div>
${fileLocationHTML}
- <div style="display: grid; grid-template-columns: auto 1fr;
- gap: 8px 16px; font-size: 14px;">
- <span style="color: #5a6c7d; font-weight: 500;">Execution Time:</span>
- <strong style="color: #2e3338;">${timeMs} ms</strong>
-
- <span style="color: #5a6c7d; font-weight: 500;">Percentage:</span>
- <strong style="color: #3776ab;">${percentage}%</strong>
-
- ${calls > 0 ? `
- <span style="color: #5a6c7d; font-weight: 500;">Function Calls:</span>
- <strong style="color: #2e3338;">${calls.toLocaleString()}</strong>
- ` : ''}
-
- ${childCount > 0 ? `
- <span style="color: #5a6c7d; font-weight: 500;">Child Functions:</span>
- <strong style="color: #2e3338;">${childCount}</strong>
- ` : ''}
- </div>
- ${sourceSection}
- <div style="margin-top: 16px; padding-top: 12px;
- border-top: 1px solid #e9ecef; font-size: 13px;
- color: #5a6c7d; text-align: center;">
- ${childCount > 0 ?
- "Click to focus on this function" :
- "Leaf function - no children"}
- </div>
+ </div>
+ <div class="tooltip-stats">
+ <span class="tooltip-stat-label">Execution Time:</span>
+ <span class="tooltip-stat-value">${timeMs} ms</span>
+
+ <span class="tooltip-stat-label">Percentage:</span>
+ <span class="tooltip-stat-value accent">${percentage}%</span>
+
+ ${calls > 0 ? `
+ <span class="tooltip-stat-label">Function Calls:</span>
+ <span class="tooltip-stat-value">${calls.toLocaleString()}</span>
+ ` : ''}
+
+ ${childCount > 0 ? `
+ <span class="tooltip-stat-label">Child Functions:</span>
+ <span class="tooltip-stat-value">${childCount}</span>
+ ` : ''}
+ </div>
+ ${sourceSection}
+ <div class="tooltip-hint">
+ ${childCount > 0 ? "Click to zoom into this function" : "Leaf function - no children"}
</div>
`;
- // Get mouse position
+ // Position tooltip
const event = d3.event || window.event;
const mouseX = event.pageX || event.clientX;
const mouseY = event.pageY || event.clientY;
+ const padding = 12;
- // Calculate tooltip dimensions (default to 320px width if not rendered yet)
- let tooltipWidth = 320;
- let tooltipHeight = 200;
- if (this._tooltip && this._tooltip.node()) {
- const node = this._tooltip
- .style("opacity", 0)
- .style("display", "block")
- .node();
- tooltipWidth = node.offsetWidth || 320;
- tooltipHeight = node.offsetHeight || 200;
- this._tooltip.style("display", null);
- }
+ this._tooltip.html(tooltipHTML);
- // Calculate horizontal position: if overflow, show to the left of cursor
- const padding = 10;
- const rightEdge = mouseX + padding + tooltipWidth;
- const viewportWidth = window.innerWidth;
- let left;
- if (rightEdge > viewportWidth) {
+ // Measure tooltip
+ const node = this._tooltip.style("display", "block").style("opacity", 0).node();
+ const tooltipWidth = node.offsetWidth || 320;
+ const tooltipHeight = node.offsetHeight || 200;
+
+ // Calculate position
+ let left = mouseX + padding;
+ let top = mouseY + padding;
+
+ if (left + tooltipWidth > window.innerWidth) {
left = mouseX - tooltipWidth - padding;
- if (left < 0) left = padding; // prevent off left edge
- } else {
- left = mouseX + padding;
+ if (left < 0) left = padding;
}
- // Calculate vertical position: if overflow, show above cursor
- const bottomEdge = mouseY + padding + tooltipHeight;
- const viewportHeight = window.innerHeight;
- let top;
- if (bottomEdge > viewportHeight) {
+ if (top + tooltipHeight > window.innerHeight) {
top = mouseY - tooltipHeight - padding;
- if (top < 0) top = padding; // prevent off top edge
- } else {
- top = mouseY + padding;
+ if (top < 0) top = padding;
}
this._tooltip
- .html(tooltipHTML)
.style("left", left + "px")
.style("top", top + "px")
.transition()
- .duration(200)
+ .duration(150)
.style("opacity", 1);
+
+ // Update status bar
+ updateStatusBar(d.data, data.value);
};
- // Override the hide method
pythonTooltip.hide = function () {
if (this._tooltip) {
- this._tooltip.transition().duration(200).style("opacity", 0);
+ this._tooltip.transition().duration(150).style("opacity", 0);
}
+ clearStatusBar();
};
+
return pythonTooltip;
}
+// ============================================================================
+// Flamegraph Creation
+// ============================================================================
+
+function ensureLibraryLoaded() {
+ if (typeof flamegraph === "undefined") {
+ console.error("d3-flame-graph library not loaded");
+ document.getElementById("chart").innerHTML =
+ '<div style="padding: 40px; text-align: center; color: var(--text-muted);">Error: d3-flame-graph library failed to load</div>';
+ throw new Error("d3-flame-graph library failed to load");
+ }
+}
+
+const HEAT_THRESHOLDS = [
+ [0.6, 8],
+ [0.35, 7],
+ [0.18, 6],
+ [0.12, 5],
+ [0.06, 4],
+ [0.03, 3],
+ [0.01, 2],
+];
+
+function getHeatLevel(percentage) {
+ for (const [threshold, level] of HEAT_THRESHOLDS) {
+ if (percentage >= threshold) return level;
+ }
+ return 1;
+}
+
+function getHeatColors() {
+ const style = getComputedStyle(document.documentElement);
+ const colors = {};
+ for (let i = 1; i <= 8; i++) {
+ colors[i] = style.getPropertyValue(`--heat-${i}`).trim();
+ }
+ return colors;
+}
+
function createFlamegraph(tooltip, rootValue) {
+ const chartArea = document.querySelector('.chart-area');
+ const width = chartArea ? chartArea.clientWidth - 32 : window.innerWidth - 320;
+ const heatColors = getHeatColors();
+
let chart = flamegraph()
- .width(window.innerWidth - 80)
+ .width(width)
.cellHeight(20)
.transitionDuration(300)
.minFrameSize(1)
.inverted(true)
.setColorMapper(function (d) {
const percentage = d.data.value / rootValue;
- let colorIndex;
- if (percentage >= 0.6) colorIndex = 7;
- else if (percentage >= 0.35) colorIndex = 6;
- else if (percentage >= 0.18) colorIndex = 5;
- else if (percentage >= 0.12) colorIndex = 4;
- else if (percentage >= 0.06) colorIndex = 3;
- else if (percentage >= 0.03) colorIndex = 2;
- else if (percentage >= 0.01) colorIndex = 1;
- else colorIndex = 0; // <1%
- return pythonColors[colorIndex];
+ const level = getHeatLevel(percentage);
+ return heatColors[level];
});
+
return chart;
}
function renderFlamegraph(chart, data) {
d3.select("#chart").datum(data).call(chart);
- window.flamegraphChart = chart; // for controls
- window.flamegraphData = data; // for resize/search
+ window.flamegraphChart = chart;
+ window.flamegraphData = data;
populateStats(data);
}
-function attachPanelControls() {
- const infoBtn = document.getElementById("show-info-btn");
- const infoPanel = document.getElementById("info-panel");
- const closeBtn = document.getElementById("close-info-btn");
- if (infoBtn && infoPanel) {
- infoBtn.addEventListener("click", function () {
- const isOpen = infoPanel.style.display === "block";
- infoPanel.style.display = isOpen ? "none" : "block";
- });
- }
- if (closeBtn && infoPanel) {
- closeBtn.addEventListener("click", function () {
- infoPanel.style.display = "none";
- });
- }
-}
+// ============================================================================
+// Search
+// ============================================================================
function updateSearchHighlight(searchTerm, searchInput) {
d3.selectAll("#chart rect")
- .style("stroke", null)
- .style("stroke-width", null)
- .style("opacity", null);
+ .classed("search-match", false)
+ .classed("search-dim", false);
+
+ // Clear active state from all hotspots
+ document.querySelectorAll('.hotspot').forEach(h => h.classList.remove('active'));
+
if (searchTerm && searchTerm.length > 0) {
- d3.selectAll("#chart rect").style("opacity", 0.3);
let matchCount = 0;
+
d3.selectAll("#chart rect").each(function (d) {
if (d && d.data) {
const name = resolveString(d.data.name) || "";
const funcname = resolveString(d.data.funcname) || "";
const filename = resolveString(d.data.filename) || "";
+ const lineno = d.data.lineno;
const term = searchTerm.toLowerCase();
- const matches =
- name.toLowerCase().includes(term) ||
- funcname.toLowerCase().includes(term) ||
- filename.toLowerCase().includes(term);
+
+ // Check if search term looks like file:line pattern
+ const fileLineMatch = term.match(/^(.+):(\d+)$/);
+ let matches = false;
+
+ if (fileLineMatch) {
+ // Exact file:line matching
+ const searchFile = fileLineMatch[1];
+ const searchLine = parseInt(fileLineMatch[2], 10);
+ const basename = filename.split('/').pop().toLowerCase();
+ matches = basename.includes(searchFile) && lineno === searchLine;
+ } else {
+ // Regular substring search
+ matches =
+ name.toLowerCase().includes(term) ||
+ funcname.toLowerCase().includes(term) ||
+ filename.toLowerCase().includes(term);
+ }
+
if (matches) {
matchCount++;
- d3.select(this)
- .style("opacity", 1)
- .style("stroke", "#ff6b35")
- .style("stroke-width", "2px")
- .style("stroke-dasharray", "3,3");
+ d3.select(this).classed("search-match", true);
+ } else {
+ d3.select(this).classed("search-dim", true);
}
}
});
+
if (searchInput) {
- if (matchCount > 0) {
- searchInput.style.borderColor = "rgba(40, 167, 69, 0.8)";
- searchInput.style.boxShadow = "0 6px 20px rgba(40, 167, 69, 0.2)";
- } else {
- searchInput.style.borderColor = "rgba(220, 53, 69, 0.8)";
- searchInput.style.boxShadow = "0 6px 20px rgba(220, 53, 69, 0.2)";
- }
+ searchInput.classList.remove("has-matches", "no-matches");
+ searchInput.classList.add(matchCount > 0 ? "has-matches" : "no-matches");
}
+
+ // Mark matching hotspot as active
+ document.querySelectorAll('.hotspot').forEach(h => {
+ if (h.dataset.searchterm && h.dataset.searchterm.toLowerCase() === searchTerm.toLowerCase()) {
+ h.classList.add('active');
+ }
+ });
} else if (searchInput) {
- searchInput.style.borderColor = "rgba(255, 255, 255, 0.2)";
- searchInput.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.1)";
+ searchInput.classList.remove("has-matches", "no-matches");
+ }
+}
+
+function searchForHotspot(funcname) {
+ const searchInput = document.getElementById('search-input');
+ const searchWrapper = document.querySelector('.search-wrapper');
+ if (searchInput) {
+ // Toggle: if already searching for this term, clear it
+ if (searchInput.value.trim() === funcname) {
+ clearSearch();
+ } else {
+ searchInput.value = funcname;
+ if (searchWrapper) {
+ searchWrapper.classList.add('has-value');
+ }
+ performSearch();
+ }
}
}
function initSearchHandlers() {
const searchInput = document.getElementById("search-input");
+ const searchWrapper = document.querySelector(".search-wrapper");
if (!searchInput) return;
+
let searchTimeout;
function performSearch() {
const term = searchInput.value.trim();
updateSearchHighlight(term, searchInput);
+ // Toggle has-value class for clear button visibility
+ if (searchWrapper) {
+ searchWrapper.classList.toggle("has-value", term.length > 0);
+ }
}
+
searchInput.addEventListener("input", function () {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(performSearch, 150);
});
+
window.performSearch = performSearch;
}
-function handleResize(chart, data) {
- window.addEventListener("resize", function () {
- if (chart && data) {
- const newWidth = window.innerWidth - 80;
- chart.width(newWidth);
- d3.select("#chart").datum(data).call(chart);
+function clearSearch() {
+ const searchInput = document.getElementById("search-input");
+ const searchWrapper = document.querySelector(".search-wrapper");
+ if (searchInput) {
+ searchInput.value = "";
+ searchInput.classList.remove("has-matches", "no-matches");
+ if (searchWrapper) {
+ searchWrapper.classList.remove("has-value");
}
+ // Clear highlights
+ d3.selectAll("#chart rect")
+ .classed("search-match", false)
+ .classed("search-dim", false);
+ // Clear active hotspot
+ document.querySelectorAll('.hotspot').forEach(h => h.classList.remove('active'));
+ }
+}
+
+// ============================================================================
+// Resize Handler
+// ============================================================================
+
+function handleResize() {
+ let resizeTimeout;
+ window.addEventListener("resize", function () {
+ clearTimeout(resizeTimeout);
+ resizeTimeout = setTimeout(resizeChart, 100);
});
}
-function initFlamegraph() {
- ensureLibraryLoaded();
+function initSidebarResize() {
+ const sidebar = document.getElementById('sidebar');
+ const resizeHandle = document.getElementById('sidebar-resize-handle');
+ if (!sidebar || !resizeHandle) return;
+
+ let isResizing = false;
+ let startX = 0;
+ let startWidth = 0;
+ const minWidth = 200;
+ const maxWidth = 600;
+
+ resizeHandle.addEventListener('mousedown', function(e) {
+ isResizing = true;
+ startX = e.clientX;
+ startWidth = sidebar.offsetWidth;
+ resizeHandle.classList.add('resizing');
+ document.body.classList.add('resizing-sidebar');
+ e.preventDefault();
+ });
- // Extract string table if present and resolve string indices
- let processedData = EMBEDDED_DATA;
- if (EMBEDDED_DATA.strings) {
- stringTable = EMBEDDED_DATA.strings;
- processedData = resolveStringIndices(EMBEDDED_DATA);
- }
+ document.addEventListener('mousemove', function(e) {
+ if (!isResizing) return;
- // Store original data for filtering
- originalData = processedData;
+ const deltaX = e.clientX - startX;
+ const newWidth = Math.min(Math.max(startWidth + deltaX, minWidth), maxWidth);
+ sidebar.style.width = newWidth + 'px';
+ e.preventDefault();
+ });
- // Initialize thread filter dropdown
- initThreadFilter(processedData);
+ document.addEventListener('mouseup', function() {
+ if (isResizing) {
+ isResizing = false;
+ resizeHandle.classList.remove('resizing');
+ document.body.classList.remove('resizing-sidebar');
- const tooltip = createPythonTooltip(processedData);
- const chart = createFlamegraph(tooltip, processedData.value);
- renderFlamegraph(chart, processedData);
- attachPanelControls();
- initSearchHandlers();
- handleResize(chart, processedData);
-}
+ // Save the new width
+ const width = sidebar.offsetWidth;
+ localStorage.setItem('flamegraph-sidebar-width', width);
-if (document.readyState === "loading") {
- document.addEventListener("DOMContentLoaded", initFlamegraph);
-} else {
- initFlamegraph();
+ // Resize chart after sidebar resize
+ setTimeout(() => {
+ resizeChart();
+ }, 10);
+ }
+ });
}
+// ============================================================================
+// Thread Stats
+// ============================================================================
+
// Mode constants (must match constants.py)
const PROFILING_MODE_WALL = 0;
const PROFILING_MODE_CPU = 1;
const PROFILING_MODE_ALL = 3;
function populateThreadStats(data, selectedThreadId = null) {
- // Check if thread statistics are available
const stats = data?.stats;
if (!stats || !stats.thread_stats) {
- return; // No thread stats available
+ return;
}
const mode = stats.mode !== undefined ? stats.mode : PROFILING_MODE_WALL;
let threadStats;
- // If a specific thread is selected, use per-thread stats
if (selectedThreadId !== null && stats.per_thread_stats && stats.per_thread_stats[selectedThreadId]) {
threadStats = stats.per_thread_stats[selectedThreadId];
} else {
threadStats = stats.thread_stats;
}
- // Validate threadStats object
- if (!threadStats || typeof threadStats.total !== 'number') {
- return; // Invalid thread stats
+ if (!threadStats || typeof threadStats.total !== 'number' || threadStats.total <= 0) {
+ return;
}
- const bar = document.getElementById('thread-stats-bar');
- if (!bar) {
- return; // DOM element not found
+ const section = document.getElementById('thread-stats-bar');
+ if (!section) {
+ return;
}
- // Show the bar if we have valid thread stats
- if (threadStats.total > 0) {
- bar.style.display = 'flex';
+ section.style.display = 'block';
- // Hide/show GIL stats items in GIL mode
- const gilHeldStat = document.getElementById('gil-held-stat');
- const gilReleasedStat = document.getElementById('gil-released-stat');
- const gilWaitingStat = document.getElementById('gil-waiting-stat');
- const separators = bar.querySelectorAll('.thread-stat-separator');
+ const gilHeldStat = document.getElementById('gil-held-stat');
+ const gilReleasedStat = document.getElementById('gil-released-stat');
+ const gilWaitingStat = document.getElementById('gil-waiting-stat');
- if (mode === PROFILING_MODE_GIL) {
- // In GIL mode, hide GIL-related stats
- if (gilHeldStat) gilHeldStat.style.display = 'none';
- if (gilReleasedStat) gilReleasedStat.style.display = 'none';
- if (gilWaitingStat) gilWaitingStat.style.display = 'none';
- separators.forEach((sep, i) => {
- if (i < 3) sep.style.display = 'none';
- });
- } else {
- // Show all stats in other modes
- if (gilHeldStat) gilHeldStat.style.display = 'inline-flex';
- if (gilReleasedStat) gilReleasedStat.style.display = 'inline-flex';
- if (gilWaitingStat) gilWaitingStat.style.display = 'inline-flex';
- separators.forEach(sep => sep.style.display = 'inline');
-
- // GIL Held
- const gilHeldPct = threadStats.has_gil_pct || 0;
- const gilHeldPctElem = document.getElementById('gil-held-pct');
- if (gilHeldPctElem) gilHeldPctElem.textContent = `${gilHeldPct.toFixed(2)}%`;
-
- // GIL Released (threads running without GIL)
- const gilReleasedPct = threadStats.on_cpu_pct || 0;
- const gilReleasedPctElem = document.getElementById('gil-released-pct');
- if (gilReleasedPctElem) gilReleasedPctElem.textContent = `${gilReleasedPct.toFixed(2)}%`;
-
- // Waiting for GIL
- const gilWaitingPct = threadStats.gil_requested_pct || 0;
- const gilWaitingPctElem = document.getElementById('gil-waiting-pct');
- if (gilWaitingPctElem) gilWaitingPctElem.textContent = `${gilWaitingPct.toFixed(2)}%`;
- }
+ if (mode === PROFILING_MODE_GIL) {
+ // In GIL mode, hide GIL-related stats
+ if (gilHeldStat) gilHeldStat.style.display = 'none';
+ if (gilReleasedStat) gilReleasedStat.style.display = 'none';
+ if (gilWaitingStat) gilWaitingStat.style.display = 'none';
+ } else {
+ // Show all stats
+ if (gilHeldStat) gilHeldStat.style.display = 'block';
+ if (gilReleasedStat) gilReleasedStat.style.display = 'block';
+ if (gilWaitingStat) gilWaitingStat.style.display = 'block';
- // Garbage Collection (always show)
- const gcPct = threadStats.gc_pct || 0;
- const gcPctElem = document.getElementById('gc-pct');
- if (gcPctElem) gcPctElem.textContent = `${gcPct.toFixed(2)}%`;
+ const gilHeldPctElem = document.getElementById('gil-held-pct');
+ if (gilHeldPctElem) gilHeldPctElem.textContent = `${(threadStats.has_gil_pct || 0).toFixed(1)}%`;
+
+ const gilReleasedPctElem = document.getElementById('gil-released-pct');
+ if (gilReleasedPctElem) gilReleasedPctElem.textContent = `${(threadStats.on_cpu_pct || 0).toFixed(1)}%`;
+
+ const gilWaitingPctElem = document.getElementById('gil-waiting-pct');
+ if (gilWaitingPctElem) gilWaitingPctElem.textContent = `${(threadStats.gil_requested_pct || 0).toFixed(1)}%`;
}
+
+ const gcPctElem = document.getElementById('gc-pct');
+ if (gcPctElem) gcPctElem.textContent = `${(threadStats.gc_pct || 0).toFixed(1)}%`;
}
+// ============================================================================
+// Profile Summary Stats
+// ============================================================================
+
+function formatNumber(num) {
+ if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
+ if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
+ return num.toLocaleString();
+}
+
+function formatDuration(seconds) {
+ if (seconds >= 3600) {
+ const h = Math.floor(seconds / 3600);
+ const m = Math.floor((seconds % 3600) / 60);
+ return `${h}h ${m}m`;
+ }
+ if (seconds >= 60) {
+ const m = Math.floor(seconds / 60);
+ const s = Math.floor(seconds % 60);
+ return `${m}m ${s}s`;
+ }
+ return seconds.toFixed(2) + 's';
+}
+
+function populateProfileSummary(data) {
+ const stats = data.stats || {};
+ const totalSamples = stats.total_samples || data.value || 0;
+ const duration = stats.duration_sec || 0;
+ const sampleRate = stats.sample_rate || (duration > 0 ? totalSamples / duration : 0);
+ const errorRate = stats.error_rate || 0;
+ const missedSamples= stats.missed_samples || 0;
+
+ const samplesEl = document.getElementById('stat-total-samples');
+ if (samplesEl) samplesEl.textContent = formatNumber(totalSamples);
+
+ const durationEl = document.getElementById('stat-duration');
+ if (durationEl) durationEl.textContent = duration > 0 ? formatDuration(duration) : '--';
+
+ const rateEl = document.getElementById('stat-sample-rate');
+ if (rateEl) rateEl.textContent = sampleRate > 0 ? formatNumber(Math.round(sampleRate)) : '--';
+
+ // Count unique functions
+ let functionCount = 0;
+ function countFunctions(node) {
+ if (!node) return;
+ functionCount++;
+ if (node.children) node.children.forEach(countFunctions);
+ }
+ countFunctions(data);
+
+ const functionsEl = document.getElementById('stat-functions');
+ if (functionsEl) functionsEl.textContent = formatNumber(functionCount);
+
+ // Efficiency bar
+ if (errorRate !== undefined && errorRate !== null) {
+ const efficiency = Math.max(0, Math.min(100, (100 - errorRate)));
+
+ const efficiencySection = document.getElementById('efficiency-section');
+ if (efficiencySection) efficiencySection.style.display = 'block';
+
+ const efficiencyValue = document.getElementById('stat-efficiency');
+ if (efficiencyValue) efficiencyValue.textContent = efficiency.toFixed(1) + '%';
+
+ const efficiencyFill = document.getElementById('efficiency-fill');
+ if (efficiencyFill) efficiencyFill.style.width = efficiency + '%';
+ }
+ // MissedSamples bar
+ if (missedSamples !== undefined && missedSamples !== null) {
+ const sampleEfficiency = Math.max(0, missedSamples);
+
+ const efficiencySection = document.getElementById('efficiency-section');
+ if (efficiencySection) efficiencySection.style.display = 'block';
+
+ const sampleEfficiencyValue = document.getElementById('stat-missed-samples');
+ if (sampleEfficiencyValue) sampleEfficiencyValue.textContent = sampleEfficiency.toFixed(1) + '%';
+
+ const sampleEfficiencyFill = document.getElementById('missed-samples-fill');
+ if (sampleEfficiencyFill) sampleEfficiencyFill.style.width = sampleEfficiency + '%';
+ }
+}
+
+// ============================================================================
+// Hotspot Stats
+// ============================================================================
+
function populateStats(data) {
const totalSamples = data.value || 0;
+ // Populate profile summary
+ populateProfileSummary(data);
+
// Populate thread statistics if available
populateThreadStats(data);
- // Collect all functions with their metrics, aggregated by function name
const functionMap = new Map();
function collectFunctions(node) {
if (!node) return;
- let filename = typeof node.filename === 'number' ? resolveString(node.filename) : node.filename;
- let funcname = typeof node.funcname === 'number' ? resolveString(node.funcname) : node.funcname;
+ let filename = resolveString(node.filename);
+ let funcname = resolveString(node.funcname);
if (!filename || !funcname) {
- const nameStr = typeof node.name === 'number' ? resolveString(node.name) : node.name;
+ const nameStr = resolveString(node.name);
if (nameStr?.includes('(')) {
const match = nameStr.match(/^(.+?)\s*\((.+?):(\d+)\)$/);
if (match) {
funcname = funcname || 'unknown';
if (filename !== 'unknown' && funcname !== 'unknown' && node.value > 0) {
- // Calculate direct samples (this node's value minus children's values)
let childrenValue = 0;
if (node.children) {
childrenValue = node.children.reduce((sum, child) => sum + child.value, 0);
}
const directSamples = Math.max(0, node.value - childrenValue);
- // Use file:line:funcname as key to ensure uniqueness
const funcKey = `${filename}:${node.lineno || '?'}:${funcname}`;
if (functionMap.has(funcKey)) {
const existing = functionMap.get(funcKey);
existing.directSamples += directSamples;
existing.directPercent = (existing.directSamples / totalSamples) * 100;
- // Keep the most representative file/line (the one with more samples)
if (directSamples > existing.maxSingleSamples) {
existing.filename = filename;
existing.lineno = node.lineno || '?';
collectFunctions(data);
- // Convert map to array and get top 3 hotspots
const hotSpots = Array.from(functionMap.values())
- .filter(f => f.directPercent > 0.5) // At least 0.5% to be significant
+ .filter(f => f.directPercent > 0.5)
.sort((a, b) => b.directPercent - a.directPercent)
.slice(0, 3);
- // Populate the 3 cards
+ // Populate and animate hotspot cards
for (let i = 0; i < 3; i++) {
const num = i + 1;
- if (i < hotSpots.length && hotSpots[i]) {
- const hotspot = hotSpots[i];
- const filename = hotspot.filename || 'unknown';
- const lineno = hotspot.lineno ?? '?';
- let funcDisplay = hotspot.funcname || 'unknown';
- if (funcDisplay.length > 35) {
- funcDisplay = funcDisplay.substring(0, 32) + '...';
- }
+ const card = document.getElementById(`hotspot-${num}`);
+ const funcEl = document.getElementById(`hotspot-func-${num}`);
+ const fileEl = document.getElementById(`hotspot-file-${num}`);
+ const percentEl = document.getElementById(`hotspot-percent-${num}`);
+ const samplesEl = document.getElementById(`hotspot-samples-${num}`);
- // Don't show file:line for special frames like <GC> and <native>
+ if (i < hotSpots.length && hotSpots[i]) {
+ const h = hotSpots[i];
+ const filename = h.filename || 'unknown';
+ const lineno = h.lineno ?? '?';
const isSpecialFrame = filename === '~' && (lineno === 0 || lineno === '?');
- let fileDisplay;
- if (isSpecialFrame) {
- fileDisplay = '--';
- } else {
- const basename = filename !== 'unknown' ? filename.split('/').pop() : 'unknown';
- fileDisplay = `${basename}:${lineno}`;
- }
- document.getElementById(`hotspot-file-${num}`).textContent = fileDisplay;
- document.getElementById(`hotspot-func-${num}`).textContent = funcDisplay;
- document.getElementById(`hotspot-detail-${num}`).textContent = `${hotspot.directPercent.toFixed(1)}% samples (${hotspot.directSamples.toLocaleString()})`;
+ let funcDisplay = h.funcname || 'unknown';
+ if (funcDisplay.length > 28) funcDisplay = funcDisplay.substring(0, 25) + '...';
+
+ if (funcEl) funcEl.textContent = funcDisplay;
+ if (fileEl) {
+ if (isSpecialFrame) {
+ fileEl.textContent = '--';
+ } else {
+ const basename = filename !== 'unknown' ? filename.split('/').pop() : 'unknown';
+ fileEl.textContent = `${basename}:${lineno}`;
+ }
+ }
+ if (percentEl) percentEl.textContent = `${h.directPercent.toFixed(1)}%`;
+ if (samplesEl) samplesEl.textContent = ` (${h.directSamples.toLocaleString()})`;
} else {
- document.getElementById(`hotspot-file-${num}`).textContent = '--';
- document.getElementById(`hotspot-func-${num}`).textContent = '--';
- document.getElementById(`hotspot-detail-${num}`).textContent = '--';
+ if (funcEl) funcEl.textContent = '--';
+ if (fileEl) fileEl.textContent = '--';
+ if (percentEl) percentEl.textContent = '--';
+ if (samplesEl) samplesEl.textContent = '';
}
- }
-}
-
-// Control functions
-function resetZoom() {
- if (window.flamegraphChart) {
- window.flamegraphChart.resetZoom();
- }
-}
-function exportSVG() {
- const svgElement = document.querySelector("#chart svg");
- if (svgElement) {
- const serializer = new XMLSerializer();
- const svgString = serializer.serializeToString(svgElement);
- const blob = new Blob([svgString], { type: "image/svg+xml" });
- const url = URL.createObjectURL(blob);
- const a = document.createElement("a");
- a.href = url;
- a.download = "python-performance-flamegraph.svg";
- a.click();
- URL.revokeObjectURL(url);
- }
-}
-
-function toggleLegend() {
- const legendPanel = document.getElementById("legend-panel");
- const isHidden =
- legendPanel.style.display === "none" || legendPanel.style.display === "";
- legendPanel.style.display = isHidden ? "block" : "none";
-}
+ // Add click handler and animate entrance
+ if (card) {
+ if (i < hotSpots.length && hotSpots[i]) {
+ const h = hotSpots[i];
+ const basename = h.filename !== 'unknown' ? h.filename.split('/').pop() : '';
+ const searchTerm = basename && h.lineno !== '?' ? `${basename}:${h.lineno}` : h.funcname;
+ card.dataset.searchterm = searchTerm;
+ card.onclick = () => searchForHotspot(searchTerm);
+ card.style.cursor = 'pointer';
+ } else {
+ card.onclick = null;
+ delete card.dataset.searchterm;
+ card.style.cursor = 'default';
+ }
-function clearSearch() {
- const searchInput = document.getElementById("search-input");
- if (searchInput) {
- searchInput.value = "";
- if (window.flamegraphChart) {
- window.flamegraphChart.clear();
+ setTimeout(() => {
+ card.classList.add('visible');
+ }, 100 + i * 80);
}
}
}
+// ============================================================================
+// Thread Filter
+// ============================================================================
+
function initThreadFilter(data) {
const threadFilter = document.getElementById('thread-filter');
- const threadWrapper = document.querySelector('.thread-filter-wrapper');
+ const threadSection = document.getElementById('thread-section');
- if (!threadFilter || !data.threads) {
- return;
- }
+ if (!threadFilter || !data.threads) return;
- // Clear existing options except "All Threads"
threadFilter.innerHTML = '<option value="all">All Threads</option>';
- // Add thread options
const threads = data.threads || [];
threads.forEach(threadId => {
const option = document.createElement('option');
threadFilter.appendChild(option);
});
- // Show filter if more than one thread
- if (threads.length > 1 && threadWrapper) {
- threadWrapper.style.display = 'inline-flex';
+ if (threads.length > 1 && threadSection) {
+ threadSection.style.display = 'block';
}
}
let selectedThreadId = null;
if (selectedThread === 'all') {
- // Show all data
filteredData = originalData;
} else {
- // Filter data by thread
- selectedThreadId = parseInt(selectedThread);
+ selectedThreadId = parseInt(selectedThread, 10);
filteredData = filterDataByThread(originalData, selectedThreadId);
if (filteredData.strings) {
}
}
- // Re-render flamegraph with filtered data
const tooltip = createPythonTooltip(filteredData);
const chart = createFlamegraph(tooltip, filteredData.value);
renderFlamegraph(chart, filteredData);
- // Update thread stats to show per-thread or aggregate stats
populateThreadStats(originalData, selectedThreadId);
}
return null;
}
- const filteredNode = {
- ...node,
- children: []
- };
+ const filteredNode = { ...node, children: [] };
if (node.children && Array.isArray(node.children)) {
filteredNode.children = node.children
return filteredNode;
}
- const filteredRoot = {
- ...data,
- children: []
- };
-
- if (data.children && Array.isArray(data.children)) {
- filteredRoot.children = data.children
- .map(child => filterNode(child))
- .filter(child => child !== null);
- }
-
function recalculateValue(node) {
if (!node.children || node.children.length === 0) {
return node.value || 0;
return node.value;
}
- recalculateValue(filteredRoot);
+ const filteredRoot = { ...data, children: [] };
+ if (data.children && Array.isArray(data.children)) {
+ filteredRoot.children = data.children
+ .map(child => filterNode(child))
+ .filter(child => child !== null);
+ }
+
+ recalculateValue(filteredRoot);
return filteredRoot;
}
+// ============================================================================
+// Control Functions
+// ============================================================================
+
+function resetZoom() {
+ if (window.flamegraphChart) {
+ window.flamegraphChart.resetZoom();
+ }
+}
+
+function exportSVG() {
+ const svgElement = document.querySelector("#chart svg");
+ if (!svgElement) {
+ console.warn("Cannot export: No flamegraph SVG found");
+ return;
+ }
+ const serializer = new XMLSerializer();
+ const svgString = serializer.serializeToString(svgElement);
+ const blob = new Blob([svgString], { type: "image/svg+xml" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = "python-performance-flamegraph.svg";
+ a.click();
+ URL.revokeObjectURL(url);
+}
+
+// ============================================================================
+// Initialization
+// ============================================================================
+
+function initFlamegraph() {
+ ensureLibraryLoaded();
+ restoreUIState();
+
+ let processedData = EMBEDDED_DATA;
+ if (EMBEDDED_DATA.strings) {
+ stringTable = EMBEDDED_DATA.strings;
+ processedData = resolveStringIndices(EMBEDDED_DATA);
+ }
+
+ originalData = processedData;
+ initThreadFilter(processedData);
+
+ const tooltip = createPythonTooltip(processedData);
+ const chart = createFlamegraph(tooltip, processedData.value);
+ renderFlamegraph(chart, processedData);
+ initSearchHandlers();
+ initSidebarResize();
+ handleResize();
+}
+
+if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", initFlamegraph);
+} else {
+ initFlamegraph();
+}
<!doctype html>
-<html lang="en">
+<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>Python Performance Flamegraph</title>
+ <title>Tachyon Profiler - Flamegraph</title>
<!-- INLINE_VENDOR_D3_JS -->
<!-- INLINE_VENDOR_FLAMEGRAPH_CSS -->
<!-- INLINE_VENDOR_FLAMEGRAPH_JS -->
<!-- INLINE_CSS -->
</head>
<body>
- <div class="header">
- <div class="header-content">
- <div class="python-logo"><!-- INLINE_LOGO --></div>
- <div class="header-text">
- <h1>Tachyon Profiler Performance Flamegraph</h1>
- <div class="subtitle">
- Interactive visualization of function call performance
- </div>
+ <div class="app-layout">
+ <!-- Top Bar -->
+ <header class="top-bar">
+ <div class="brand">
+ <span class="brand-text">Tachyon</span>
+ <span class="brand-divider"></span>
+ <span class="brand-subtitle">Profiler</span>
</div>
- <div class="header-search">
- <input type="text" id="search-input" placeholder="🔍 Search functions..." />
+ <div class="search-wrapper">
+ <input
+ type="text"
+ id="search-input"
+ class="search-input"
+ placeholder="Search functions..."
+ />
+ <button
+ class="search-clear"
+ id="search-clear"
+ onclick="clearSearch()"
+ title="Clear search"
+ >×</button>
</div>
- </div>
- </div>
+ <div class="toolbar">
+ <button
+ class="toolbar-btn"
+ onclick="resetZoom()"
+ title="Reset zoom"
+ >⌂</button>
+ <button
+ class="toolbar-btn"
+ onclick="exportSVG()"
+ title="Export SVG"
+ >↓</button>
+ <button
+ class="toolbar-btn theme-toggle"
+ onclick="toggleTheme()"
+ title="Toggle theme"
+ id="theme-btn"
+ >☾</button>
+ </div>
+ </header>
- <!-- Compact Thread Stats Bar -->
- <div class="thread-stats-bar" id="thread-stats-bar" style="display: none;">
- <span class="thread-stat-item" id="gil-held-stat">
- <span class="stat-label">🟢 GIL Held:</span>
- <span class="stat-value" id="gil-held-pct">--</span>
- </span>
- <span class="thread-stat-separator">│</span>
- <span class="thread-stat-item" id="gil-released-stat">
- <span class="stat-label">🔴 GIL Released:</span>
- <span class="stat-value" id="gil-released-pct">--</span>
- </span>
- <span class="thread-stat-separator">│</span>
- <span class="thread-stat-item" id="gil-waiting-stat">
- <span class="stat-label">🟡 Waiting:</span>
- <span class="stat-value" id="gil-waiting-pct">--</span>
- </span>
- <span class="thread-stat-separator">│</span>
- <span class="thread-stat-item" id="gc-stat">
- <span class="stat-label">🗑️ GC:</span>
- <span class="stat-value" id="gc-pct">--</span>
- </span>
- </div>
+ <!-- Main Content -->
+ <div class="main-content">
+ <!-- Sidebar -->
+ <aside class="sidebar" id="sidebar">
+ <button
+ class="sidebar-toggle"
+ onclick="toggleSidebar()"
+ title="Toggle sidebar"
+ aria-label="Toggle sidebar"
+ >
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
+ <path d="M10 12L6 8L10 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+ </svg>
+ </button>
+ <div class="sidebar-resize-handle" id="sidebar-resize-handle"></div>
- <div class="stats-section">
- <!-- Hot Spots -->
- <div class="stats-container">
- <div class="stat-card hotspot-card">
- <div class="stat-icon">🥇</div>
- <div class="stat-content">
- <div class="stat-label">#1 Hot Spot</div>
- <div class="stat-file" id="hotspot-file-1">--</div>
- <div class="stat-value" id="hotspot-func-1">--</div>
- <div class="stat-detail" id="hotspot-detail-1">--</div>
- </div>
- </div>
- <div class="stat-card hotspot-card">
- <div class="stat-icon">🥈</div>
- <div class="stat-content">
- <div class="stat-label">#2 Hot Spot</div>
- <div class="stat-file" id="hotspot-file-2">--</div>
- <div class="stat-value" id="hotspot-func-2">--</div>
- <div class="stat-detail" id="hotspot-detail-2">--</div>
- </div>
- </div>
- <div class="stat-card hotspot-card">
- <div class="stat-icon">🥉</div>
- <div class="stat-content">
- <div class="stat-label">#3 Hot Spot</div>
- <div class="stat-file" id="hotspot-file-3">--</div>
- <div class="stat-value" id="hotspot-func-3">--</div>
- <div class="stat-detail" id="hotspot-detail-3">--</div>
- </div>
- </div>
- </div>
- </div>
+ <div class="sidebar-content">
+ <!-- Logo Section -->
+ <div class="sidebar-logo">
+ <div class="sidebar-logo-img"><!-- INLINE_LOGO --></div>
+ </div>
- <div class="controls">
- <div class="controls-content">
- <button onclick="resetZoom()">🏠 Reset Zoom</button>
- <button onclick="exportSVG()" class="secondary">📁 Export SVG</button>
- <button onclick="toggleLegend()">🔥 Heat Map Legend</button>
- <div class="thread-filter-wrapper">
- <label class="thread-filter-label">🧵 Thread:</label>
- <select id="thread-filter" class="thread-filter-select" onchange="filterByThread()">
- <option value="all">All Threads</option>
- </select>
- </div>
- </div>
- </div>
+ <!-- Profile Summary Section -->
+ <section class="sidebar-section collapsible" id="summary-section">
+ <button class="section-header" onclick="toggleSection('summary-section')">
+ <h3 class="section-title">Profile Summary</h3>
+ <svg class="section-chevron" width="12" height="12" viewBox="0 0 12 12" fill="none">
+ <path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+ </svg>
+ </button>
+ <div class="section-content">
+ <div class="summary-grid">
+ <div class="summary-card" id="summary-samples">
+ <div class="summary-icon">📊</div>
+ <div class="summary-data">
+ <div class="summary-value" id="stat-total-samples">--</div>
+ <div class="summary-label">Total Samples</div>
+ </div>
+ </div>
+ <div class="summary-card" id="summary-duration">
+ <div class="summary-icon">⏱</div>
+ <div class="summary-data">
+ <div class="summary-value" id="stat-duration">--</div>
+ <div class="summary-label">Duration</div>
+ </div>
+ </div>
+ <div class="summary-card" id="summary-rate">
+ <div class="summary-icon">⚡</div>
+ <div class="summary-data">
+ <div class="summary-value" id="stat-sample-rate">--</div>
+ <div class="summary-label">Samples/sec</div>
+ </div>
+ </div>
+ <div class="summary-card" id="summary-functions">
+ <div class="summary-icon">λ</div>
+ <div class="summary-data">
+ <div class="summary-value" id="stat-functions">--</div>
+ <div class="summary-label">Functions</div>
+ </div>
+ </div>
+ </div>
+ <!-- Efficiency Bar -->
+ <div class="efficiency-section" id="efficiency-section" style="display: none;">
+ <div class="efficiency-header">
+ <span class="efficiency-label">Sampling Efficiency</span>
+ <span class="efficiency-value" id="stat-efficiency">--</span>
+ </div>
+ <div class="efficiency-bar">
+ <div class="efficiency-fill" id="efficiency-fill"></div>
+ </div>
+ <div class="missed-samples-header">
+ <span class="efficiency-label">Missed samples</span>
+ <span class="efficiency-value" id="stat-missed-samples">--</span>
+ </div>
+ <div class="efficiency-bar">
+ <div class="efficiency-fill" id="missed-samples-fill"></div>
+ </div>
+
+ </div>
+ </div>
+ </section>
- <button id="show-info-btn" title="Show navigation guide">ℹ</button>
+ <!-- Thread Stats Section (GIL/GC) -->
+ <section class="sidebar-section thread-stats-section collapsible" id="thread-stats-bar" style="display: none;">
+ <button class="section-header" onclick="toggleSection('thread-stats-bar')">
+ <h3 class="section-title">Runtime Stats</h3>
+ <svg class="section-chevron" width="12" height="12" viewBox="0 0 12 12" fill="none">
+ <path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+ </svg>
+ </button>
+ <div class="section-content">
+ <div class="stats-grid">
+ <div class="stat-tile stat-tile--green" id="gil-held-stat">
+ <div class="stat-tile-value" id="gil-held-pct">--</div>
+ <div class="stat-tile-label">GIL Held</div>
+ </div>
+ <div class="stat-tile stat-tile--red" id="gil-released-stat">
+ <div class="stat-tile-value" id="gil-released-pct">--</div>
+ <div class="stat-tile-label">GIL Released</div>
+ </div>
+ <div class="stat-tile stat-tile--yellow" id="gil-waiting-stat">
+ <div class="stat-tile-value" id="gil-waiting-pct">--</div>
+ <div class="stat-tile-label">Waiting</div>
+ </div>
+ <div class="stat-tile stat-tile--purple" id="gc-stat">
+ <div class="stat-tile-value" id="gc-pct">--</div>
+ <div class="stat-tile-label">GC</div>
+ </div>
+ </div>
+ </div>
+ </section>
- <div class="info-panel" id="info-panel">
- <button id="close-info-btn" title="Close">×</button>
- <h3>Navigation Guide</h3>
- <p><strong>Click:</strong> Zoom into function</p>
- <p><strong>Hover:</strong> Show detailed information</p>
- <p><strong>Width:</strong> Time spent in function</p>
- <p><strong>Height:</strong> Call stack depth</p>
- <p><strong>Color:</strong> Performance intensity</p>
- </div>
+ <!-- Hotspots Section -->
+ <section class="sidebar-section collapsible" id="hotspots-section">
+ <button class="section-header" onclick="toggleSection('hotspots-section')">
+ <h3 class="section-title">Hotspots</h3>
+ <svg class="section-chevron" width="12" height="12" viewBox="0 0 12 12" fill="none">
+ <path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+ </svg>
+ </button>
+ <div class="section-content">
+ <div class="hotspot" id="hotspot-1">
+ <div class="hotspot-rank hotspot-rank--1">1</div>
+ <div class="hotspot-info">
+ <div class="hotspot-func" id="hotspot-func-1">--</div>
+ <div class="hotspot-file" id="hotspot-file-1">--</div>
+ <div class="hotspot-stats">
+ <span class="hotspot-percent" id="hotspot-percent-1">--</span>
+ <span id="hotspot-samples-1"></span>
+ </div>
+ </div>
+ </div>
+ <div class="hotspot" id="hotspot-2">
+ <div class="hotspot-rank hotspot-rank--2">2</div>
+ <div class="hotspot-info">
+ <div class="hotspot-func" id="hotspot-func-2">--</div>
+ <div class="hotspot-file" id="hotspot-file-2">--</div>
+ <div class="hotspot-stats">
+ <span class="hotspot-percent" id="hotspot-percent-2">--</span>
+ <span id="hotspot-samples-2"></span>
+ </div>
+ </div>
+ </div>
+ <div class="hotspot" id="hotspot-3">
+ <div class="hotspot-rank hotspot-rank--3">3</div>
+ <div class="hotspot-info">
+ <div class="hotspot-func" id="hotspot-func-3">--</div>
+ <div class="hotspot-file" id="hotspot-file-3">--</div>
+ <div class="hotspot-stats">
+ <span class="hotspot-percent" id="hotspot-percent-3">--</span>
+ <span id="hotspot-samples-3"></span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
- <div class="legend-panel" id="legend-panel">
- <h3>🔥 Performance Heat Map</h3>
- <div class="legend-item">
- <div class="legend-color" style="background-color: #3776ab"></div>
- <div>
- <div class="legend-label">Hottest Functions (≥60%)</div>
- <div class="legend-description">Highest performance impact</div>
- </div>
- </div>
- <div class="legend-item">
- <div class="legend-color" style="background-color: #4584bb"></div>
- <div>
- <div class="legend-label">Very Hot Functions (35-60%)</div>
- <div class="legend-description">High performance impact</div>
- </div>
- </div>
- <div class="legend-item">
- <div class="legend-color" style="background-color: #5592cc"></div>
- <div>
- <div class="legend-label">Hot Functions (18-35%)</div>
- <div class="legend-description">Notable performance cost</div>
- </div>
- </div>
- <div class="legend-item">
- <div class="legend-color" style="background-color: #ffd43b"></div>
- <div>
- <div class="legend-label">Warm Functions (12-18%)</div>
- <div class="legend-description">Moderate impact</div>
- </div>
- </div>
- <div class="legend-item">
- <div class="legend-color" style="background-color: #ffdc5c"></div>
- <div>
- <div class="legend-label">Medium Functions (6-12%)</div>
- <div class="legend-description">Some performance impact</div>
- </div>
- </div>
- <div class="legend-item">
- <div class="legend-color" style="background-color: #ffe47d"></div>
- <div>
- <div class="legend-label">Cool Functions (3-6%)</div>
- <div class="legend-description">Low performance impact</div>
- </div>
- </div>
- <div class="legend-item">
- <div class="legend-color" style="background-color: #ffec9e"></div>
- <div>
- <div class="legend-label">Cold Functions (1-3%)</div>
- <div class="legend-description">Minimal performance impact</div>
- </div>
- </div>
- <div class="legend-item">
- <div class="legend-color" style="background-color: #fff4bf"></div>
- <div>
- <div class="legend-label">Coldest Functions (<1%)</div>
- <div class="legend-description">Very low performance impact</div>
- </div>
+ <!-- Thread Filter Section -->
+ <section class="sidebar-section filter-section" id="thread-section" style="display: none;">
+ <label class="filter-label" for="thread-filter">Thread Filter</label>
+ <select
+ id="thread-filter"
+ class="filter-select"
+ onchange="filterByThread()"
+ >
+ <option value="all">All Threads</option>
+ </select>
+ </section>
+
+ <!-- Legend Section -->
+ <section class="sidebar-section legend-section collapsible" id="legend-section">
+ <button class="section-header" onclick="toggleSection('legend-section')">
+ <h3 class="section-title">Heat Map</h3>
+ <svg class="section-chevron" width="12" height="12" viewBox="0 0 12 12" fill="none">
+ <path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+ </svg>
+ </button>
+ <div class="section-content">
+ <div class="legend">
+ <div class="legend-item">
+ <div class="legend-color" style="background: var(--heat-8)"></div>
+ <span class="legend-label">Hottest</span>
+ <span class="legend-range">≥60%</span>
+ </div>
+ <div class="legend-item">
+ <div class="legend-color" style="background: var(--heat-7)"></div>
+ <span class="legend-label">Very Hot</span>
+ <span class="legend-range">35-60%</span>
+ </div>
+ <div class="legend-item">
+ <div class="legend-color" style="background: var(--heat-6)"></div>
+ <span class="legend-label">Hot</span>
+ <span class="legend-range">18-35%</span>
+ </div>
+ <div class="legend-item">
+ <div class="legend-color" style="background: var(--heat-5)"></div>
+ <span class="legend-label">Warm</span>
+ <span class="legend-range">12-18%</span>
+ </div>
+ <div class="legend-item">
+ <div class="legend-color" style="background: var(--heat-4)"></div>
+ <span class="legend-label">Medium</span>
+ <span class="legend-range">6-12%</span>
+ </div>
+ <div class="legend-item">
+ <div class="legend-color" style="background: var(--heat-3)"></div>
+ <span class="legend-label">Cool</span>
+ <span class="legend-range">3-6%</span>
+ </div>
+ <div class="legend-item">
+ <div class="legend-color" style="background: var(--heat-2)"></div>
+ <span class="legend-label">Cold</span>
+ <span class="legend-range">1-3%</span>
+ </div>
+ <div class="legend-item">
+ <div class="legend-color" style="background: var(--heat-1)"></div>
+ <span class="legend-label">Coldest</span>
+ <span class="legend-range"><1%</span>
+ </div>
+ </div>
+ </div>
+ </section>
+ </div>
+ </aside>
+
+ <!-- Chart Area -->
+ <main class="chart-area">
+ <div id="chart"></div>
+ </main>
</div>
- </div>
- <div class="chart-container">
- <div id="chart"></div>
+ <!-- Status Bar -->
+ <footer class="status-bar">
+ <span class="status-item" id="status-location" style="display: none;">
+ <span class="status-label">File:</span>
+ <span class="status-value" id="status-file">--</span>
+ </span>
+ <span class="status-item" id="status-func-item" style="display: none;">
+ <span class="status-label">Func:</span>
+ <span class="status-value" id="status-func">--</span>
+ </span>
+ <span class="status-item" id="status-time-item" style="display: none;">
+ <span class="status-label">Time:</span>
+ <span class="status-value" id="status-time">--</span>
+ </span>
+ <span class="status-item" id="status-percent-item" style="display: none;">
+ <span class="status-value accent" id="status-percent">--</span>
+ </span>
+ </footer>
</div>
<!-- INLINE_JS -->
if self.realtime_stats and len(self.sample_intervals) > 0:
print() # Add newline after real-time stats
- sample_rate = num_samples / running_time
+ sample_rate = num_samples / running_time if running_time > 0 else 0
error_rate = (errors / num_samples) * 100 if num_samples > 0 else 0
+ expected_samples = int(duration_sec / sample_interval_sec)
+ missed_samples = (expected_samples - num_samples) / expected_samples * 100 if expected_samples > 0 else 0
# Don't print stats for live mode (curses is handling display)
is_live_mode = LiveStatsCollector is not None and isinstance(collector, LiveStatsCollector)
# Pass stats to flamegraph collector if it's the right type
if hasattr(collector, 'set_stats'):
- collector.set_stats(self.sample_interval_usec, running_time, sample_rate, error_rate, mode=self.mode)
+ collector.set_stats(self.sample_interval_usec, running_time, sample_rate, error_rate, missed_samples, mode=self.mode)
- expected_samples = int(duration_sec / sample_interval_sec)
if num_samples < expected_samples and not is_live_mode and not interrupted:
print(
f"Warning: missed {expected_samples - num_samples} samples "
# Call parent collect to process frames
super().collect(stack_frames, skip_idle=skip_idle)
- def set_stats(self, sample_interval_usec, duration_sec, sample_rate, error_rate=None, mode=None):
+ def set_stats(self, sample_interval_usec, duration_sec, sample_rate,
+ error_rate=None, missed_samples=None, mode=None):
"""Set profiling statistics to include in flamegraph data."""
self.stats = {
"sample_interval_usec": sample_interval_usec,
"duration_sec": duration_sec,
"sample_rate": sample_rate,
"error_rate": error_rate,
+ "missed_samples": missed_samples,
"mode": mode
}
# Should be valid HTML
self.assertIn("<!doctype html>", content.lower())
self.assertIn("<html", content)
- self.assertIn("Python Performance Flamegraph", content)
+ self.assertIn("Tachyon Profiler - Flamegraph", content)
self.assertIn("d3-flame-graph", content)
# Should contain the data