@@ -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!
@@ -94,9 +96,10 @@ function toggleTheme() {
9496 }
9597
9698 // Re-render flamegraph with new theme colors
97- if ( window . flamegraphData && originalData ) {
98- const tooltip = createPythonTooltip ( originalData ) ;
99- const chart = createFlamegraph ( tooltip , originalData . value ) ;
99+ if ( window . flamegraphData && normalData ) {
100+ const currentData = isInverted ? invertedData : normalData ;
101+ const tooltip = createPythonTooltip ( currentData ) ;
102+ const chart = createFlamegraph ( tooltip , currentData . value ) ;
100103 renderFlamegraph ( chart , window . flamegraphData ) ;
101104 }
102105}
@@ -485,6 +488,9 @@ function createFlamegraph(tooltip, rootValue) {
485488 . tooltip ( tooltip )
486489 . inverted ( true )
487490 . setColorMapper ( function ( d ) {
491+ // Root node should be transparent
492+ if ( d . depth === 0 ) return 'transparent' ;
493+
488494 const percentage = d . data . value / rootValue ;
489495 const level = getHeatLevel ( percentage ) ;
490496 return heatColors [ level ] ;
@@ -796,16 +802,35 @@ function populateProfileSummary(data) {
796802 if ( rateEl ) rateEl . textContent = sampleRate > 0 ? formatNumber ( Math . round ( sampleRate ) ) : '--' ;
797803
798804 // Count unique functions
799- let functionCount = 0 ;
800- function countFunctions ( node ) {
805+ // Use normal (non-inverted) tree structure, but respect thread filtering
806+ const uniqueFunctions = new Set ( ) ;
807+ function collectUniqueFunctions ( node ) {
801808 if ( ! node ) return ;
802- functionCount ++ ;
803- if ( node . children ) node . children . forEach ( countFunctions ) ;
809+ const filename = resolveString ( node . filename ) || 'unknown' ;
810+ const funcname = resolveString ( node . funcname ) || resolveString ( node . name ) || 'unknown' ;
811+ const lineno = node . lineno || 0 ;
812+ const key = `${ filename } |${ lineno } |${ funcname } ` ;
813+ uniqueFunctions . add ( key ) ;
814+ if ( node . children ) node . children . forEach ( collectUniqueFunctions ) ;
815+ }
816+ // In inverted mode, use normalData (with thread filter if active)
817+ // In normal mode, use the passed data (already has thread filter applied if any)
818+ let functionCountSource ;
819+ if ( ! normalData ) {
820+ functionCountSource = data ;
821+ } else if ( isInverted ) {
822+ if ( currentThreadFilter !== 'all' ) {
823+ functionCountSource = filterDataByThread ( normalData , parseInt ( currentThreadFilter ) ) ;
824+ } else {
825+ functionCountSource = normalData ;
826+ }
827+ } else {
828+ functionCountSource = data ;
804829 }
805- countFunctions ( data ) ;
830+ collectUniqueFunctions ( functionCountSource ) ;
806831
807832 const functionsEl = document . getElementById ( 'stat-functions' ) ;
808- if ( functionsEl ) functionsEl . textContent = formatNumber ( functionCount ) ;
833+ if ( functionsEl ) functionsEl . textContent = formatNumber ( uniqueFunctions . size ) ;
809834
810835 // Efficiency bar
811836 if ( errorRate !== undefined && errorRate !== null ) {
@@ -840,14 +865,31 @@ function populateProfileSummary(data) {
840865// ============================================================================
841866
842867function populateStats ( data ) {
843- const totalSamples = data . value || 0 ;
844-
845868 // Populate profile summary
846869 populateProfileSummary ( data ) ;
847870
848871 // Populate thread statistics if available
849872 populateThreadStats ( data ) ;
850873
874+ // For hotspots: use normal (non-inverted) tree structure, but respect thread filtering.
875+ // In inverted view, the tree structure changes but the hottest functions remain the same.
876+ // However, if a thread filter is active, we need to show that thread's hotspots.
877+ let hotspotSource ;
878+ if ( ! normalData ) {
879+ hotspotSource = data ;
880+ } else if ( isInverted ) {
881+ // In inverted mode, use normalData (with thread filter if active)
882+ if ( currentThreadFilter !== 'all' ) {
883+ hotspotSource = filterDataByThread ( normalData , parseInt ( currentThreadFilter ) ) ;
884+ } else {
885+ hotspotSource = normalData ;
886+ }
887+ } else {
888+ // In normal mode, use the passed data (already has thread filter applied if any)
889+ hotspotSource = data ;
890+ }
891+ const totalSamples = hotspotSource . value || 0 ;
892+
851893 const functionMap = new Map ( ) ;
852894
853895 function collectFunctions ( node ) {
@@ -905,7 +947,7 @@ function populateStats(data) {
905947 }
906948 }
907949
908- collectFunctions ( data ) ;
950+ collectFunctions ( hotspotSource ) ;
909951
910952 const hotSpots = Array . from ( functionMap . values ( ) )
911953 . filter ( f => f . directPercent > 0.5 )
@@ -997,19 +1039,20 @@ function initThreadFilter(data) {
9971039
9981040function filterByThread ( ) {
9991041 const threadFilter = document . getElementById ( 'thread-filter' ) ;
1000- if ( ! threadFilter || ! originalData ) return ;
1042+ if ( ! threadFilter || ! normalData ) return ;
10011043
10021044 const selectedThread = threadFilter . value ;
10031045 currentThreadFilter = selectedThread ;
1046+ const baseData = isInverted ? invertedData : normalData ;
10041047
10051048 let filteredData ;
10061049 let selectedThreadId = null ;
10071050
10081051 if ( selectedThread === 'all' ) {
1009- filteredData = originalData ;
1052+ filteredData = baseData ;
10101053 } else {
10111054 selectedThreadId = parseInt ( selectedThread , 10 ) ;
1012- filteredData = filterDataByThread ( originalData , selectedThreadId ) ;
1055+ filteredData = filterDataByThread ( baseData , selectedThreadId ) ;
10131056
10141057 if ( filteredData . strings ) {
10151058 stringTable = filteredData . strings ;
@@ -1021,7 +1064,7 @@ function filterByThread() {
10211064 const chart = createFlamegraph ( tooltip , filteredData . value ) ;
10221065 renderFlamegraph ( chart , filteredData ) ;
10231066
1024- populateThreadStats ( originalData , selectedThreadId ) ;
1067+ populateThreadStats ( baseData , selectedThreadId ) ;
10251068}
10261069
10271070function filterDataByThread ( data , threadId ) {
@@ -1089,6 +1132,137 @@ function exportSVG() {
10891132 URL . revokeObjectURL ( url ) ;
10901133}
10911134
1135+ // ============================================================================
1136+ // Inverted Flamegraph
1137+ // ============================================================================
1138+
1139+ // Example: "file.py|10|foo" or "~|0|<GC>" for special frames
1140+ function getInvertNodeKey ( node ) {
1141+ return `${ node . filename || '~' } |${ node . lineno || 0 } |${ node . funcname || node . name } ` ;
1142+ }
1143+
1144+ function accumulateInvertedNode ( parent , stackFrame , leaf ) {
1145+ const key = getInvertNodeKey ( stackFrame ) ;
1146+
1147+ if ( ! parent . children [ key ] ) {
1148+ parent . children [ key ] = {
1149+ name : stackFrame . name ,
1150+ value : 0 ,
1151+ children : { } ,
1152+ filename : stackFrame . filename ,
1153+ lineno : stackFrame . lineno ,
1154+ funcname : stackFrame . funcname ,
1155+ source : stackFrame . source ,
1156+ threads : new Set ( )
1157+ } ;
1158+ }
1159+
1160+ const node = parent . children [ key ] ;
1161+ node . value += leaf . value ;
1162+ if ( leaf . threads ) {
1163+ leaf . threads . forEach ( t => node . threads . add ( t ) ) ;
1164+ }
1165+
1166+ return node ;
1167+ }
1168+
1169+ function processLeaf ( invertedRoot , path , leafNode ) {
1170+ if ( ! path || path . length === 0 ) {
1171+ return ;
1172+ }
1173+
1174+ let invertedParent = accumulateInvertedNode ( invertedRoot , leafNode , leafNode ) ;
1175+
1176+ // Walk backwards through the call stack
1177+ for ( let i = path . length - 2 ; i >= 0 ; i -- ) {
1178+ invertedParent = accumulateInvertedNode ( invertedParent , path [ i ] , leafNode ) ;
1179+ }
1180+ }
1181+
1182+ function traverseInvert ( path , currentNode , invertedRoot ) {
1183+ const children = currentNode . children || [ ] ;
1184+ const childThreads = new Set ( children . flatMap ( c => c . threads || [ ] ) ) ;
1185+ const selfThreads = ( currentNode . threads || [ ] ) . filter ( t => ! childThreads . has ( t ) ) ;
1186+
1187+ if ( selfThreads . length > 0 ) {
1188+ processLeaf ( invertedRoot , path , { ...currentNode , threads : selfThreads } ) ;
1189+ }
1190+
1191+ children . forEach ( child => traverseInvert ( path . concat ( [ child ] ) , child , invertedRoot ) ) ;
1192+ }
1193+
1194+ function convertInvertDictToArray ( node ) {
1195+ if ( node . threads instanceof Set ) {
1196+ node . threads = Array . from ( node . threads ) . sort ( ( a , b ) => a - b ) ;
1197+ }
1198+
1199+ const children = node . children ;
1200+ if ( children && typeof children === 'object' && ! Array . isArray ( children ) ) {
1201+ node . children = Object . values ( children ) ;
1202+ node . children . sort ( ( a , b ) => b . value - a . value || a . name . localeCompare ( b . name ) ) ;
1203+ node . children . forEach ( convertInvertDictToArray ) ;
1204+ }
1205+ return node ;
1206+ }
1207+
1208+ function generateInvertedFlamegraph ( data ) {
1209+ const invertedRoot = {
1210+ name : data . name ,
1211+ value : data . value ,
1212+ children : { } ,
1213+ stats : data . stats ,
1214+ threads : data . threads
1215+ } ;
1216+
1217+ const children = data . children || [ ] ;
1218+ if ( children . length === 0 ) {
1219+ // Single-frame tree: the root is its own leaf
1220+ processLeaf ( invertedRoot , [ data ] , data ) ;
1221+ } else {
1222+ children . forEach ( child => traverseInvert ( [ child ] , child , invertedRoot ) ) ;
1223+ }
1224+
1225+ convertInvertDictToArray ( invertedRoot ) ;
1226+ return invertedRoot ;
1227+ }
1228+
1229+ function updateToggleUI ( toggleId , isOn ) {
1230+ const toggle = document . getElementById ( toggleId ) ;
1231+ if ( toggle ) {
1232+ const track = toggle . querySelector ( '.toggle-track' ) ;
1233+ const labels = toggle . querySelectorAll ( '.toggle-label' ) ;
1234+ if ( isOn ) {
1235+ track . classList . add ( 'on' ) ;
1236+ labels [ 0 ] . classList . remove ( 'active' ) ;
1237+ labels [ 1 ] . classList . add ( 'active' ) ;
1238+ } else {
1239+ track . classList . remove ( 'on' ) ;
1240+ labels [ 0 ] . classList . add ( 'active' ) ;
1241+ labels [ 1 ] . classList . remove ( 'active' ) ;
1242+ }
1243+ }
1244+ }
1245+
1246+ function toggleInvert ( ) {
1247+ isInverted = ! isInverted ;
1248+ updateToggleUI ( 'toggle-invert' , isInverted ) ;
1249+
1250+ // Build inverted data on first use
1251+ if ( isInverted && ! invertedData ) {
1252+ invertedData = generateInvertedFlamegraph ( normalData ) ;
1253+ }
1254+
1255+ let dataToRender = isInverted ? invertedData : normalData ;
1256+
1257+ if ( currentThreadFilter !== 'all' ) {
1258+ dataToRender = filterDataByThread ( dataToRender , parseInt ( currentThreadFilter ) ) ;
1259+ }
1260+
1261+ const tooltip = createPythonTooltip ( dataToRender ) ;
1262+ const chart = createFlamegraph ( tooltip , dataToRender . value ) ;
1263+ renderFlamegraph ( chart , dataToRender ) ;
1264+ }
1265+
10921266// ============================================================================
10931267// Initialization
10941268// ============================================================================
@@ -1098,24 +1272,32 @@ function initFlamegraph() {
10981272 restoreUIState ( ) ;
10991273 setupLogos ( ) ;
11001274
1101- let processedData = EMBEDDED_DATA ;
11021275 if ( EMBEDDED_DATA . strings ) {
11031276 stringTable = EMBEDDED_DATA . strings ;
1104- processedData = resolveStringIndices ( EMBEDDED_DATA ) ;
1277+ normalData = resolveStringIndices ( EMBEDDED_DATA ) ;
1278+ } else {
1279+ normalData = EMBEDDED_DATA ;
11051280 }
11061281
11071282 // Initialize opcode mapping from embedded data
11081283 initOpcodeMapping ( EMBEDDED_DATA ) ;
11091284
1110- originalData = processedData ;
1111- initThreadFilter ( processedData ) ;
1285+ // Inverted data will be built on first toggle
1286+ invertedData = null ;
1287+
1288+ initThreadFilter ( normalData ) ;
11121289
1113- const tooltip = createPythonTooltip ( processedData ) ;
1114- const chart = createFlamegraph ( tooltip , processedData . value ) ;
1115- renderFlamegraph ( chart , processedData ) ;
1290+ const tooltip = createPythonTooltip ( normalData ) ;
1291+ const chart = createFlamegraph ( tooltip , normalData . value ) ;
1292+ renderFlamegraph ( chart , normalData ) ;
11161293 initSearchHandlers ( ) ;
11171294 initSidebarResize ( ) ;
11181295 handleResize ( ) ;
1296+
1297+ const toggleInvertBtn = document . getElementById ( 'toggle-invert' ) ;
1298+ if ( toggleInvertBtn ) {
1299+ toggleInvertBtn . addEventListener ( 'click' , toggleInvert ) ;
1300+ }
11191301}
11201302
11211303if ( document . readyState === "loading" ) {
0 commit comments