]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-138122: New Tachyon UI (#142116)
authorLászló Kiss Kollár <kiss.kollar.laszlo@gmail.com>
Mon, 1 Dec 2025 17:34:14 +0000 (17:34 +0000)
committerGitHub <noreply@github.com>
Mon, 1 Dec 2025 17:34:14 +0000 (17:34 +0000)
Co-authored-by: Pablo Galindo Salgado <pablogsal@gmail.com>
Lib/profiling/sampling/flamegraph.css
Lib/profiling/sampling/flamegraph.js
Lib/profiling/sampling/flamegraph_template.html
Lib/profiling/sampling/sample.py
Lib/profiling/sampling/stack_collector.py
Lib/test/test_profiling/test_sampling_profiler/test_collectors.py

index 0a6fde2ad329e6cbf5c177ea3692d9d03821584e..1703815acd9e1df2ba9d42f0430c54ef7b25cf1e 100644 (file)
-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;
+  }
 }
index 7faac0effbc561630c3e49a08d6b2f8fadf0a9fb..7a2b2ef2e3135e8d64318a8adbad8a88d6d15996 100644 (file)
@@ -5,93 +5,219 @@ let stringTable = [];
 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' ? '&#9788;' : '&#9790;';  // 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' ? '&#9788;' : '&#9790;';
     }
+  }
 
-    // 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);
     }
 
@@ -101,163 +227,153 @@ function createPythonTooltip(data) {
     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, "&amp;")
-              .replace(/</g, "&lt;")
-              .replace(/>/g, "&gt;")}</div>`,
-        )
+        .map((line) => {
+          const isCurrent = line.startsWith("→");
+          const escaped = line.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
+          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)
@@ -265,142 +381,207 @@ function createFlamegraph(tooltip, rootValue) {
     .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;
@@ -408,97 +589,164 @@ const PROFILING_MODE_GIL = 2;
 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) {
@@ -512,21 +760,18 @@ function populateStats(data) {
     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 || '?';
@@ -551,96 +796,81 @@ function populateStats(data) {
 
   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');
@@ -649,9 +879,8 @@ function initThreadFilter(data) {
     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';
   }
 }
 
@@ -666,11 +895,9 @@ function filterByThread() {
   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) {
@@ -679,12 +906,10 @@ function filterByThread() {
     }
   }
 
-  // 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);
 }
 
@@ -694,10 +919,7 @@ function filterDataByThread(data, threadId) {
       return null;
     }
 
-    const filteredNode = {
-      ...node,
-      children: []
-    };
+    const filteredNode = { ...node, children: [] };
 
     if (node.children && Array.isArray(node.children)) {
       filteredNode.children = node.children
@@ -708,17 +930,6 @@ function filterDataByThread(data, threadId) {
     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;
@@ -728,8 +939,72 @@ function filterDataByThread(data, threadId) {
     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();
+}
index 5f94bbe69c4f4f55dd50ea36708daa10eb7ad213..09b673b76da506765aa038b694b45121e4b1b842 100644 (file)
@@ -1,9 +1,9 @@
 <!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"
+          >&times;</button>
         </div>
-      </div>
-    </div>
+        <div class="toolbar">
+          <button
+            class="toolbar-btn"
+            onclick="resetZoom()"
+            title="Reset zoom"
+          >&#8962;</button>
+          <button
+            class="toolbar-btn"
+            onclick="exportSVG()"
+            title="Export SVG"
+          >&#8595;</button>
+          <button
+            class="toolbar-btn theme-toggle"
+            onclick="toggleTheme()"
+            title="Toggle theme"
+            id="theme-btn"
+          >&#9790;</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">&#8505;</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">&times;</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">&ge;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">&lt;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 -->
index 82c0d3959ba22d1301a0a107f1a8e438b42d4734..88d9a4fa13baf957ca4020d3cca7b98e8ee5f71f 100644 (file)
@@ -112,8 +112,10 @@ class SampleProfiler:
         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)
@@ -124,9 +126,8 @@ class SampleProfiler:
 
         # 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 "
index 9028a8bebb19b4081893997776677f288bacc1bb..146a058a03ac1454299c7294f59bfeb6c36ca365 100644 (file)
@@ -113,13 +113,15 @@ class FlamegraphCollector(StackTraceCollector):
         # 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
         }
 
index 38665f5a591eec55a8fb8f52c03ca2faa766cc02..e8c12c2221549afb99ce2d7758b5b5a959ad67a0 100644 (file)
@@ -494,7 +494,7 @@ class TestSampleProfilerComponents(unittest.TestCase):
         # 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