@@ -211,6 +211,9 @@ function main() {
211211
212212 // Make chart globally accessible for controls
213213 window . flamegraphChart = chart ;
214+
215+ // Populate stats cards
216+ populateStats ( data ) ;
214217}
215218
216219// Wait for libraries to load
@@ -220,6 +223,8 @@ document.addEventListener("DOMContentLoaded", function () {
220223 const infoBtn = document . getElementById ( "show-info-btn" ) ;
221224 const infoPanel = document . getElementById ( "info-panel" ) ;
222225 const closeBtn = document . getElementById ( "close-info-btn" ) ;
226+ const searchInput = document . getElementById ( "search-input" ) ;
227+
223228 if ( infoBtn && infoPanel ) {
224229 infoBtn . addEventListener ( "click" , function ( ) {
225230 const isOpen = infoPanel . style . display === "block" ;
@@ -231,6 +236,72 @@ document.addEventListener("DOMContentLoaded", function () {
231236 infoPanel . style . display = "none" ;
232237 } ) ;
233238 }
239+
240+ // Add search functionality - wait for chart to be ready
241+ if ( searchInput ) {
242+ let searchTimeout ;
243+
244+ function performSearch ( ) {
245+ const searchTerm = searchInput . value . trim ( ) ;
246+
247+ // Clear previous search highlighting
248+ d3 . selectAll ( "#chart rect" )
249+ . style ( "stroke" , null )
250+ . style ( "stroke-width" , null )
251+ . style ( "opacity" , null ) ;
252+
253+ if ( searchTerm && searchTerm . length > 0 ) {
254+ // First, dim all rectangles
255+ d3 . selectAll ( "#chart rect" )
256+ . style ( "opacity" , 0.3 ) ;
257+
258+ // Then highlight and restore opacity for matching nodes
259+ let matchCount = 0 ;
260+ d3 . selectAll ( "#chart rect" )
261+ . each ( function ( d ) {
262+ if ( d && d . data ) {
263+ const name = d . data . name || "" ;
264+ const funcname = d . data . funcname || "" ;
265+ const filename = d . data . filename || "" ;
266+
267+ const matches = name . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ||
268+ funcname . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ||
269+ filename . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ;
270+
271+ if ( matches ) {
272+ matchCount ++ ;
273+ d3 . select ( this )
274+ . style ( "opacity" , 1 )
275+ . style ( "stroke" , "#ff6b35" )
276+ . style ( "stroke-width" , "2px" )
277+ . style ( "stroke-dasharray" , "3,3" ) ;
278+ }
279+ }
280+ } ) ;
281+
282+ // Update search input style based on results
283+ if ( matchCount > 0 ) {
284+ searchInput . style . borderColor = "#28a745" ;
285+ searchInput . style . boxShadow = "0 0 0 3px rgba(40, 167, 69, 0.1)" ;
286+ } else {
287+ searchInput . style . borderColor = "#dc3545" ;
288+ searchInput . style . boxShadow = "0 0 0 3px rgba(220, 53, 69, 0.1)" ;
289+ }
290+ } else {
291+ // Reset search input style
292+ searchInput . style . borderColor = "#e9ecef" ;
293+ searchInput . style . boxShadow = "0 2px 4px rgba(0, 0, 0, 0.06)" ;
294+ }
295+ }
296+
297+ searchInput . addEventListener ( "input" , function ( ) {
298+ clearTimeout ( searchTimeout ) ;
299+ searchTimeout = setTimeout ( performSearch , 150 ) ;
300+ } ) ;
301+
302+ // Make search function globally accessible
303+ window . performSearch = performSearch ;
304+ }
234305} ) ;
235306
236307// Python color palette - cold to hot
@@ -245,6 +316,81 @@ const pythonColors = [
245316 "#3776ab" , // Hottest - Python blue (≥60%)
246317] ;
247318
319+ function populateStats ( data ) {
320+ const totalSamples = data . value || 0 ;
321+
322+ // Collect all functions with their metrics, aggregated by function name
323+ const functionMap = new Map ( ) ;
324+
325+ function collectFunctions ( node ) {
326+ if ( node . filename && node . funcname ) {
327+ // Calculate direct samples (this node's value minus children's values)
328+ let childrenValue = 0 ;
329+ if ( node . children ) {
330+ childrenValue = node . children . reduce ( ( sum , child ) => sum + child . value , 0 ) ;
331+ }
332+ const directSamples = Math . max ( 0 , node . value - childrenValue ) ;
333+
334+ // Use file:line:funcname as key to ensure uniqueness
335+ const funcKey = `${ node . filename } :${ node . lineno || '?' } :${ node . funcname } ` ;
336+
337+ if ( functionMap . has ( funcKey ) ) {
338+ const existing = functionMap . get ( funcKey ) ;
339+ existing . directSamples += directSamples ;
340+ existing . directPercent = ( existing . directSamples / totalSamples ) * 100 ;
341+ // Keep the most representative file/line (the one with more samples)
342+ if ( directSamples > existing . maxSingleSamples ) {
343+ existing . filename = node . filename ;
344+ existing . lineno = node . lineno || '?' ;
345+ existing . maxSingleSamples = directSamples ;
346+ }
347+ } else {
348+ functionMap . set ( funcKey , {
349+ filename : node . filename ,
350+ lineno : node . lineno || '?' ,
351+ funcname : node . funcname ,
352+ directSamples,
353+ directPercent : ( directSamples / totalSamples ) * 100 ,
354+ maxSingleSamples : directSamples
355+ } ) ;
356+ }
357+ }
358+
359+ if ( node . children ) {
360+ node . children . forEach ( child => collectFunctions ( child ) ) ;
361+ }
362+ }
363+
364+ collectFunctions ( data ) ;
365+
366+ // Convert map to array and get top 3 hotspots
367+ const hotSpots = Array . from ( functionMap . values ( ) )
368+ . filter ( f => f . directPercent > 0.5 ) // At least 0.5% to be significant
369+ . sort ( ( a , b ) => b . directPercent - a . directPercent )
370+ . slice ( 0 , 3 ) ;
371+
372+ // Populate the 3 cards
373+ for ( let i = 0 ; i < 3 ; i ++ ) {
374+ const num = i + 1 ;
375+ if ( i < hotSpots . length ) {
376+ const hotspot = hotSpots [ i ] ;
377+ const basename = hotspot . filename . split ( '/' ) . pop ( ) ;
378+ let funcDisplay = hotspot . funcname ;
379+ if ( funcDisplay . length > 35 ) {
380+ funcDisplay = funcDisplay . substring ( 0 , 32 ) + '...' ;
381+ }
382+
383+ document . getElementById ( `hotspot-file-${ num } ` ) . textContent = `${ basename } :${ hotspot . lineno } ` ;
384+ document . getElementById ( `hotspot-func-${ num } ` ) . textContent = funcDisplay ;
385+ document . getElementById ( `hotspot-detail-${ num } ` ) . textContent = `${ hotspot . directPercent . toFixed ( 1 ) } % samples (${ hotspot . directSamples . toLocaleString ( ) } )` ;
386+ } else {
387+ document . getElementById ( `hotspot-file-${ num } ` ) . textContent = '--' ;
388+ document . getElementById ( `hotspot-func-${ num } ` ) . textContent = '--' ;
389+ document . getElementById ( `hotspot-detail-${ num } ` ) . textContent = '--' ;
390+ }
391+ }
392+ }
393+
248394// Control functions
249395function resetZoom ( ) {
250396 if ( window . flamegraphChart ) {
@@ -274,6 +420,16 @@ function toggleLegend() {
274420 legendPanel . style . display = isHidden ? "block" : "none" ;
275421}
276422
423+ function clearSearch ( ) {
424+ const searchInput = document . getElementById ( "search-input" ) ;
425+ if ( searchInput ) {
426+ searchInput . value = "" ;
427+ if ( window . flamegraphChart ) {
428+ window . flamegraphChart . clear ( ) ;
429+ }
430+ }
431+ }
432+
277433// Handle window resize
278434window . addEventListener ( "resize" , function ( ) {
279435 if ( window . flamegraphChart && window . flamegraphData ) {
0 commit comments