mirror of
https://github.com/python/cpython.git
synced 2026-01-04 14:32:21 +00:00
3261 lines
691 KiB
HTML
3261 lines
691 KiB
HTML
|
|
<!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.d3||{})}(this,(function(t){"use strict";function n(t,n){return null==t||null==n?NaN:t<n?-1:t>n?1:t>=n?0:NaN}function e(t,n){return null==t||null==n?NaN:n<t?-1:n>t?1:n>=t?0:NaN}function r(t){let r,o,a;function u(t,n,e=0,i=t.length){if(e<i){if(0!==r(n,n))return i;do{const r=e+i>>>1;o(t[r],n)<0?e=r+1:i=r}while(e<i)}return e}return 2!==t.length?(r=n,o=(e,r)=>n(t(e),r),a=(n,e)=>t(n)-e):(r=t===n||t===e?t:i,o=t,a=t),{left:u,center:function(t,n,e=0,r=t.length){const i=u(t,n,e,r-1);return i>e&&a(t[i-1],n)>-a(t[i],n)?i-1:i},right:function(t,n,e=0,i=t.length){if(e<i){if(0!==r(n,n))return i;do{const r=e+i>>>1;o(t[r],n)<=0?e=r+1:i=r}while(e<i)}return e}}}function i(){return 0}function o(t){return null===t?NaN:+t}const a=r(n),u=a.right,c=a.left,f=r(o).center;var s=u;const l=d(y),h=d((function(t){const n=y(t);return(t,e,r,i,o)=>{n(t,e,(r<<=2)+0,(i<<=2)+0,o<<=2),n(t,e,r+1,i+1,o),n(t,e,r+2,i+2,o),n(t,e,r+3,i+3,o)}}));function d(t){return function(n,e,r=e){if(!((e=+e)>=0))throw new RangeError("invalid rx");if(!((r=+r)>=0))throw new RangeError("invalid ry");let{data:i,width:o,height:a}=n;if(!((o=Math.floor(o))>=0))throw new RangeError("invalid width");if(!((a=Math.floor(void 0!==a?a:i.length/o))>=0))throw new RangeError("invalid height");if(!o||!a||!e&&!r)return n;const u=e&&t(e),c=r&&t(r),f=i.slice();return u&&c?(p(u,f,i,o,a),p(u,i,f,o,a),p(u,f,i,o,a),g(c,i,f,o,a),g(c,f,i,o,a),g(c,i,f,o,a)):u?(p(u,i,f,o,a),p(u,f,i,o,a),p(u,i,f,o,a)):c&&(g(c,i,f,o,a),g(c,f,i,o,a),g(c,i,f,o,a)),n}}function p(t,n,e,r,i){for(let o=0,a=r*i;o<a;)t(n,e,o,o+=r,1)}function g(t,n,e,r,i){for(let o=0,a=r*i;o<r;++o)t(n,e,o,o+a,r)}function y(t){const n=Math.floor(t);if(n===t)return function(t){const n=2*t+1;return(e,r,i,o,a)=>{if(!((o-=a)>=i))return;let u=t*r[i];const c=a*t;for(let t=i,n=i+c;t<n;t+=a)u+=r[Math.min(o,t)];for(let t=i,f=o;t<=f;t+=a)u+=r[Math.min(o,t+c)],e[t]=u/n,u-=r[Math.max(i,t-c)]}}(t);const e=t-n,r=2*t+1;return(t,i,o,a,u)=>{if(!((a-=u)>=o))return;let c=n*i[o];const f=u*n,s=f+u;for(let t=o,n=o+f;t<n;t+=u)c+=i[Math.min(a,t)];for(let n=o,l=a;n<=l;n+=u)c+=i[Math.min(a,n+f)],t[n]=(c+e*(i[Math.max(o,n-s)]+i[Math.min(a,n+s)]))/r,c-=i[Math.max(o,n-f)]}}function v(t,n){let e=0;if(void 0===n)for(let n of t)null!=n&&(n=+n)>=n&&++e;else{let r=-1;for(let i of t)null!=(i=n(i,++r,t))&&(i=+i)>=i&&++e}return e}function _(t){return 0|t.length}function b(t){return!(t>0)}function m(t){return"object"!=typeof t||"length"in t?t:Array.from(t)}function x(t,n){let e,r=0,i=0,o=0;if(void 0===n)for(let n of t)null!=n&&(n=+n)>=n&&(e=n-i,i+=e/++r,o+=e*(n-i));else{let a=-1;for(let u of t)null!=(u=n(u,++a,t))&&(u=+u)>=u&&(e=u-i,i+=e/++r,o+=e*(u-i))}if(r>1)return o/(r-1)}function w(t,n){const e=x(t,n);return e?Math.sqrt(e):e}function M(t,n){let e,r;if(void 0===n)for(const n of t)null!=n&&(void 0===e?n>=n&&(e=r=n):(e>n&&(e=n),r<n&&(r=n)));else{let i=-1;for(let o of t)null!=(o=n(o,++i,t))&&(void 0===e?o>=o&&(e=r=o):(e>o&&(e=o),r<o&&(r=o)))}return[e,r]}class T{constructor(){this._partials=new Float64Array(32),this._n=0}add(t){const n=this._partials;let e=0;for(let r=0;r<this._n&&r<32;r++){const i=n[r],o=t+i,a=Math.abs(t)<Math.abs(i)?t-(o-i):i-(o-t);a&&(n[e++]=a),t=o}return n[e]=t,this._n=e+1,this}valueOf(){const t=this._partials;let n,e,r,i=this._n,o=0;if(i>0){for(o=t[--i];i>0&&(n=o,e=t[--i],o=n+e,r=e-(o-n),!r););i>0&&(r<0&&t[i-1]<0||r>0&&t[i-1]>0)&&(e=2*r,n=o+e,e==n-o&&(o=n))}return o}}class InternMap extends Map{constructor(t,n=N){if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:n}}),null!=t)for(const[n,e]of t)this.set(n,e)}get(t){return super.get(A(this,t))}has(t){return super.has(A(this,t))}set(t,n){return super.set(S(this,t),n)}delete(t){return super.delete(E(this,t))}}class InternSet extends Set{constructor(t,n=N){if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:n}}),null!=t)for(const n of t)this.add(n)}has(t){return super.has(A(
|
||
|
|
|
||
|
|
</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()}(self,(function(){return(()=>{"use strict";var t={d:(n,e)=>{for(var r in e)t.o(e,r)&&!t.o(n,r)&&Object.defineProperty(n,r,{enumerable:!0,get:e[r]})},o:(t,n)=>Object.prototype.hasOwnProperty.call(t,n)},n={};function e(){}function r(t){return null==t?e:function(){return this.querySelector(t)}}function i(t){return null==t?[]:Array.isArray(t)?t:Array.from(t)}function o(){return[]}function u(t){return null==t?o:function(){return this.querySelectorAll(t)}}function a(t){return function(){return this.matches(t)}}function l(t){return function(n){return n.matches(t)}}t.d(n,{default:()=>xr});var s=Array.prototype.find;function c(){return this.firstElementChild}var h=Array.prototype.filter;function f(){return Array.from(this.children)}function p(t){return new Array(t.length)}function d(t,n){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=n}function g(t){return function(){return t}}function v(t,n,e,r,i,o){for(var u,a=0,l=n.length,s=o.length;a<s;++a)(u=n[a])?(u.__data__=o[a],r[a]=u):e[a]=new d(t,o[a]);for(;a<l;++a)(u=n[a])&&(i[a]=u)}function y(t,n,e,r,i,o,u){var a,l,s,c=new Map,h=n.length,f=o.length,p=new Array(h);for(a=0;a<h;++a)(l=n[a])&&(p[a]=s=u.call(l,l.__data__,a,n)+"",c.has(s)?i[a]=l:c.set(s,l));for(a=0;a<f;++a)s=u.call(t,o[a],a,o)+"",(l=c.get(s))?(r[a]=l,l.__data__=o[a],c.delete(s)):e[a]=new d(t,o[a]);for(a=0;a<h;++a)(l=n[a])&&c.get(p[a])===l&&(i[a]=l)}function m(t){return t.__data__}function w(t){return"object"==typeof t&&"length"in t?t:Array.from(t)}function _(t,n){return t<n?-1:t>n?1:t>=n?0:NaN}d.prototype={constructor:d,appendChild:function(t){return this._parent.insertBefore(t,this._next)},insertBefore:function(t,n){return this._parent.insertBefore(t,n)},querySelector:function(t){return this._parent.querySelector(t)},querySelectorAll:function(t){return this._parent.querySelectorAll(t)}};var b="http://www.w3.org/1999/xhtml";const x={svg:"http://www.w3.org/2000/svg",xhtml:b,xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};function M(t){var n=t+="",e=n.indexOf(":");return e>=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),x.hasOwnProperty(n)?{space:x[n],local:t}:t}function A(t){return function(){this.removeAttribute(t)}}function N(t){return function(){this.removeAttributeNS(t.space,t.local)}}function k(t,n){return function(){this.setAttribute(t,n)}}function E(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function S(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function C(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function j(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function q(t){return function(){this.style.removeProperty(t)}}function O(t,n,e){return function(){this.style.setProperty(t,n,e)}}function P(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}function H(t,n){return t.style.getPropertyValue(n)||j(t).getComputedStyle(t,null).getPropertyValue(n)}function L(t){return function(){delete this[t]}}function T(t,n){return function(){this[t]=n}}function D(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}function B(t){return t.trim().split(/^|\s+/)}function X(t){return t.classList||new z(t)}function z(t){this._node=t,this._names=B(t.getAttribute("class")||"")}function R(t,n){for(var e=X(t),r=-1,i=n.length;++r<i;)e.add(n[r])}function I(t,n){for(var e=X(t),r=-1,i=n.length;++r<i;)e.remove(n[r])}function $(t){return function(){R(this,t)}}function V(t){return function(){I(this,t)}}function Y(t,n){return function(){(n.apply(this,arguments)?R:I)(this,t)}}function F(){this.textCon
|
||
|
|
</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.flamegraph||{},t.flamegraph.tooltip=n())}(self,(function(){return(()=>{"use strict";var t={d:(n,e)=>{for(var r in e)t.o(e,r)&&!t.o(n,r)&&Object.defineProperty(n,r,{enumerable:!0,get:e[r]})},o:(t,n)=>Object.prototype.hasOwnProperty.call(t,n),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},n={};function e(){}function r(t){return null==t?e:function(){return this.querySelector(t)}}function i(t){return null==t?[]:Array.isArray(t)?t:Array.from(t)}function o(){return[]}function u(t){return null==t?o:function(){return this.querySelectorAll(t)}}function a(t){return function(){return this.matches(t)}}function s(t){return function(n){return n.matches(t)}}t.r(n),t.d(n,{defaultFlamegraphTooltip:()=>Ae});var l=Array.prototype.find;function c(){return this.firstElementChild}var f=Array.prototype.filter;function h(){return Array.from(this.children)}function p(t){return new Array(t.length)}function d(t,n){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=n}function y(t){return function(){return t}}function v(t,n,e,r,i,o){for(var u,a=0,s=n.length,l=o.length;a<l;++a)(u=n[a])?(u.__data__=o[a],r[a]=u):e[a]=new d(t,o[a]);for(;a<s;++a)(u=n[a])&&(i[a]=u)}function g(t,n,e,r,i,o,u){var a,s,l,c=new Map,f=n.length,h=o.length,p=new Array(f);for(a=0;a<f;++a)(s=n[a])&&(p[a]=l=u.call(s,s.__data__,a,n)+"",c.has(l)?i[a]=s:c.set(l,s));for(a=0;a<h;++a)l=u.call(t,o[a],a,o)+"",(s=c.get(l))?(r[a]=s,s.__data__=o[a],c.delete(l)):e[a]=new d(t,o[a]);for(a=0;a<f;++a)(s=n[a])&&c.get(p[a])===s&&(i[a]=s)}function _(t){return t.__data__}function m(t){return"object"==typeof t&&"length"in t?t:Array.from(t)}function w(t,n){return t<n?-1:t>n?1:t>=n?0:NaN}d.prototype={constructor:d,appendChild:function(t){return this._parent.insertBefore(t,this._next)},insertBefore:function(t,n){return this._parent.insertBefore(t,n)},querySelector:function(t){return this._parent.querySelector(t)},querySelectorAll:function(t){return this._parent.querySelectorAll(t)}};var b="http://www.w3.org/1999/xhtml";const x={svg:"http://www.w3.org/2000/svg",xhtml:b,xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};function A(t){var n=t+="",e=n.indexOf(":");return e>=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),x.hasOwnProperty(n)?{space:x[n],local:t}:t}function N(t){return function(){this.removeAttribute(t)}}function k(t){return function(){this.removeAttributeNS(t.space,t.local)}}function M(t,n){return function(){this.setAttribute(t,n)}}function E(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function S(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function C(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function P(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function O(t){return function(){this.style.removeProperty(t)}}function q(t,n,e){return function(){this.style.setProperty(t,n,e)}}function j(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}function T(t,n){return t.style.getPropertyValue(n)||P(t).getComputedStyle(t,null).getPropertyValue(n)}function X(t){return function(){delete this[t]}}function R(t,n){return function(){this[t]=n}}function I(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}function L(t){return t.trim().split(/^|\s+/)}function D(t){return t.classList||new H(t)}function H(t){this._node=t,this._names=L(t.getAttribute("class")||"")}function Y(t,n){for(var e=D(t),r=-1,i=n.length;++r<i;)e.add(n[r])}function B(t,n){for(var
|
||
|
|
</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/6C9p5MAAAAHdElNRQfpDAwAKSgDSoVCAACAAElEQVR42uy9d5xlaV3n//4+zznn5nsrh85pIjkPIDBDUhFQQUCCLmBcMayru7Lqb0WYQdngIoiuyq6IAWTNLqISBmVNSBKYPD2hY+W6t24+4fn+/ji3qqu7q7qqY1X33M/r1dM9Vec+5wnnPs/nfMPnK/TRRx999NHHVQxt/B5Um5DxQR2YIEDdzaC7QTKgGZATiNzFyd+fYfRloALRDtAWsve7tnoI1zRkqzvQRx999NFHHxcKnf2t9B8uApPJoclTQF8BPB8YB/WABJgB7gX9LOjnGf2+e92936GokNT+GfEH8Z/+1a0ezjWJPtHoo48++ujjqoSb+hXE+JC/UWjd/zjgh1H9NmAsNVmcAU3AhV3V7gOEM5/WcO5TJI0vJkl7Shc+o6iD8cfB4kNknlPb6uFdM+gTjT766KOPPq46uGPvBrGIzQKZVyPyLuAmQEGaiGkBHZAIcKBWk1aOpJVHnafxQkYbd4euee8jGk5/lqT1ZxrrP9uR59W0cwRv11uQ3EGoFWDv3Yj3s1s95KsWfaLRRx999NHHVQU9djsArnGPSPlpb8Tk/gtiJgVTQ+xJMEuIRICmH0B6/xZwFlRwsa/hVMk17x4iWhjQuNomaXxRk9Yfo8knvV3fe0zGX91i4e8SbAFUIZuA34XOEDL4nVs9DVcN+kSjjz766KOPqwZ64nZwSpJ/Prb9D29WU/glTGZQxB4H7zhIlJ5s6kBkxYWicuZ5p2AAJ7hWXuPFYY0WRjWaD3DxCclM3iv+4J3iD30Sm3sUR43IdPFjMAKxgfFHQBxEr0L8J2z11Gxb9IlGH3300UcfVw30yO3pQY++GJP/HWxhFPEeBW+aZQuGqEuPtzPiNM4mGz2IIgoaB5q0ysSNEXWtQVQRv3Jc/KGvIuYrYP4Z9OuInCSIO4QWHOBi6B6B//tOeMpzkOf8w1ZP07ZCn2j00UcfffRxVUCP3UHKJXQS8f4IW342JvcA2NnTLhR1y/86vQE5x5mnICgYBWdIOgWNa2PAsPiVAGwdMUcQczfIvwJfAr4OUsWXOnECpgSJj4x+61ZP1bZCn2j00UcfffSx7aEn3gluOVNV/iMm84vYweOIf5RlS8YyBE2DKi6EaMhKI6gKGmdBR8CNgWbB1BF7EjEnQI6CPgz8P9B/ohLXqflpE74Hs0+Hfb+AeL+91dO3pegTjT766KOPPrY9lgNAgX1gPoFX2oUp3dPLKjkbgvaCQDd7h97nZHUA6fIvJBX9SsbQZAw00KRTB50Rr7SAeDXgS6D/gPJ1DFNEroWx4EXgBBn9nq2ewi1Dn2j00UcfffSxrbGKZOSA9yDBD+ENPYD481ySc+wMkoFKGt4huirOY5lw5NBoVKO5EU2aGTFBE1s5KV55ETF10BngbuDjiNyNahcFgg40BpFdr9vq6bzi6BONPvroo48+tjX02O0gWJQXg/wfvHKEKd/HmS6TC4WonnYcuiijSasiYmJMtol4YUo6VqXJapjReGlUk/o4GgdiclW8ypTYwhJiItBZ4O+BT6B6P6IxHQftCbjujxD5ra2e1iuGPtHoo48++uhjW0OPvxsyg9CZ/3HE/yW84cOIP8X5nmGCA3Wo2A0/q2FW46VhXKeELcTilatgaj1XzSmC47p5jasTmjRGUTVic1W88pR4hSUwEehx4PdQ7sQzTRIFk6TdGX7rVk/tFUGfaPTRRx999LGtocfuABdajP872NJ3YstfBdPgvImGxoCi4m36s66bhXgU8YZ6Vo0aqnOYTLP3/6QCYO2ixrVxTZrDoCImtyD+wBQ23wCpgv498EkM91CjTiX9GGEH2fEDWz3FlxV9otFHH3300ce2RfdvA0z+OnCdfWbw1k+b8tPHMPm7SNNPzg+i2gu5uICzTwNwA7jWqIbzWUx2UbzKNCbXSlW7YIVwRNUJTRrDiKjYwrx45VlsbgnMAugXgL8gV/ky7SXtZdGAFpCx1271dF8WeFvdgT766KOPPvpYDxJMotEimjSfI3F1j2p8UpB4zaJpG+Gc6a0b9iQEO40U5vGSYZLahIYnhzC5qtjSLCbXQLwYk69LJtvENWaJahOaNEc1aY6IzdXEG5jB5odArqdd+yjwOTBVRCBsoK3fRfJv2uopv+ToE40++uijjz62L5IWbmnWt5Pf8K2S3euJ+NUt7I0gJhFvcBpbWtCkPkhSH3Xh9PViMk3xho5i83VEnNjSIqawRNIqkyyNqesMkjQGcGFVjD+AzU+AeRboRxFzF55Tcgat/jYy8G+2etYv8aT1sS2hJ98JXxe4ubdEy/HOQip5e9YHBIxDdv5/W931Pvroo49LgvhrL0fDKTRpPNmUn/7XpnBjRjI77wHjNt1IKt51kdaMdRtXSKzGS8MaLe4GZ8UW5sSvTGFyrTRkVABncJ2Cus64xksjaISYXA2vfFJs/iuI/V3UfRboIC7d730fGfzurV6CSzNLW92BPk5Bj9/ei2XWXi0gltO4M0AWwUPwUSooMdAE6qhrYsxKO7KzX864jz76uPoRff0VeGMvI5764ztM4XE/Lbn9h8UbnLogt8nlhabZJ4uTmjRHQBLxylPiDcwgfpj2V1Jp85Wg0dYQqka8wpz4A1/HZP8CMX9EnDmKaaX1XCKQyTdv9dguGtttsR5z0OO3n8rMXoZTHyNDwONRbkTYDwwAWSAA8qR2jQ4wD9yN8rcgX0fUoYARZMfPbPXw+uijjz4uCNGXnw8k4LoHpHjTX0vu4C6T2fX19ODelkhfDZNmhXhhp7puRYzfwFamxZbmES8+RTjU4NoFjRZ3aNIaRmwoXumY+OXPYjK/h8rfQxKmQaIJjL4FuRwGmSuEq7fnVzFU3wEnvFMEI3FgzShwALgBuAnYA7IzjXRWi6qBpPe3uFTBzsSI0d6DO4/yZ8DHgBpxDDaD7P5PWz3cPvroo4/zRvgvT8Df/f0ksx//GcnfcLtk9x4Rf/jYVvdrY4iisafJ0ojGtR1olBWTq4k3cBxbWDqVErvsdqkPaVzdiQsLYoI6fuWrYksfxvh/TjY/RXsJohjyRWTg9Vs9uAubka3uwGMJOvWudMaTnlvEYHDsAW7t/dkBkgf10MTDhVl1nRIuKkISoM5X1JD6VpyIDRGvg8nUxOaWEK8F/DPKexGOpzcVZFffstFHH31cHdCHfwu1D+Ia94ALr5Ng4uNkd+01mZ1fR4LOVvdv8xBNRb+qYxrXJ8CZNNV1+Bgm0z7l/hFNVUZrYxovTYDzxRaPij/4SWz2f2IrXyReVIJBiJvI8Bu2emDnjX7WyRWAHu/p9DvSlGlLFjiA41uAZ4OMpVaL2FfXKZF0KriwhCapNQNRxMSICQVR1BlFPXVREcIySWuMpNHEFk+KV3weIi2UX0KobfXY++ijjz7OC0WQeAQbPSguf8v34g1dJ7Z05OoiGZC+5fld8UePii0taFwd16Qxhgvz+MNHxBZqaVCrCuKH4o8cE1tY1Ghht8b1Pbjua/EH9ogmv4HJ/BVhtYFYtPoH0KpeVSJffYvGZYKeuCMlFgKg4BwYOwz6dOAlwI3AEOo8XDenSWsI1x1UTTKIicT4DUy2JmJDMAnG64JNVe1QAWfRJFAXFkhaw+rCMoiKV5gSr3IEsf8H+DWQNoDs7Fs1+uijj+0LPfkrYMup9lXlaUZn/+plGi18EJOtmMzOuxC/u9V9vHCkgaAa10aJF3ejiYctzoo3eDK1biznFfbcLvHipMa1HYCIV3xQ/IGPYvL/C6KjqEBHwIDsvDrSYPtE4xJDj9+R6ti7XhaIGkHc9cBLgacDe4AcLsyoa1dI2kOqUREFMUEdm5sXk6ulxGJ1CtdaUdbL5YwTX5P6iMb1XagLxObmxR+
|
||
|
|
</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, "value": 1874, "children": [{"name": 83, "value": 1872, "children": [{"name": 87, "value": 1872, "children": [{"name": 198, "value": 735, "children": [{"name": 218, "value": 283, "children": [], "filename": 22, "lineno": 76, "funcname": 212, "threads": [98230], "source": [219, 214, 220, 221, 217], "opcodes": {"44": 146, "113": 16, "94": 32, "112": 51, "78": 17, "173": 18, "190": 1, "146": 1, "176": 1}}, {"name": 204, "value": 250, "children": [], "filename": 22, "lineno": 70, "funcname": 203, "threads": [98230], "source": [205, 206, 207], "opcodes": {"3": 74, "50": 62, "12": 100, "86": 3, "176": 7, "82": 4}}, {"name": 213, "value": 185, "children": [], "filename": 22, "lineno": 77, "funcname": 212, "threads": [98230], "source": [214, 215, 216, 217], "opcodes": {"44": 84, "94": 42, "112": 8, "172": 16, "167": 12, "113": 11, "78": 6, "176": 4, "103": 2}}, {"name": 208, "value": 9, "children": [], "filename": 22, "lineno": 69, "funcname": 203, "threads": [98230], "source": [209, 205, 210, 211], "opcodes": {"173": 7, "146": 2}}, {"name": 222, "value": 8, "children": [], "filename": 22, "lineno": 78, "funcname": 212, "threads": [98230], "source": [215, 221, 223], "opcodes": {"148": 7, "190": 1}}], "filename": 22, "lineno": 97, "funcname": 197, "threads": [98230], "source": [199, 200, 201, 202], "opcodes": {"52": 735}}, {"name": 134, "value": 471, "children": [{"name": 149, "value": 434, "children": [{"name": 177, "value": 222, "children": [{"name": 185, "value": 220, "children": [{"name": 191, "value": 220, "children": [], "filename": 183, "lineno": 361, "funcname": 190, "threads": [98230], "source": [192, 193, 194, 195, 196], "opcodes": {"52": 1, "160": 218, "209": 1}}], "filename": 183, "lineno": 345, "funcname": 184, "threads": [98230], "source": [186, 187, 188, 189], "opcodes": {"55": 2, "153": 218}}], "filename": 154, "lineno": 351, "funcname": 176, "threads": [98230], "source": [178, 179, 180, 181, 182], "opcodes": {"52": 2, "162": 220}}, {"name": 156, "value": 199, "children": [{"name": 164, "value": 199, "children": [{"name": 171, "value": 199, "children": [], "filename": 162, "lineno": 263, "funcname": 170, "threads": [98230], "source": [172, 173, 174, 175], "opcodes": {"52": 1, "160": 198}}], "filename": 162, "lineno": 202, "funcname": 163, "threads": [98230], "source": [165, 166, 167, 168, 169], "opcodes": {"55": 1, "153": 198}}], "filename": 154, "lineno": 234, "funcname": 155, "threads": [98230], "source": [157, 158, 159, 160, 161], "opcodes": {"52": 1, "161": 198}}], "filename": 22, "lineno": 60, "funcname": 148, "threads": [98230], "source": [150, 151, 152, 153], "opcodes": {"52": 434}}, {"name": 145, "value": 24, "children": [], "filename": 22, "lineno": 47, "funcname": 139, "threads": [98230], "source": [142, 146, 147], "opcodes": {"148": 23, "158": 1}}, {"name": 140, "value": 13, "children": [], "filename": 22, "lineno": 46, "funcname": 139, "threads": [98230], "source": [141, 142, 143, 144], "opcodes": {"52": 1, "148": 11, "158": 1}}], "filename": 22, "lineno": 91, "funcname": 133, "threads": [98230], "source": [135, 136, 137, 138], "opcodes": {"52": 471}}, {"name": 93, "value": 259, "children": [{"name": 130, "value": 130, "children": [], "filename": 22, "lineno": 37, "funcname": 114, "threads": [98230], "source": [126, 131, 132, 129], "opcodes": {"137": 69, "133": 22, "87": 6, "129": 11, "200": 8, "59": 3, "176": 1, "117": 8, "86": 2}}, {"name": 100, "value": 106, "children": [{"name": 100, "value": 106, "children": [{"name": 100, "value": 106, "children": [{"name": 100, "value": 106, "children": [{"name": 100, "value": 106, "children": [{"name": 100, "value": 106, "children": [{"name": 100, "value": 106, "children": [{"name": 100, "value": 106, "children": [{"name": 100, "value": 106, "children": [{"name": 100, "value": 106, "children": [{"name": 100, "value": 106, "children": [{"name": 100, "value": 105, "ch
|
||
|
|
|
||
|
|
// 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>
|