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
+ }
1
87
function main ( ) {
2
88
const data = { { FLAMEGRAPH_DATA } }
3
89
@@ -9,7 +95,6 @@ function main() {
9
95
}
10
96
11
97
const pythonTooltip = flamegraph . tooltip . defaultFlamegraphTooltip ( ) ;
12
-
13
98
pythonTooltip . show = function ( d , element ) {
14
99
if ( ! this . _tooltip ) {
15
100
this . _tooltip = d3
@@ -232,93 +317,12 @@ function main() {
232
317
populateStats ( data ) ;
233
318
}
234
319
235
- // Wait for libraries to load
236
- document . addEventListener ( "DOMContentLoaded" , function ( ) {
237
- main ( ) ;
238
320
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
+ }
322
326
323
327
// Python color palette - cold to hot
324
328
const pythonColors = [
@@ -334,10 +338,10 @@ const pythonColors = [
334
338
335
339
function populateStats ( data ) {
336
340
const totalSamples = data . value || 0 ;
337
-
341
+
338
342
// Collect all functions with their metrics, aggregated by function name
339
343
const functionMap = new Map ( ) ;
340
-
344
+
341
345
function collectFunctions ( node ) {
342
346
if ( node . filename && node . funcname ) {
343
347
// Calculate direct samples (this node's value minus children's values)
@@ -346,10 +350,10 @@ function populateStats(data) {
346
350
childrenValue = node . children . reduce ( ( sum , child ) => sum + child . value , 0 ) ;
347
351
}
348
352
const directSamples = Math . max ( 0 , node . value - childrenValue ) ;
349
-
353
+
350
354
// Use file:line:funcname as key to ensure uniqueness
351
355
const funcKey = `${ node . filename } :${ node . lineno || '?' } :${ node . funcname } ` ;
352
-
356
+
353
357
if ( functionMap . has ( funcKey ) ) {
354
358
const existing = functionMap . get ( funcKey ) ;
355
359
existing . directSamples += directSamples ;
@@ -371,20 +375,20 @@ function populateStats(data) {
371
375
} ) ;
372
376
}
373
377
}
374
-
378
+
375
379
if ( node . children ) {
376
380
node . children . forEach ( child => collectFunctions ( child ) ) ;
377
381
}
378
382
}
379
-
383
+
380
384
collectFunctions ( data ) ;
381
-
385
+
382
386
// Convert map to array and get top 3 hotspots
383
387
const hotSpots = Array . from ( functionMap . values ( ) )
384
388
. filter ( f => f . directPercent > 0.5 ) // At least 0.5% to be significant
385
389
. sort ( ( a , b ) => b . directPercent - a . directPercent )
386
390
. slice ( 0 , 3 ) ;
387
-
391
+
388
392
// Populate the 3 cards
389
393
for ( let i = 0 ; i < 3 ; i ++ ) {
390
394
const num = i + 1 ;
@@ -395,7 +399,7 @@ function populateStats(data) {
395
399
if ( funcDisplay . length > 35 ) {
396
400
funcDisplay = funcDisplay . substring ( 0 , 32 ) + '...' ;
397
401
}
398
-
402
+
399
403
document . getElementById ( `hotspot-file-${ num } ` ) . textContent = `${ basename } :${ hotspot . lineno } ` ;
400
404
document . getElementById ( `hotspot-func-${ num } ` ) . textContent = funcDisplay ;
401
405
document . getElementById ( `hotspot-detail-${ num } ` ) . textContent = `${ hotspot . directPercent . toFixed ( 1 ) } % samples (${ hotspot . directSamples . toLocaleString ( ) } )` ;
0 commit comments