@@ -2,6 +2,8 @@ const EMBEDDED_DATA = {{FLAMEGRAPH_DATA}};
22
33// Global string table for resolving string indices
44let stringTable = [ ] ;
5+ let originalData = null ;
6+ let currentThreadFilter = 'all' ;
57
68// Function to resolve string indices to actual strings
79function resolveString ( index ) {
@@ -374,6 +376,12 @@ function initFlamegraph() {
374376 processedData = resolveStringIndices ( EMBEDDED_DATA ) ;
375377 }
376378
379+ // Store original data for filtering
380+ originalData = processedData ;
381+
382+ // Initialize thread filter dropdown
383+ initThreadFilter ( processedData ) ;
384+
377385 const tooltip = createPythonTooltip ( processedData ) ;
378386 const chart = createFlamegraph ( tooltip , processedData . value ) ;
379387 renderFlamegraph ( chart , processedData ) ;
@@ -395,10 +403,26 @@ function populateStats(data) {
395403 const functionMap = new Map ( ) ;
396404
397405 function collectFunctions ( node ) {
398- const filename = resolveString ( node . filename ) ;
399- const funcname = resolveString ( node . funcname ) ;
406+ if ( ! node ) return ;
407+
408+ let filename = typeof node . filename === 'number' ? resolveString ( node . filename ) : node . filename ;
409+ let funcname = typeof node . funcname === 'number' ? resolveString ( node . funcname ) : node . funcname ;
410+
411+ if ( ! filename || ! funcname ) {
412+ const nameStr = typeof node . name === 'number' ? resolveString ( node . name ) : node . name ;
413+ if ( nameStr ?. includes ( '(' ) ) {
414+ const match = nameStr . match ( / ^ ( .+ ?) \s * \( ( .+ ?) : ( \d + ) \) $ / ) ;
415+ if ( match ) {
416+ funcname = funcname || match [ 1 ] ;
417+ filename = filename || match [ 2 ] ;
418+ }
419+ }
420+ }
400421
401- if ( filename && funcname ) {
422+ filename = filename || 'unknown' ;
423+ funcname = funcname || 'unknown' ;
424+
425+ if ( filename !== 'unknown' && funcname !== 'unknown' && node . value > 0 ) {
402426 // Calculate direct samples (this node's value minus children's values)
403427 let childrenValue = 0 ;
404428 if ( node . children ) {
@@ -447,15 +471,17 @@ function populateStats(data) {
447471 // Populate the 3 cards
448472 for ( let i = 0 ; i < 3 ; i ++ ) {
449473 const num = i + 1 ;
450- if ( i < hotSpots . length ) {
474+ if ( i < hotSpots . length && hotSpots [ i ] ) {
451475 const hotspot = hotSpots [ i ] ;
452- const basename = hotspot . filename . split ( '/' ) . pop ( ) ;
453- let funcDisplay = hotspot . funcname ;
476+ const filename = hotspot . filename || 'unknown' ;
477+ const basename = filename !== 'unknown' ? filename . split ( '/' ) . pop ( ) : 'unknown' ;
478+ const lineno = hotspot . lineno ?? '?' ;
479+ let funcDisplay = hotspot . funcname || 'unknown' ;
454480 if ( funcDisplay . length > 35 ) {
455481 funcDisplay = funcDisplay . substring ( 0 , 32 ) + '...' ;
456482 }
457483
458- document . getElementById ( `hotspot-file-${ num } ` ) . textContent = `${ basename } :${ hotspot . lineno } ` ;
484+ document . getElementById ( `hotspot-file-${ num } ` ) . textContent = `${ basename } :${ lineno } ` ;
459485 document . getElementById ( `hotspot-func-${ num } ` ) . textContent = funcDisplay ;
460486 document . getElementById ( `hotspot-detail-${ num } ` ) . textContent = `${ hotspot . directPercent . toFixed ( 1 ) } % samples (${ hotspot . directSamples . toLocaleString ( ) } )` ;
461487 } else {
@@ -505,3 +531,102 @@ function clearSearch() {
505531 }
506532}
507533
534+ function initThreadFilter ( data ) {
535+ const threadFilter = document . getElementById ( 'thread-filter' ) ;
536+ const threadWrapper = document . querySelector ( '.thread-filter-wrapper' ) ;
537+
538+ if ( ! threadFilter || ! data . threads ) {
539+ return ;
540+ }
541+
542+ // Clear existing options except "All Threads"
543+ threadFilter . innerHTML = '<option value="all">All Threads</option>' ;
544+
545+ // Add thread options
546+ const threads = data . threads || [ ] ;
547+ threads . forEach ( threadId => {
548+ const option = document . createElement ( 'option' ) ;
549+ option . value = threadId ;
550+ option . textContent = `Thread ${ threadId } ` ;
551+ threadFilter . appendChild ( option ) ;
552+ } ) ;
553+
554+ // Show filter if more than one thread
555+ if ( threads . length > 1 && threadWrapper ) {
556+ threadWrapper . style . display = 'inline-flex' ;
557+ }
558+ }
559+
560+ function filterByThread ( ) {
561+ const threadFilter = document . getElementById ( 'thread-filter' ) ;
562+ if ( ! threadFilter || ! originalData ) return ;
563+
564+ const selectedThread = threadFilter . value ;
565+ currentThreadFilter = selectedThread ;
566+
567+ let filteredData ;
568+ if ( selectedThread === 'all' ) {
569+ // Show all data
570+ filteredData = originalData ;
571+ } else {
572+ // Filter data by thread
573+ const threadId = parseInt ( selectedThread ) ;
574+ filteredData = filterDataByThread ( originalData , threadId ) ;
575+
576+ if ( filteredData . strings ) {
577+ stringTable = filteredData . strings ;
578+ filteredData = resolveStringIndices ( filteredData ) ;
579+ }
580+ }
581+
582+ // Re-render flamegraph with filtered data
583+ const tooltip = createPythonTooltip ( filteredData ) ;
584+ const chart = createFlamegraph ( tooltip , filteredData . value ) ;
585+ renderFlamegraph ( chart , filteredData ) ;
586+ }
587+
588+ function filterDataByThread ( data , threadId ) {
589+ function filterNode ( node ) {
590+ if ( ! node . threads || ! node . threads . includes ( threadId ) ) {
591+ return null ;
592+ }
593+
594+ const filteredNode = {
595+ ...node ,
596+ children : [ ]
597+ } ;
598+
599+ if ( node . children && Array . isArray ( node . children ) ) {
600+ filteredNode . children = node . children
601+ . map ( child => filterNode ( child ) )
602+ . filter ( child => child !== null ) ;
603+ }
604+
605+ return filteredNode ;
606+ }
607+
608+ const filteredRoot = {
609+ ...data ,
610+ children : [ ]
611+ } ;
612+
613+ if ( data . children && Array . isArray ( data . children ) ) {
614+ filteredRoot . children = data . children
615+ . map ( child => filterNode ( child ) )
616+ . filter ( child => child !== null ) ;
617+ }
618+
619+ function recalculateValue ( node ) {
620+ if ( ! node . children || node . children . length === 0 ) {
621+ return node . value || 0 ;
622+ }
623+ const childrenValue = node . children . reduce ( ( sum , child ) => sum + recalculateValue ( child ) , 0 ) ;
624+ node . value = Math . max ( node . value || 0 , childrenValue ) ;
625+ return node . value ;
626+ }
627+
628+ recalculateValue ( filteredRoot ) ;
629+
630+ return filteredRoot ;
631+ }
632+
0 commit comments