1+ function initFlamegraphUI ( ) {
2+ main ( ) ;
3+
4+ const infoBtn = document . getElementById ( "show-info-btn" ) ;
5+ const infoPanel = document . getElementById ( "info-panel" ) ;
6+ const closeBtn = document . getElementById ( "close-info-btn" ) ;
7+ const searchInput = document . getElementById ( "search-input" ) ;
8+
9+ if ( infoBtn && infoPanel ) {
10+ infoBtn . addEventListener ( "click" , function ( ) {
11+ const isOpen = infoPanel . style . display === "block" ;
12+ infoPanel . style . display = isOpen ? "none" : "block" ;
13+ } ) ;
14+ }
15+ if ( closeBtn && infoPanel ) {
16+ closeBtn . addEventListener ( "click" , function ( ) {
17+ infoPanel . style . display = "none" ;
18+ } ) ;
19+ }
20+
21+ // Add search functionality - wait for chart to be ready
22+ if ( searchInput ) {
23+ let searchTimeout ;
24+
25+ function performSearch ( ) {
26+ const searchTerm = searchInput . value . trim ( ) ;
27+
28+ // Clear previous search highlighting
29+ d3 . selectAll ( "#chart rect" )
30+ . style ( "stroke" , null )
31+ . style ( "stroke-width" , null )
32+ . style ( "opacity" , null ) ;
33+
34+ if ( searchTerm && searchTerm . length > 0 ) {
35+ // First, dim all rectangles
36+ d3 . selectAll ( "#chart rect" )
37+ . style ( "opacity" , 0.3 ) ;
38+
39+ // Then highlight and restore opacity for matching nodes
40+ let matchCount = 0 ;
41+ d3 . selectAll ( "#chart rect" )
42+ . each ( function ( d ) {
43+ if ( d && d . data ) {
44+ const name = d . data . name || "" ;
45+ const funcname = d . data . funcname || "" ;
46+ const filename = d . data . filename || "" ;
47+
48+ const matches = name . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ||
49+ funcname . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ||
50+ filename . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ;
51+
52+ if ( matches ) {
53+ matchCount ++ ;
54+ d3 . select ( this )
55+ . style ( "opacity" , 1 )
56+ . style ( "stroke" , "#ff6b35" )
57+ . style ( "stroke-width" , "2px" )
58+ . style ( "stroke-dasharray" , "3,3" ) ;
59+ }
60+ }
61+ } ) ;
62+
63+ // Update search input style based on results
64+ if ( matchCount > 0 ) {
65+ searchInput . style . borderColor = "rgba(40, 167, 69, 0.8)" ;
66+ searchInput . style . boxShadow = "0 6px 20px rgba(40, 167, 69, 0.2)" ;
67+ } else {
68+ searchInput . style . borderColor = "rgba(220, 53, 69, 0.8)" ;
69+ searchInput . style . boxShadow = "0 6px 20px rgba(220, 53, 69, 0.2)" ;
70+ }
71+ } else {
72+ // Reset search input style
73+ searchInput . style . borderColor = "rgba(255, 255, 255, 0.2)" ;
74+ searchInput . style . boxShadow = "0 4px 12px rgba(0, 0, 0, 0.1)" ;
75+ }
76+ }
77+
78+ searchInput . addEventListener ( "input" , function ( ) {
79+ clearTimeout ( searchTimeout ) ;
80+ searchTimeout = setTimeout ( performSearch , 150 ) ;
81+ } ) ;
82+
83+ // Make search function globally accessible
84+ window . performSearch = performSearch ;
85+ }
86+ }
187function main ( ) {
288 const data = { { FLAMEGRAPH_DATA } }
389
@@ -9,7 +95,6 @@ function main() {
995 }
1096
1197 const pythonTooltip = flamegraph . tooltip . defaultFlamegraphTooltip ( ) ;
12-
1398 pythonTooltip . show = function ( d , element ) {
1499 if ( ! this . _tooltip ) {
15100 this . _tooltip = d3
@@ -232,93 +317,12 @@ function main() {
232317 populateStats ( data ) ;
233318}
234319
235- // Wait for libraries to load
236- document . addEventListener ( "DOMContentLoaded" , function ( ) {
237- main ( ) ;
238320
239- const infoBtn = document . getElementById ( "show-info-btn" ) ;
240- const infoPanel = document . getElementById ( "info-panel" ) ;
241- const closeBtn = document . getElementById ( "close-info-btn" ) ;
242- const searchInput = document . getElementById ( "search-input" ) ;
243-
244- if ( infoBtn && infoPanel ) {
245- infoBtn . addEventListener ( "click" , function ( ) {
246- const isOpen = infoPanel . style . display === "block" ;
247- infoPanel . style . display = isOpen ? "none" : "block" ;
248- } ) ;
249- }
250- if ( closeBtn && infoPanel ) {
251- closeBtn . addEventListener ( "click" , function ( ) {
252- infoPanel . style . display = "none" ;
253- } ) ;
254- }
255-
256- // Add search functionality - wait for chart to be ready
257- if ( searchInput ) {
258- let searchTimeout ;
259-
260- function performSearch ( ) {
261- const searchTerm = searchInput . value . trim ( ) ;
262-
263- // Clear previous search highlighting
264- d3 . selectAll ( "#chart rect" )
265- . style ( "stroke" , null )
266- . style ( "stroke-width" , null )
267- . style ( "opacity" , null ) ;
268-
269- if ( searchTerm && searchTerm . length > 0 ) {
270- // First, dim all rectangles
271- d3 . selectAll ( "#chart rect" )
272- . style ( "opacity" , 0.3 ) ;
273-
274- // Then highlight and restore opacity for matching nodes
275- let matchCount = 0 ;
276- d3 . selectAll ( "#chart rect" )
277- . each ( function ( d ) {
278- if ( d && d . data ) {
279- const name = d . data . name || "" ;
280- const funcname = d . data . funcname || "" ;
281- const filename = d . data . filename || "" ;
282-
283- const matches = name . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ||
284- funcname . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ||
285- filename . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ;
286-
287- if ( matches ) {
288- matchCount ++ ;
289- d3 . select ( this )
290- . style ( "opacity" , 1 )
291- . style ( "stroke" , "#ff6b35" )
292- . style ( "stroke-width" , "2px" )
293- . style ( "stroke-dasharray" , "3,3" ) ;
294- }
295- }
296- } ) ;
297-
298- // Update search input style based on results
299- if ( matchCount > 0 ) {
300- searchInput . style . borderColor = "rgba(40, 167, 69, 0.8)" ;
301- searchInput . style . boxShadow = "0 6px 20px rgba(40, 167, 69, 0.2)" ;
302- } else {
303- searchInput . style . borderColor = "rgba(220, 53, 69, 0.8)" ;
304- searchInput . style . boxShadow = "0 6px 20px rgba(220, 53, 69, 0.2)" ;
305- }
306- } else {
307- // Reset search input style
308- searchInput . style . borderColor = "rgba(255, 255, 255, 0.2)" ;
309- searchInput . style . boxShadow = "0 4px 12px rgba(0, 0, 0, 0.1)" ;
310- }
311- }
312-
313- searchInput . addEventListener ( "input" , function ( ) {
314- clearTimeout ( searchTimeout ) ;
315- searchTimeout = setTimeout ( performSearch , 150 ) ;
316- } ) ;
317-
318- // Make search function globally accessible
319- window . performSearch = performSearch ;
320- }
321- } ) ;
321+ if ( document . readyState === "loading" ) {
322+ document . addEventListener ( "DOMContentLoaded" , initFlamegraphUI ) ;
323+ } else {
324+ initFlamegraphUI ( ) ;
325+ }
322326
323327// Python color palette - cold to hot
324328const pythonColors = [
@@ -334,10 +338,10 @@ const pythonColors = [
334338
335339function populateStats ( data ) {
336340 const totalSamples = data . value || 0 ;
337-
341+
338342 // Collect all functions with their metrics, aggregated by function name
339343 const functionMap = new Map ( ) ;
340-
344+
341345 function collectFunctions ( node ) {
342346 if ( node . filename && node . funcname ) {
343347 // Calculate direct samples (this node's value minus children's values)
@@ -346,10 +350,10 @@ function populateStats(data) {
346350 childrenValue = node . children . reduce ( ( sum , child ) => sum + child . value , 0 ) ;
347351 }
348352 const directSamples = Math . max ( 0 , node . value - childrenValue ) ;
349-
353+
350354 // Use file:line:funcname as key to ensure uniqueness
351355 const funcKey = `${ node . filename } :${ node . lineno || '?' } :${ node . funcname } ` ;
352-
356+
353357 if ( functionMap . has ( funcKey ) ) {
354358 const existing = functionMap . get ( funcKey ) ;
355359 existing . directSamples += directSamples ;
@@ -371,20 +375,20 @@ function populateStats(data) {
371375 } ) ;
372376 }
373377 }
374-
378+
375379 if ( node . children ) {
376380 node . children . forEach ( child => collectFunctions ( child ) ) ;
377381 }
378382 }
379-
383+
380384 collectFunctions ( data ) ;
381-
385+
382386 // Convert map to array and get top 3 hotspots
383387 const hotSpots = Array . from ( functionMap . values ( ) )
384388 . filter ( f => f . directPercent > 0.5 ) // At least 0.5% to be significant
385389 . sort ( ( a , b ) => b . directPercent - a . directPercent )
386390 . slice ( 0 , 3 ) ;
387-
391+
388392 // Populate the 3 cards
389393 for ( let i = 0 ; i < 3 ; i ++ ) {
390394 const num = i + 1 ;
@@ -395,7 +399,7 @@ function populateStats(data) {
395399 if ( funcDisplay . length > 35 ) {
396400 funcDisplay = funcDisplay . substring ( 0 , 32 ) + '...' ;
397401 }
398-
402+
399403 document . getElementById ( `hotspot-file-${ num } ` ) . textContent = `${ basename } :${ hotspot . lineno } ` ;
400404 document . getElementById ( `hotspot-func-${ num } ` ) . textContent = funcDisplay ;
401405 document . getElementById ( `hotspot-detail-${ num } ` ) . textContent = `${ hotspot . directPercent . toFixed ( 1 ) } % samples (${ hotspot . directSamples . toLocaleString ( ) } )` ;
0 commit comments