@@ -2,8 +2,10 @@ const EMBEDDED_DATA = {{FLAMEGRAPH_DATA}};
22
33// Global string table for resolving string indices
44let stringTable = [ ] ;
5- let originalData = null ;
5+ let normalData = null ;
6+ let invertedData = null ;
67let currentThreadFilter = 'all' ;
8+ let isInverted = false ;
79
810// Heat colors are now defined in CSS variables (--heat-1 through --heat-8)
911// and automatically switch with theme changes - no JS color arrays needed!
@@ -68,9 +70,10 @@ function toggleTheme() {
6870 }
6971
7072 // Re-render flamegraph with new theme colors
71- if ( window . flamegraphData && originalData ) {
72- const tooltip = createPythonTooltip ( originalData ) ;
73- const chart = createFlamegraph ( tooltip , originalData . value ) ;
73+ if ( window . flamegraphData && normalData ) {
74+ const currentData = isInverted ? invertedData : normalData ;
75+ const tooltip = createPythonTooltip ( currentData ) ;
76+ const chart = createFlamegraph ( tooltip , currentData . value ) ;
7477 renderFlamegraph ( chart , window . flamegraphData ) ;
7578 }
7679}
@@ -380,6 +383,9 @@ function createFlamegraph(tooltip, rootValue) {
380383 . tooltip ( tooltip )
381384 . inverted ( true )
382385 . setColorMapper ( function ( d ) {
386+ // Root node should be transparent
387+ if ( d . depth === 0 ) return 'transparent' ;
388+
383389 const percentage = d . data . value / rootValue ;
384390 const level = getHeatLevel ( percentage ) ;
385391 return heatColors [ level ] ;
@@ -888,19 +894,20 @@ function initThreadFilter(data) {
888894
889895function filterByThread ( ) {
890896 const threadFilter = document . getElementById ( 'thread-filter' ) ;
891- if ( ! threadFilter || ! originalData ) return ;
897+ if ( ! threadFilter || ! normalData ) return ;
892898
893899 const selectedThread = threadFilter . value ;
894900 currentThreadFilter = selectedThread ;
901+ const baseData = isInverted ? invertedData : normalData ;
895902
896903 let filteredData ;
897904 let selectedThreadId = null ;
898905
899906 if ( selectedThread === 'all' ) {
900- filteredData = originalData ;
907+ filteredData = baseData ;
901908 } else {
902909 selectedThreadId = parseInt ( selectedThread , 10 ) ;
903- filteredData = filterDataByThread ( originalData , selectedThreadId ) ;
910+ filteredData = filterDataByThread ( baseData , selectedThreadId ) ;
904911
905912 if ( filteredData . strings ) {
906913 stringTable = filteredData . strings ;
@@ -912,7 +919,7 @@ function filterByThread() {
912919 const chart = createFlamegraph ( tooltip , filteredData . value ) ;
913920 renderFlamegraph ( chart , filteredData ) ;
914921
915- populateThreadStats ( originalData , selectedThreadId ) ;
922+ populateThreadStats ( baseData , selectedThreadId ) ;
916923}
917924
918925function filterDataByThread ( data , threadId ) {
@@ -980,6 +987,131 @@ function exportSVG() {
980987 URL . revokeObjectURL ( url ) ;
981988}
982989
990+ // ============================================================================
991+ // Inverted Flamegraph
992+ // ============================================================================
993+
994+ // Example: "file.py|10|foo" or "~|0|<GC>" for special frames
995+ function getInvertNodeKey ( node ) {
996+ return `${ node . filename || '~' } |${ node . lineno || 0 } |${ node . funcname || node . name } ` ;
997+ }
998+
999+ function accumulateInvertedNode ( parent , stackFrame , leaf ) {
1000+ const key = getInvertNodeKey ( stackFrame ) ;
1001+
1002+ if ( ! parent . children [ key ] ) {
1003+ parent . children [ key ] = {
1004+ name : stackFrame . name ,
1005+ value : 0 ,
1006+ children : { } ,
1007+ filename : stackFrame . filename ,
1008+ lineno : stackFrame . lineno ,
1009+ funcname : stackFrame . funcname ,
1010+ source : stackFrame . source ,
1011+ threads : new Set ( )
1012+ } ;
1013+ }
1014+
1015+ const node = parent . children [ key ] ;
1016+ node . value += leaf . value ;
1017+ if ( leaf . threads ) {
1018+ leaf . threads . forEach ( t => node . threads . add ( t ) ) ;
1019+ }
1020+
1021+ return node ;
1022+ }
1023+
1024+ function traverseInvert ( path , currentNode , invertedRoot ) {
1025+ if ( ! currentNode . children || currentNode . children . length === 0 ) {
1026+ // We've reached a leaf node
1027+ if ( ! path || path . length === 0 ) {
1028+ return ;
1029+ }
1030+
1031+ let invertedParent = accumulateInvertedNode ( invertedRoot , currentNode , currentNode ) ;
1032+
1033+ // Walk backwards through the call stack
1034+ for ( let i = path . length - 2 ; i >= 0 ; i -- ) {
1035+ invertedParent = accumulateInvertedNode ( invertedParent , path [ i ] , currentNode ) ;
1036+ }
1037+ } else {
1038+ // Not a leaf, continue traversing down the tree
1039+ for ( const child of currentNode . children ) {
1040+ traverseInvert ( path . concat ( [ child ] ) , child , invertedRoot ) ;
1041+ }
1042+ }
1043+ }
1044+
1045+ function convertInvertDictToArray ( node ) {
1046+ if ( node . threads instanceof Set ) {
1047+ node . threads = Array . from ( node . threads ) . sort ( ( a , b ) => a - b ) ;
1048+ }
1049+
1050+ const children = node . children ;
1051+ if ( children && typeof children === 'object' && ! Array . isArray ( children ) ) {
1052+ node . children = Object . values ( children ) ;
1053+ node . children . sort ( ( a , b ) => b . value - a . value || a . name . localeCompare ( b . name ) ) ;
1054+ node . children . forEach ( convertInvertDictToArray ) ;
1055+ }
1056+ return node ;
1057+ }
1058+
1059+ function generateInvertedFlamegraph ( data ) {
1060+ const invertedRoot = {
1061+ name : data . name ,
1062+ value : data . value ,
1063+ children : { } ,
1064+ stats : data . stats ,
1065+ threads : data . threads
1066+ } ;
1067+
1068+ data . children ?. forEach ( child => {
1069+ traverseInvert ( [ child ] , child , invertedRoot ) ;
1070+ } ) ;
1071+
1072+ // Convert children dictionaries to arrays for rendering
1073+ convertInvertDictToArray ( invertedRoot ) ;
1074+
1075+ return invertedRoot ;
1076+ }
1077+
1078+ function updateToggleUI ( toggleId , isOn ) {
1079+ const toggle = document . getElementById ( toggleId ) ;
1080+ if ( toggle ) {
1081+ const track = toggle . querySelector ( '.toggle-track' ) ;
1082+ const labels = toggle . querySelectorAll ( '.toggle-label' ) ;
1083+ if ( isOn ) {
1084+ track . classList . add ( 'on' ) ;
1085+ labels [ 0 ] . classList . remove ( 'active' ) ;
1086+ labels [ 1 ] . classList . add ( 'active' ) ;
1087+ } else {
1088+ track . classList . remove ( 'on' ) ;
1089+ labels [ 0 ] . classList . add ( 'active' ) ;
1090+ labels [ 1 ] . classList . remove ( 'active' ) ;
1091+ }
1092+ }
1093+ }
1094+
1095+ function toggleInvert ( ) {
1096+ isInverted = ! isInverted ;
1097+ updateToggleUI ( 'toggle-invert' , isInverted ) ;
1098+
1099+ // Build inverted data on first use
1100+ if ( isInverted && ! invertedData ) {
1101+ invertedData = generateInvertedFlamegraph ( normalData ) ;
1102+ }
1103+
1104+ let dataToRender = isInverted ? invertedData : normalData ;
1105+
1106+ if ( currentThreadFilter !== 'all' ) {
1107+ dataToRender = filterDataByThread ( dataToRender , parseInt ( currentThreadFilter ) ) ;
1108+ }
1109+
1110+ const tooltip = createPythonTooltip ( dataToRender ) ;
1111+ const chart = createFlamegraph ( tooltip , dataToRender . value ) ;
1112+ renderFlamegraph ( chart , dataToRender ) ;
1113+ }
1114+
9831115// ============================================================================
9841116// Initialization
9851117// ============================================================================
@@ -988,21 +1120,29 @@ function initFlamegraph() {
9881120 ensureLibraryLoaded ( ) ;
9891121 restoreUIState ( ) ;
9901122
991- let processedData = EMBEDDED_DATA ;
9921123 if ( EMBEDDED_DATA . strings ) {
9931124 stringTable = EMBEDDED_DATA . strings ;
994- processedData = resolveStringIndices ( EMBEDDED_DATA ) ;
1125+ normalData = resolveStringIndices ( EMBEDDED_DATA ) ;
1126+ } else {
1127+ normalData = EMBEDDED_DATA ;
9951128 }
9961129
997- originalData = processedData ;
998- initThreadFilter ( processedData ) ;
1130+ // Inverted data will be built on first toggle
1131+ invertedData = null ;
9991132
1000- const tooltip = createPythonTooltip ( processedData ) ;
1001- const chart = createFlamegraph ( tooltip , processedData . value ) ;
1002- renderFlamegraph ( chart , processedData ) ;
1133+ initThreadFilter ( normalData ) ;
1134+
1135+ const tooltip = createPythonTooltip ( normalData ) ;
1136+ const chart = createFlamegraph ( tooltip , normalData . value ) ;
1137+ renderFlamegraph ( chart , normalData ) ;
10031138 initSearchHandlers ( ) ;
10041139 initSidebarResize ( ) ;
10051140 handleResize ( ) ;
1141+
1142+ const toggleInvertBtn = document . getElementById ( 'toggle-invert' ) ;
1143+ if ( toggleInvertBtn ) {
1144+ toggleInvertBtn . addEventListener ( 'click' , toggleInvert ) ;
1145+ }
10061146}
10071147
10081148if ( document . readyState === "loading" ) {
0 commit comments