/
githubmirror
/
cpython
Обзор
Документация
Войти
/
githubmirror
/
cpython
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
Doc/_static/tachyon-example-flamegraph.html
3 260 строк
691 KB
Pablo Galindo Salgado
gh-138122: Add some screenshots to the profiling.sampling docs (#142676)
14 дек 2025, 20:43
Не верифицирован
14 дек 2025, 20:43
e4d3c83
Код
Авторство
О чём код?
<!doctype html> <html lang="en" data-theme="light"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Tachyon Profiler - Flamegraph Report</title> <script> // https://d3js.org v7.8.5 Copyright 2010-2023 Mike Bostock !function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((t="undefined"!=typeof globalThis?globalThis:t||self).d3=t. … [Строка слишком длинная. Вы можете скачать файл] </script> <style> .d3-flame-graph rect { stroke: #EEEEEE; fill-opacity: .8; } .d3-flame-graph rect:hover { stroke: #474747; stroke-width: 0.5; cursor: pointer; } .d3-flame-graph-label { pointer-events: none; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; font-size: 12px; font-family: Verdana; margin-left: 4px; margin-right: 4px; line-height: 1.5; padding: 0 0 0; font-weight: 400; color: black; text-align: left; } .d3-flame-graph .fade { opacity: 0.6 !important; } .d3-flame-graph .title { font-size: 20px; font-family: Verdana; } .d3-flame-graph-tip { background-color: black; border: none; border-radius: 3px; padding: 5px 10px 5px 10px; min-width: 250px; text-align: left; color: white; z-index: 10; } </style> <script> !function(t,n){"object"==typeof exports&&"object"==typeof module?module.exports=n():"function"==typeof define&&define.amd?define([],n):"object"==typeof exports?exports.flamegraph=n():t.flamegraph=n()} … [Строка слишком длинная. Вы можете скачать файл] </script> <script> !function(t,n){"object"==typeof exports&&"object"==typeof module?module.exports=n():"function"==typeof define&&define.amd?define([],n):"object"==typeof exports?exports.flamegraph=n():(t.flamegraph=t.f … [Строка слишком длинная. Вы можете скачать файл] </script> <style> /* ========================================================================== Python Profiler - Shared CSS Foundation Design system shared between Flamegraph and Heatmap viewers ========================================================================== */ /* -------------------------------------------------------------------------- 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: 56px; --statusbar-height: 32px; /* Border radius */ --radius-sm: 4px; --radius-md: 8px; --radius-lg: 12px; /* Transitions */ --transition-fast: 0.15s ease; --transition-normal: 0.25s ease; --transition-slow: 0.3s ease; } /* Light theme (default) */ :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: #6f767e; --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: #7ba3d1; --heat-2: #a8d0ef; --heat-3: #d6e9f8; --heat-4: #ffe6a8; --heat-5: #ffd43b; --heat-6: #ffb84d; --heat-7: #ff9966; --heat-8: #ff6347; /* Code view specific */ --code-bg: #ffffff; --code-bg-line: #f8f9fa; --code-border: #e9ecef; --code-text: #2e3338; --code-text-muted: #8b949e; --code-accent: #3776ab; /* Navigation colors */ --nav-caller: #2563eb; --nav-caller-hover: #1d4ed8; --nav-callee: #dc2626; --nav-callee-hover: #b91c1c; /* Specialization status colors */ --spec-high: #4caf50; --spec-high-text: #2e7d32; --spec-high-bg: rgba(76, 175, 80, 0.15); --spec-medium: #ff9800; --spec-medium-text: #e65100; --spec-medium-bg: rgba(255, 152, 0, 0.15); --spec-low: #9e9e9e; --spec-low-text: #616161; --spec-low-bg: rgba(158, 158, 158, 0.15); /* Heatmap span highlighting colors */ --span-hot-base: 255, 100, 50; --span-cold-base: 150, 150, 150; } /* Dark theme */ [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: #757e8a; --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 - muted colors that provide sufficient contrast with light text */ --heat-1: rgba(74, 123, 167, 0.35); --heat-2: rgba(90, 159, 168, 0.38); --heat-3: rgba(106, 181, 181, 0.40); --heat-4: rgba(126, 196, 136, 0.42); --heat-5: rgba(160, 216, 120, 0.45); --heat-6: rgba(196, 222, 106, 0.48); --heat-7: rgba(244, 212, 77, 0.50); --heat-8: rgba(255, 107, 53, 0.55); /* Code view specific - dark mode */ --code-bg: #0d1117; --code-bg-line: #161b22; --code-border: #30363d; --code-text: #e6edf3; --code-text-muted: #6e7681; --code-accent: #58a6ff; /* Navigation colors - dark theme friendly */ --nav-caller: #58a6ff; --nav-caller-hover: #4184e4; --nav-callee: #f87171; --nav-callee-hover: #e53e3e; /* Specialization status colors - dark theme */ --spec-high: #81c784; --spec-high-text: #81c784; --spec-high-bg: rgba(129, 199, 132, 0.2); --spec-medium: #ffb74d; --spec-medium-text: #ffb74d; --spec-medium-bg: rgba(255, 183, 77, 0.2); --spec-low: #bdbdbd; --spec-low-text: #9e9e9e; --spec-low-bg: rgba(189, 189, 189, 0.15); /* Heatmap span highlighting colors - dark theme */ --span-hot-base: 255, 107, 53; --span-cold-base: 189, 189, 189; } /* -------------------------------------------------------------------------- Base Styles -------------------------------------------------------------------------- */ *, *::before, *::after { box-sizing: border-box; } html, body { margin: 0; padding: 0; } body { font-family: var(--font-sans); font-size: 14px; line-height: 1.6; color: var(--text-primary); background: var(--bg-primary); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; transition: background var(--transition-normal), color var(--transition-normal); } /* -------------------------------------------------------------------------- Layout Structure -------------------------------------------------------------------------- */ .app-layout { display: flex; flex-direction: column; } /* -------------------------------------------------------------------------- Top Bar -------------------------------------------------------------------------- */ .top-bar { height: var(--topbar-height); background: var(--header-gradient); display: flex; align-items: center; 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); } /* 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: 48px; height: 40px; flex-shrink: 0; } /* Style the inlined SVG/img inside brand-logo */ .brand-logo svg, .brand-logo img { width: 48px; height: 40px; display: block; object-fit: contain; filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2)); } .brand-info { display: flex; flex-direction: column; line-height: 1.15; } .brand-text { font-family: var(--font-sans); font-weight: 700; font-size: 16px; letter-spacing: -0.3px; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.15); color: inherit; text-decoration: none; } .brand-subtitle { font-weight: 500; font-size: 10px; opacity: 0.9; text-transform: uppercase; letter-spacing: 0.5px; } .brand-divider { width: 1px; height: 16px; background: rgba(255, 255, 255, 0.3); } /* Toolbar */ .toolbar { display: flex; align-items: center; gap: 6px; margin-left: auto; } .toolbar-btn { display: flex; align-items: center; justify-content: center; 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; text-decoration: none; transition: all var(--transition-fast); } .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); } /* -------------------------------------------------------------------------- Status Bar -------------------------------------------------------------------------- */ .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; } .status-item { display: flex; align-items: center; gap: 5px; } .status-item::before { content: ''; width: 4px; height: 4px; background: var(--python-gold); border-radius: 50%; } .status-item:first-child::before { display: none; } .status-label { color: var(--text-muted); } .status-value { color: var(--text-primary); font-weight: 500; } .status-value.accent { color: var(--accent); font-weight: 600; } /* -------------------------------------------------------------------------- Animations -------------------------------------------------------------------------- */ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes slideUp { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } } @keyframes shimmer { 0% { left: -100%; } 100% { left: 100%; } } /* -------------------------------------------------------------------------- Focus States (Accessibility) -------------------------------------------------------------------------- */ button:focus-visible, select:focus-visible, input:focus-visible, .toggle-switch:focus-visible, a.toolbar-btn:focus-visible { outline: 2px solid var(--python-gold); outline-offset: 2px; } /* -------------------------------------------------------------------------- Shared Responsive -------------------------------------------------------------------------- */ @media (max-width: 900px) { .brand-subtitle { display: none; } } @media (max-width: 600px) { .toolbar-btn:not(.theme-toggle) { display: none; } } /* -------------------------------------------------------------------------- Toggle Switch -------------------------------------------------------------------------- */ .toggle-switch { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; user-select: none; font-family: var(--font-sans); transition: opacity var(--transition-fast); flex-shrink: 0; } .toggle-switch:hover { opacity: 0.85; } .toggle-switch .toggle-label { font-size: 11px; font-weight: 500; color: var(--text-muted); transition: color var(--transition-fast); white-space: nowrap; display: inline-flex; flex-direction: column; } .toggle-switch .toggle-label.active { color: var(--text-primary); font-weight: 600; } /* Reserve space for bold text to prevent layout shift on toggle */ .toggle-switch .toggle-label::after { content: attr(data-text); font-weight: 600; height: 0; visibility: hidden; } .toggle-switch.disabled { opacity: 0.4; pointer-events: none; cursor: not-allowed; } .toggle-track { position: relative; width: 36px; height: 20px; background: var(--bg-tertiary); border: 2px solid var(--border); border-radius: 12px; transition: all var(--transition-fast); box-shadow: inset var(--shadow-sm); } .toggle-track:hover { border-color: var(--text-muted); } .toggle-track.on { background: var(--accent); border-color: var(--accent); box-shadow: 0 0 8px var(--accent-glow); } .toggle-track::after { content: ''; position: absolute; top: 1px; left: 1px; width: 14px; height: 14px; background: white; border-radius: 50%; box-shadow: var(--shadow-sm); transition: all var(--transition-fast); } .toggle-track.on::after { transform: translateX(16px); box-shadow: var(--shadow-md); } /* ========================================================================== Flamegraph Viewer - Component-Specific CSS DEPENDENCY: Requires _shared_assets/base.css to be loaded first This file extends the shared foundation with flamegraph-specific styles. ========================================================================== */ /* -------------------------------------------------------------------------- Layout Overrides (Flamegraph-specific) -------------------------------------------------------------------------- */ html, body { height: 100%; overflow: hidden; } .app-layout { height: 100vh; } .main-content { display: flex; flex: 1; min-height: 0; } /* -------------------------------------------------------------------------- Search Input (Flamegraph-specific) -------------------------------------------------------------------------- */ .search-wrapper { flex: 1; max-width: 360px; position: relative; } .search-input { width: 100%; padding: 8px 36px 8px 14px; font-family: var(--font-sans); font-size: 13px; color: #2e3338; 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); } .search-input::placeholder { color: #6c757d; } .search-input:focus { border-color: rgba(255, 255, 255, 0.8); 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); } [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); } .search-clear:hover { color: #2e3338; } [data-theme="dark"] .search-clear { color: #8b949e; } [data-theme="dark"] .search-clear:hover { color: #e6edf3; } .search-wrapper.has-value .search-clear { display: flex; } /* -------------------------------------------------------------------------- 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; justify-content: center; 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; } .sidebar-toggle svg { transition: transform var(--transition-fast); } .sidebar-toggle:hover { color: var(--accent); border-color: var(--accent); background: var(--accent-glow); } .sidebar.collapsed .sidebar-toggle { right: 9px; } .sidebar.collapsed .sidebar-toggle svg { transform: rotate(180deg); } .sidebar-content { flex: 1; overflow-y: auto; padding: 44px 14px 14px; } .sidebar.collapsed .sidebar-content { display: none; } .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; } .sidebar-resize-handle:hover, .sidebar-resize-handle.resizing { background: var(--python-gold); } .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); } .sidebar-resize-handle:hover::before { opacity: 1; } .sidebar.collapsed .sidebar-resize-handle { display: none; } body.resizing-sidebar { cursor: col-resize; user-select: none; } /* Sidebar Logo */ .sidebar-logo { display: flex; justify-content: center; margin-bottom: 16px; } .sidebar-logo-img { width: 220px; height: 180px; display: flex; align-items: center; justify-content: center; } .sidebar-logo-img svg, .sidebar-logo-img img { width: 100%; height: 100%; object-fit: contain; } /* Sidebar sections */ .sidebar-section { margin-bottom: 20px; } .sidebar-section:last-child { margin-bottom: 0; } .section-title { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.8px; color: var(--accent); margin: 0; flex: 1; } /* View Mode Section */ .view-mode-section .section-content { display: flex; justify-content: center; } /* Collapsible sections */ .collapsible .section-header { display: flex; 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 { transition: max-height var(--transition-slow) ease-out, opacity var(--transition-normal) ease-out, padding var(--transition-normal) ease-out; max-height: 1000px; opacity: 1; } .collapsible.collapsed .section-content { max-height: 0; opacity: 0; padding-top: 0; pointer-events: none; transition: max-height var(--transition-slow) ease-in, opacity var(--transition-normal) ease-in, padding var(--transition-normal) ease-in; } /* -------------------------------------------------------------------------- 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: 2px 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.08s); overflow: hidden; position: relative; } .summary-card:nth-child(1) { --i: 0; --card-color: 55, 118, 171; } .summary-card:nth-child(2) { --i: 1; --card-color: 40, 167, 69; } .summary-card:nth-child(3) { --i: 2; --card-color: 255, 193, 7; } .summary-card:nth-child(4) { --i: 3; --card-color: 111, 66, 193; } .summary-card:hover { border-color: rgba(var(--card-color), 0.6); background: linear-gradient(135deg, rgba(var(--card-color), 0.08) 0%, var(--bg-primary) 100%); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(var(--card-color), 0.15); } .summary-icon { font-size: 14px; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, rgba(var(--card-color), 0.15) 0%, rgba(var(--card-color), 0.05) 100%); border: 1px solid rgba(var(--card-color), 0.2); border-radius: 6px; flex-shrink: 0; transition: all var(--transition-fast); } .summary-card:hover .summary-icon { transform: scale(1.05); background: linear-gradient(135deg, rgba(var(--card-color), 0.25) 0%, rgba(var(--card-color), 0.1) 100%); } .summary-data { min-width: 0; flex: 1; overflow: hidden; } .summary-value { font-family: var(--font-mono); font-size: 13px; font-weight: 800; color: rgb(var(--card-color)); 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.2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* 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: var(--accent); } .efficiency-bar { height: 6px; background: var(--bg-tertiary); border-radius: 3px; overflow: hidden; } .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; } .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; } /* -------------------------------------------------------------------------- Thread Stats Grid (in Sidebar) -------------------------------------------------------------------------- */ .thread-stats-section { display: block; } .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; } .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; text-transform: uppercase; letter-spacing: 0.3px; color: var(--text-muted); margin-top: 2px; } /* 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 -------------------------------------------------------------------------- */ .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); } .hotspot.visible { opacity: 1; transform: translateY(0); } .hotspot:hover { border-color: var(--accent); box-shadow: var(--shadow-md); transform: translateY(-2px); } .hotspot.active { border-color: var(--python-gold); background: var(--accent-glow); box-shadow: 0 0 0 3px var(--accent-glow); } .hotspot:last-child { margin-bottom: 0; } .hotspot-rank { width: 26px; height: 26px; border-radius: 50%; display: flex; align-items: center; 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); } .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; } .hotspot-func { font-family: var(--font-mono); font-size: 11px; font-weight: 600; 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 { 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; } .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; outline: none; 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 6px center; background-size: 14px; transition: all var(--transition-fast); } .filter-select:hover { border-color: var(--accent-hover); box-shadow: 0 2px 6px var(--accent-glow); } .filter-select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); } /* -------------------------------------------------------------------------- Chart Area -------------------------------------------------------------------------- */ .chart-area { flex: 1; min-width: 0; overflow: hidden; background: var(--bg-primary); position: relative; } #chart { width: 100%; height: 100%; padding: 16px; overflow: auto; } /* D3 Flamegraph overrides */ .d3-flame-graph rect { stroke: rgba(55, 118, 171, 0.3); stroke-width: 1px; cursor: pointer; transition: filter 0.1s ease; } .d3-flame-graph rect:hover { stroke: var(--python-blue); stroke-width: 2px; filter: brightness(1.08); } .d3-flame-graph text { font-family: var(--font-sans); font-size: 12px; font-weight: 500; fill: var(--text-primary); pointer-events: none; } /* Search highlight */ .d3-flame-graph rect.search-match { stroke: #ff6b35 !important; stroke-width: 2px !important; stroke-dasharray: 4 2; } .d3-flame-graph rect.search-dim { opacity: 0.25; } /* -------------------------------------------------------------------------- Tooltip -------------------------------------------------------------------------- */ .python-tooltip { position: absolute; z-index: 1000; pointer-events: none; background: var(--bg-primary); border: 1px solid var(--border); border-radius: 8px; 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; font-weight: 600; color: var(--accent); line-height: 1.3; word-break: break-word; margin-bottom: 4px; } .tooltip-location { font-family: var(--font-mono); font-size: 11px; color: var(--text-secondary); 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; } .tooltip-stat-value.accent { color: var(--accent); } .tooltip-source { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border); } .tooltip-source-title { font-size: 11px; font-weight: 600; color: var(--accent); margin-bottom: 6px; } .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; } .tooltip-source-line { color: var(--text-secondary); padding: 1px 0; } .tooltip-source-line.current { color: var(--accent); font-weight: 600; } .tooltip-hint { margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--border); font-size: 11px; color: var(--text-muted); text-align: center; } /* -------------------------------------------------------------------------- Tooltip Bytecode/Opcode Section -------------------------------------------------------------------------- */ .tooltip-opcodes { margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--border); } .tooltip-opcodes-title { color: var(--accent); font-size: 13px; margin-bottom: 8px; font-weight: 600; } .tooltip-opcodes-list { background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: 6px; padding: 10px; } .tooltip-opcode-row { display: grid; grid-template-columns: 1fr 60px 60px; gap: 8px; align-items: center; padding: 3px 0; } .tooltip-opcode-name { font-family: var(--font-mono); font-size: 11px; color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .tooltip-opcode-name.specialized { color: var(--spec-high-text); } .tooltip-opcode-base-hint { color: var(--text-muted); font-size: 11px; margin-left: 4px; } .tooltip-opcode-badge { background: var(--spec-high); color: white; font-size: 9px; padding: 1px 4px; border-radius: 3px; margin-left: 4px; } .tooltip-opcode-count { text-align: right; font-size: 11px; color: var(--text-secondary); } .tooltip-opcode-bar { background: var(--bg-secondary); border-radius: 2px; height: 8px; overflow: hidden; } .tooltip-opcode-bar-fill { background: linear-gradient(90deg, var(--python-blue), var(--python-blue-light)); height: 100%; } /* -------------------------------------------------------------------------- Responsive (Flamegraph-specific) -------------------------------------------------------------------------- */ @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); } .search-wrapper { max-width: 220px; } } @media (max-width: 600px) { .search-wrapper { max-width: 160px; } .brand-info { display: none; } .stats-grid { grid-template-columns: 1fr; } } /* -------------------------------------------------------------------------- Flamegraph Root Node Styling -------------------------------------------------------------------------- */ /* Style the root node - no border, themed text */ .d3-flame-graph g:first-of-type rect { stroke: none; } .d3-flame-graph g:first-of-type .d3-flame-graph-label { color: var(--text-muted); } /* -------------------------------------------------------------------------- Flamegraph-Specific Toggle Override -------------------------------------------------------------------------- */ #toggle-invert .toggle-track.on { background: #8e44ad; border-color: #8e44ad; box-shadow: 0 0 8px rgba(142, 68, 173, 0.3); } .toggle-switch:focus-visible { border-radius: 4px; } </style> </head> <body> <div class="app-layout"> <!-- Top Bar --> <header class="top-bar"> <div class="brand"> <div class="brand-logo" id="navbar-logo"></div> <span class="brand-text">Tachyon</span> <span class="brand-divider"></span> <span class="brand-subtitle">Flamegraph Report</span> </div> <div class="search-wrapper"> <input type="text" id="search-input" class="search-input" placeholder="Search functions..." /> <button class="search-clear" id="search-clear" onclick="clearSearch()" title="Clear search" >×</button> </div> <div class="toolbar"> <button class="toolbar-btn" onclick="resetZoom()" title="Reset zoom" > <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"> <path d="M8.707 1.5a1 1 0 0 0-1.414 0L.646 8.146a.5.5 0 0 0 .708.708L2 8.207V13.5A1.5 1.5 0 0 0 3.5 15h9a1.5 1.5 0 0 0 1.5-1.5V8.207l.646.647a.5.5 0 0 0 .708-.708L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293zM13 7.207V13.5a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5V7.207l5-5z"/> </svg> </button> <button class="toolbar-btn" onclick="exportSVG()" title="Export SVG" > <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"> <path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5"/><path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708z"/> </svg> </button> <a class="toolbar-btn" href="https://docs.python.org/3.15/library/profiling.sampling.html" target="_blank" title="Documentation" > <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"> <path d="M1 2.828c.885-.37 2.154-.769 3.388-.893 1.33-.134 2.458.063 3.112.752v9.746c-.935-.53-2.12-.603-3.213-.493-1.18.12-2.37.461-3.287.811zm7.5-.141c.654-.689 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v9.923c-.918-.35-2.107-.692-3.287-.81-1.094-.111-2.278-.039-3.213.492zM8 1.783C7.015.936 5.587.81 4.287.94c-1.514.153-3.042.672-3.994 1.105A.5.5 0 0 0 0 2.5v11a.5.5 0 0 0 .707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 0 0 .78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0 0 16 13.5v-11a.5.5 0 0 0-.293-.455c-.952-.433-2.48-.952-3.994-1.105C10.413.809 8.985.936 8 1.783"/> </svg> </a> <button class="toolbar-btn theme-toggle" onclick="toggleTheme()" title="Toggle theme" id="theme-btn" > <svg class="icon-moon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"> <path d="M6 .278a.77.77 0 0 1 .08.858 7.2 7.2 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277q.792-.001 1.533-.16a.79.79 0 0 1 .81.316.73.73 0 0 1-.031.893A8.35 8.35 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.75.75 0 0 1 6 .278M4.858 1.311A7.27 7.27 0 0 0 1.025 7.71c0 4.02 3.279 7.276 7.319 7.276a7.32 7.32 0 0 0 5.205-2.162q-.506.063-1.029.063c-4.61 0-8.343-3.714-8.343-8.29 0-1.167.242-2.278.681-3.286"/> </svg> <svg class="icon-sun" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" style="display:none"> <path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6m0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8M8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0m0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13m8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5M3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8m10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0m-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0m9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707M4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708"/> </svg> </button> </div> </header> <!-- 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="sidebar-content"> <!-- Logo Section --> <div class="sidebar-logo"> <div class="sidebar-logo-img"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAhoAAAHDCAYAAABiYLNcAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAGYktHRAD/AP8A/6C9 … [Строка слишком длинная. Вы можете скачать файл] </div> <!-- View Mode Section --> <section class="sidebar-section view-mode-section collapsible" id="view-mode-section"> <button class="section-header" onclick="toggleSection('view-mode-section')"> <h3 class="section-title">View Mode</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="toggle-switch" id="toggle-invert" title="Toggle between standard and inverted flamegraph view" tabindex="0"> <span class="toggle-label active" data-text="Flamegraph">Flamegraph</span> <div class="toggle-track"></div> <span class="toggle-label" data-text="Inverted Flamegraph">Inverted Flamegraph</span> </div> </div> </section> <!-- 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> <!-- 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 GIL</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 class="stat-tile stat-tile--red" id="exc-stat"> <div class="stat-tile-value" id="exc-pct">--</div> <div class="stat-tile-label">Exception</div> </div> </div> </div> </section> <!-- 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> <!-- Thread Filter Section --> <section class="sidebar-section filter-section" id="thread-section" style="display: none;"> <label class="filter-label" for="thread-filter">Thread Filter</label> <select id="thread-filter" class="filter-select" onchange="filterByThread()" > <option value="all">All Threads</option> </select> </section> <!-- Legend Section --> <section class="sidebar-section legend-section collapsible" id="legend-section"> <button class="section-header" onclick="toggleSection('legend-section')"> <h3 class="section-title">Heat Map</h3> <svg class="section-chevron" width="12" height="12" viewBox="0 0 12 12" fill="none"> <path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> </svg> </button> <div class="section-content"> <div class="legend"> <div class="legend-item"> <div class="legend-color" style="background: var(--heat-8)"></div> <span class="legend-label">Hottest</span> <span class="legend-range">≥60%</span> </div> <div class="legend-item"> <div class="legend-color" style="background: var(--heat-7)"></div> <span class="legend-label">Very Hot</span> <span class="legend-range">35-60%</span> </div> <div class="legend-item"> <div class="legend-color" style="background: var(--heat-6)"></div> <span class="legend-label">Hot</span> <span class="legend-range">18-35%</span> </div> <div class="legend-item"> <div class="legend-color" style="background: var(--heat-5)"></div> <span class="legend-label">Warm</span> <span class="legend-range">12-18%</span> </div> <div class="legend-item"> <div class="legend-color" style="background: var(--heat-4)"></div> <span class="legend-label">Medium</span> <span class="legend-range">6-12%</span> </div> <div class="legend-item"> <div class="legend-color" style="background: var(--heat-3)"></div> <span class="legend-label">Cool</span> <span class="legend-range">3-6%</span> </div> <div class="legend-item"> <div class="legend-color" style="background: var(--heat-2)"></div> <span class="legend-label">Cold</span> <span class="legend-range">1-3%</span> </div> <div class="legend-item"> <div class="legend-color" style="background: var(--heat-1)"></div> <span class="legend-label">Coldest</span> <span class="legend-range"><1%</span> </div> </div> </div> </section> </div> </aside> <!-- Chart Area --> <main class="chart-area"> <div id="chart"></div> </main> </div> <!-- Status Bar --> <footer class="status-bar"> <span class="status-item" id="status-brand"> <span class="status-value">Tachyon Profiler</span> </span> <span class="status-item" id="status-tagline"> <span class="status-label">Python Sampling Profiler</span> </span> <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> <script> const EMBEDDED_DATA = {"name": 224, "value": 1892, "children": [{"name": 4, "value": 1892, "children": [{"name": 7, "value": 1892, "children": [{"name": 11, "value": 1892, "children": [{"name": 54, "v … [Строка слишком длинная. Вы можете скачать файл] // Global string table for resolving string indices let stringTable = []; let normalData = null; let invertedData = null; let currentThreadFilter = 'all'; let isInverted = false; // Heat colors are now defined in CSS variables (--heat-1 through --heat-8) // and automatically switch with theme changes - no JS color arrays needed! // Opcode mappings - loaded from embedded data (generated by Python) let OPCODE_NAMES = {}; let DEOPT_MAP = {}; // Initialize opcode mappings from embedded data function initOpcodeMapping(data) { if (data && data.opcode_mapping) { OPCODE_NAMES = data.opcode_mapping.names || {}; DEOPT_MAP = data.opcode_mapping.deopt || {}; } } // Get opcode info from opcode number function getOpcodeInfo(opcode) { const opname = OPCODE_NAMES[opcode] || `<${opcode}>`; const baseOpcode = DEOPT_MAP[opcode]; const isSpecialized = baseOpcode !== undefined; const baseOpname = isSpecialized ? (OPCODE_NAMES[baseOpcode] || `<${baseOpcode}>`) : opname; return { opname: opname, baseOpname: baseOpname, isSpecialized: isSpecialized }; } // ============================================================================ // String Resolution // ============================================================================ function resolveString(index) { if (index === null || index === undefined) { return null; } if (typeof index === 'number' && index >= 0 && index < stringTable.length) { return stringTable[index]; } return String(index); } function resolveStringIndices(node) { if (!node) return node; const resolved = { ...node }; 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.querySelector('.icon-moon').style.display = next === 'dark' ? 'none' : ''; btn.querySelector('.icon-sun').style.display = next === 'dark' ? '' : 'none'; } // Re-render flamegraph with new theme colors if (window.flamegraphData && normalData) { const currentData = isInverted ? invertedData : normalData; const tooltip = createPythonTooltip(currentData); const chart = createFlamegraph(tooltip, currentData.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'; } } 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); } } } 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.querySelector('.icon-moon').style.display = savedTheme === 'dark' ? 'none' : ''; btn.querySelector('.icon-sun').style.display = savedTheme === 'dark' ? '' : 'none'; } } // 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'; } } // 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'); } } } // ============================================================================ // Logo/Favicon Setup // ============================================================================ function setupLogos() { const logo = document.querySelector('.sidebar-logo-img img'); if (!logo) return; const navbarLogoContainer = document.getElementById('navbar-logo'); if (navbarLogoContainer) { const navbarLogo = logo.cloneNode(true); navbarLogoContainer.appendChild(navbarLogo); } const favicon = document.createElement('link'); favicon.rel = 'icon'; favicon.type = 'image/png'; favicon.href = logo.src; document.head.appendChild(favicon); } // ============================================================================ // 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 brandEl = document.getElementById('status-brand'); const taglineEl = document.getElementById('status-tagline'); if (brandEl) brandEl.style.display = 'none'; if (taglineEl) taglineEl.style.display = 'none'; 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'; }); const brandEl = document.getElementById('status-brand'); const taglineEl = document.getElementById('status-tagline'); if (brandEl) brandEl.style.display = 'flex'; if (taglineEl) taglineEl.style.display = 'flex'; } // ============================================================================ // Tooltip // ============================================================================ function createPythonTooltip(data) { const pythonTooltip = flamegraph.tooltip.defaultFlamegraphTooltip(); pythonTooltip.show = function (d, element) { if (!this._tooltip) { this._tooltip = d3.select("body") .append("div") .attr("class", "python-tooltip") .style("opacity", 0); } const timeMs = (d.data.value / 1000).toFixed(2); const percentage = ((d.data.value / data.value) * 100).toFixed(2); const calls = d.data.calls || 0; const childCount = d.children ? d.children.length : 0; const source = d.data.source; 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) => { const isCurrent = line.startsWith("→"); const escaped = line.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); return `<div class="tooltip-source-line${isCurrent ? ' current' : ''}">${escaped}</div>`; }) .join(""); sourceSection = ` <div class="tooltip-source"> <div class="tooltip-source-title">Source Code:</div> <div class="tooltip-source-code">${sourceLines}</div> </div>`; } // Create bytecode/opcode section if available let opcodeSection = ""; const opcodes = d.data.opcodes; if (opcodes && typeof opcodes === 'object' && Object.keys(opcodes).length > 0) { // Sort opcodes by sample count (descending) const sortedOpcodes = Object.entries(opcodes) .sort((a, b) => b[1] - a[1]) .slice(0, 8); // Limit to top 8 const totalOpcodeSamples = sortedOpcodes.reduce((sum, [, count]) => sum + count, 0); const maxCount = sortedOpcodes[0][1] || 1; const opcodeLines = sortedOpcodes.map(([opcode, count]) => { const opcodeInfo = getOpcodeInfo(parseInt(opcode, 10)); const pct = ((count / totalOpcodeSamples) * 100).toFixed(1); const barWidth = (count / maxCount) * 100; const specializedBadge = opcodeInfo.isSpecialized ? '<span class="tooltip-opcode-badge">SPECIALIZED</span>' : ''; const baseOpHint = opcodeInfo.isSpecialized ? `<span class="tooltip-opcode-base-hint">(${opcodeInfo.baseOpname})</span>` : ''; const nameClass = opcodeInfo.isSpecialized ? 'tooltip-opcode-name specialized' : 'tooltip-opcode-name'; return ` <div class="tooltip-opcode-row"> <div class="${nameClass}"> ${opcodeInfo.opname}${baseOpHint}${specializedBadge} </div> <div class="tooltip-opcode-count">${count.toLocaleString()} (${pct}%)</div> <div class="tooltip-opcode-bar"> <div class="tooltip-opcode-bar-fill" style="width: ${barWidth}%;"></div> </div> </div>`; }).join(''); opcodeSection = ` <div class="tooltip-opcodes"> <div class="tooltip-opcodes-title">Bytecode Instructions:</div> <div class="tooltip-opcodes-list"> ${opcodeLines} </div> </div>`; } const fileLocationHTML = isSpecialFrame ? "" : ` <div class="tooltip-location">${filename}${d.data.lineno ? ":" + d.data.lineno : ""}</div>`; const tooltipHTML = ` <div class="tooltip-header"> <div class="tooltip-title">${funcname}</div> ${fileLocationHTML} </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} ${opcodeSection} <div class="tooltip-hint"> ${childCount > 0 ? "Click to zoom into this function" : "Leaf function - no children"} </div> `; // Position tooltip const event = d3.event || window.event; const mouseX = event.pageX || event.clientX; const mouseY = event.pageY || event.clientY; const padding = 12; this._tooltip.html(tooltipHTML); // 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; } if (top + tooltipHeight > window.innerHeight) { top = mouseY - tooltipHeight - padding; if (top < 0) top = padding; } this._tooltip .style("left", left + "px") .style("top", top + "px") .transition() .duration(150) .style("opacity", 1); // Update status bar updateStatusBar(d.data, data.value); }; pythonTooltip.hide = function () { if (this._tooltip) { 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(width) .cellHeight(20) .transitionDuration(300) .minFrameSize(1) .tooltip(tooltip) .inverted(true) .setColorMapper(function (d) { // Root node should be transparent if (d.depth === 0) return 'transparent'; const percentage = d.data.value / rootValue; const level = getHeatLevel(percentage); return heatColors[level]; }); return chart; } function renderFlamegraph(chart, data) { d3.select("#chart").datum(data).call(chart); window.flamegraphChart = chart; window.flamegraphData = data; populateStats(data); } // ============================================================================ // Search // ============================================================================ function updateSearchHighlight(searchTerm, searchInput) { d3.selectAll("#chart rect") .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) { 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(); // 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).classed("search-match", true); } else { d3.select(this).classed("search-dim", true); } } }); if (searchInput) { 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.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 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 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(); }); document.addEventListener('mousemove', function(e) { if (!isResizing) return; const deltaX = e.clientX - startX; const newWidth = Math.min(Math.max(startWidth + deltaX, minWidth), maxWidth); sidebar.style.width = newWidth + 'px'; e.preventDefault(); }); document.addEventListener('mouseup', function() { if (isResizing) { isResizing = false; resizeHandle.classList.remove('resizing'); document.body.classList.remove('resizing-sidebar'); // Save the new width const width = sidebar.offsetWidth; localStorage.setItem('flamegraph-sidebar-width', width); // Resize chart after sidebar resize setTimeout(() => { resizeChart(); }, 10); } }); } // ============================================================================ // Thread Stats // ============================================================================ // Mode constants (must match constants.py) const PROFILING_MODE_WALL = 0; const PROFILING_MODE_CPU = 1; const PROFILING_MODE_GIL = 2; const PROFILING_MODE_ALL = 3; function populateThreadStats(data, selectedThreadId = null) { const stats = data?.stats; if (!stats || !stats.thread_stats) { return; } const mode = stats.mode !== undefined ? stats.mode : PROFILING_MODE_WALL; let threadStats; if (selectedThreadId !== null && stats.per_thread_stats && stats.per_thread_stats[selectedThreadId]) { threadStats = stats.per_thread_stats[selectedThreadId]; } else { threadStats = stats.thread_stats; } if (!threadStats || typeof threadStats.total !== 'number' || threadStats.total <= 0) { return; } const section = document.getElementById('thread-stats-bar'); if (!section) { return; } section.style.display = 'block'; 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'; } else { // Show all stats if (gilHeldStat) gilHeldStat.style.display = 'block'; if (gilReleasedStat) gilReleasedStat.style.display = 'block'; if (gilWaitingStat) gilWaitingStat.style.display = 'block'; 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'); // GIL Released = not holding GIL and not waiting for it const gilReleasedPct = Math.max(0, 100 - (threadStats.has_gil_pct || 0) - (threadStats.gil_requested_pct || 0)); if (gilReleasedPctElem) gilReleasedPctElem.textContent = `${gilReleasedPct.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)}%`; // Exception stats const excPctElem = document.getElementById('exc-pct'); if (excPctElem) excPctElem.textContent = `${(threadStats.has_exception_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 // Use normal (non-inverted) tree structure, but respect thread filtering const uniqueFunctions = new Set(); function collectUniqueFunctions(node) { if (!node) return; const filename = resolveString(node.filename) || 'unknown'; const funcname = resolveString(node.funcname) || resolveString(node.name) || 'unknown'; const lineno = node.lineno || 0; const key = `${filename}|${lineno}|${funcname}`; uniqueFunctions.add(key); if (node.children) node.children.forEach(collectUniqueFunctions); } // In inverted mode, use normalData (with thread filter if active) // In normal mode, use the passed data (already has thread filter applied if any) let functionCountSource; if (!normalData) { functionCountSource = data; } else if (isInverted) { if (currentThreadFilter !== 'all') { functionCountSource = filterDataByThread(normalData, parseInt(currentThreadFilter)); } else { functionCountSource = normalData; } } else { functionCountSource = data; } collectUniqueFunctions(functionCountSource); const functionsEl = document.getElementById('stat-functions'); if (functionsEl) functionsEl.textContent = formatNumber(uniqueFunctions.size); // 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) { // Populate profile summary populateProfileSummary(data); // Populate thread statistics if available populateThreadStats(data); // For hotspots: use normal (non-inverted) tree structure, but respect thread filtering. // In inverted view, the tree structure changes but the hottest functions remain the same. // However, if a thread filter is active, we need to show that thread's hotspots. let hotspotSource; if (!normalData) { hotspotSource = data; } else if (isInverted) { // In inverted mode, use normalData (with thread filter if active) if (currentThreadFilter !== 'all') { hotspotSource = filterDataByThread(normalData, parseInt(currentThreadFilter)); } else { hotspotSource = normalData; } } else { // In normal mode, use the passed data (already has thread filter applied if any) hotspotSource = data; } const totalSamples = hotspotSource.value || 0; const functionMap = new Map(); function collectFunctions(node) { if (!node) return; let filename = resolveString(node.filename); let funcname = resolveString(node.funcname); if (!filename || !funcname) { const nameStr = resolveString(node.name); if (nameStr?.includes('(')) { const match = nameStr.match(/^(.+?)\s*\((.+?):(\d+)\)$/); if (match) { funcname = funcname || match[1]; filename = filename || match[2]; } } } filename = filename || 'unknown'; funcname = funcname || 'unknown'; if (filename !== 'unknown' && funcname !== 'unknown' && node.value > 0) { let childrenValue = 0; if (node.children) { childrenValue = node.children.reduce((sum, child) => sum + child.value, 0); } const directSamples = Math.max(0, node.value - childrenValue); const funcKey = `${filename}:${node.lineno || '?'}:${funcname}`; if (functionMap.has(funcKey)) { const existing = functionMap.get(funcKey); existing.directSamples += directSamples; existing.directPercent = (existing.directSamples / totalSamples) * 100; if (directSamples > existing.maxSingleSamples) { existing.filename = filename; existing.lineno = node.lineno || '?'; existing.maxSingleSamples = directSamples; } } else { functionMap.set(funcKey, { filename: filename, lineno: node.lineno || '?', funcname: funcname, directSamples, directPercent: (directSamples / totalSamples) * 100, maxSingleSamples: directSamples }); } } if (node.children) { node.children.forEach(child => collectFunctions(child)); } } collectFunctions(hotspotSource); const hotSpots = Array.from(functionMap.values()) .filter(f => f.directPercent > 0.5) .sort((a, b) => b.directPercent - a.directPercent) .slice(0, 3); // Populate and animate hotspot cards for (let i = 0; i < 3; i++) { const num = i + 1; 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}`); 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 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 { if (funcEl) funcEl.textContent = '--'; if (fileEl) fileEl.textContent = '--'; if (percentEl) percentEl.textContent = '--'; if (samplesEl) samplesEl.textContent = ''; } // 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'; } setTimeout(() => { card.classList.add('visible'); }, 100 + i * 80); } } } // ============================================================================ // Thread Filter // ============================================================================ function initThreadFilter(data) { const threadFilter = document.getElementById('thread-filter'); const threadSection = document.getElementById('thread-section'); if (!threadFilter || !data.threads) return; threadFilter.innerHTML = '<option value="all">All Threads</option>'; const threads = data.threads || []; threads.forEach(threadId => { const option = document.createElement('option'); option.value = threadId; option.textContent = `Thread ${threadId}`; threadFilter.appendChild(option); }); if (threads.length > 1 && threadSection) { threadSection.style.display = 'block'; } } function filterByThread() { const threadFilter = document.getElementById('thread-filter'); if (!threadFilter || !normalData) return; const selectedThread = threadFilter.value; currentThreadFilter = selectedThread; const baseData = isInverted ? invertedData : normalData; let filteredData; let selectedThreadId = null; if (selectedThread === 'all') { filteredData = baseData; } else { selectedThreadId = parseInt(selectedThread, 10); filteredData = filterDataByThread(baseData, selectedThreadId); if (filteredData.strings) { stringTable = filteredData.strings; filteredData = resolveStringIndices(filteredData); } } const tooltip = createPythonTooltip(filteredData); const chart = createFlamegraph(tooltip, filteredData.value); renderFlamegraph(chart, filteredData); populateThreadStats(baseData, selectedThreadId); } function filterDataByThread(data, threadId) { function filterNode(node) { if (!node.threads || !node.threads.includes(threadId)) { return null; } const filteredNode = { ...node, children: [] }; if (node.children && Array.isArray(node.children)) { filteredNode.children = node.children .map(child => filterNode(child)) .filter(child => child !== null); } return filteredNode; } function recalculateValue(node) { if (!node.children || node.children.length === 0) { return node.value || 0; } const childrenValue = node.children.reduce((sum, child) => sum + recalculateValue(child), 0); node.value = Math.max(node.value || 0, childrenValue); return node.value; } 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); } // ============================================================================ // Inverted Flamegraph // ============================================================================ // Example: "file.py|10|foo" or "~|0|<GC>" for special frames function getInvertNodeKey(node) { return `${node.filename || '~'}|${node.lineno || 0}|${node.funcname || node.name}`; } function accumulateInvertedNode(parent, stackFrame, leaf) { const key = getInvertNodeKey(stackFrame); if (!parent.children[key]) { parent.children[key] = { name: stackFrame.name, value: 0, children: {}, filename: stackFrame.filename, lineno: stackFrame.lineno, funcname: stackFrame.funcname, source: stackFrame.source, threads: new Set() }; } const node = parent.children[key]; node.value += leaf.value; if (leaf.threads) { leaf.threads.forEach(t => node.threads.add(t)); } return node; } function processLeaf(invertedRoot, path, leafNode) { if (!path || path.length === 0) { return; } let invertedParent = accumulateInvertedNode(invertedRoot, leafNode, leafNode); // Walk backwards through the call stack for (let i = path.length - 2; i >= 0; i--) { invertedParent = accumulateInvertedNode(invertedParent, path[i], leafNode); } } function traverseInvert(path, currentNode, invertedRoot) { const children = currentNode.children || []; const childThreads = new Set(children.flatMap(c => c.threads || [])); const selfThreads = (currentNode.threads || []).filter(t => !childThreads.has(t)); if (selfThreads.length > 0) { processLeaf(invertedRoot, path, { ...currentNode, threads: selfThreads }); } children.forEach(child => traverseInvert(path.concat([child]), child, invertedRoot)); } function convertInvertDictToArray(node) { if (node.threads instanceof Set) { node.threads = Array.from(node.threads).sort((a, b) => a - b); } const children = node.children; if (children && typeof children === 'object' && !Array.isArray(children)) { node.children = Object.values(children); node.children.sort((a, b) => b.value - a.value || a.name.localeCompare(b.name)); node.children.forEach(convertInvertDictToArray); } return node; } function generateInvertedFlamegraph(data) { const invertedRoot = { name: data.name, value: data.value, children: {}, stats: data.stats, threads: data.threads }; const children = data.children || []; if (children.length === 0) { // Single-frame tree: the root is its own leaf processLeaf(invertedRoot, [data], data); } else { children.forEach(child => traverseInvert([child], child, invertedRoot)); } convertInvertDictToArray(invertedRoot); return invertedRoot; } function updateToggleUI(toggleId, isOn) { const toggle = document.getElementById(toggleId); if (toggle) { const track = toggle.querySelector('.toggle-track'); const labels = toggle.querySelectorAll('.toggle-label'); if (isOn) { track.classList.add('on'); labels[0].classList.remove('active'); labels[1].classList.add('active'); } else { track.classList.remove('on'); labels[0].classList.add('active'); labels[1].classList.remove('active'); } } } function toggleInvert() { isInverted = !isInverted; updateToggleUI('toggle-invert', isInverted); // Build inverted data on first use if (isInverted && !invertedData) { invertedData = generateInvertedFlamegraph(normalData); } let dataToRender = isInverted ? invertedData : normalData; if (currentThreadFilter !== 'all') { dataToRender = filterDataByThread(dataToRender, parseInt(currentThreadFilter)); } const tooltip = createPythonTooltip(dataToRender); const chart = createFlamegraph(tooltip, dataToRender.value); renderFlamegraph(chart, dataToRender); } // ============================================================================ // Initialization // ============================================================================ function initFlamegraph() { ensureLibraryLoaded(); restoreUIState(); setupLogos(); if (EMBEDDED_DATA.strings) { stringTable = EMBEDDED_DATA.strings; normalData = resolveStringIndices(EMBEDDED_DATA); } else { normalData = EMBEDDED_DATA; } // Initialize opcode mapping from embedded data initOpcodeMapping(EMBEDDED_DATA); // Inverted data will be built on first toggle invertedData = null; initThreadFilter(normalData); const tooltip = createPythonTooltip(normalData); const chart = createFlamegraph(tooltip, normalData.value); renderFlamegraph(chart, normalData); initSearchHandlers(); initSidebarResize(); handleResize(); const toggleInvertBtn = document.getElementById('toggle-invert'); if (toggleInvertBtn) { toggleInvertBtn.addEventListener('click', toggleInvert); } } // Keyboard shortcut: Enter/Space activates toggle switches document.addEventListener('keydown', function(e) { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { return; } if ((e.key === 'Enter' || e.key === ' ') && e.target.classList.contains('toggle-switch')) { e.preventDefault(); e.target.click(); } }); if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initFlamegraph); } else { initFlamegraph(); } </script> </body> </html>