@@ -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,39 @@ 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+ // Debug to understand the node structure
407+ if ( ! node ) return ;
408+
409+ // Try multiple ways to get the filename and function name
410+ let filename = node . filename ;
411+ let funcname = node . funcname || node . name ;
412+
413+ // If they're numbers (string indices), resolve them
414+ if ( typeof filename === 'number' ) {
415+ filename = resolveString ( filename ) ;
416+ }
417+ if ( typeof funcname === 'number' ) {
418+ funcname = resolveString ( funcname ) ;
419+ }
400420
401- if ( filename && funcname ) {
421+ // If they're still undefined or null, try extracting from the name field
422+ if ( ! filename && node . name ) {
423+ const nameStr = typeof node . name === 'number' ? resolveString ( node . name ) : node . name ;
424+ if ( nameStr && nameStr . includes ( '(' ) ) {
425+ // Parse format: "funcname (filename:lineno)"
426+ const match = nameStr . match ( / ^ ( .+ ?) \s * \( ( .+ ?) : ( \d + ) \) $ / ) ;
427+ if ( match ) {
428+ funcname = funcname || match [ 1 ] ;
429+ filename = filename || match [ 2 ] ;
430+ }
431+ }
432+ }
433+
434+ // Final fallback
435+ filename = filename || 'unknown' ;
436+ funcname = funcname || 'unknown' ;
437+
438+ if ( filename !== 'unknown' && funcname !== 'unknown' && node . value > 0 ) {
402439 // Calculate direct samples (this node's value minus children's values)
403440 let childrenValue = 0 ;
404441 if ( node . children ) {
@@ -447,15 +484,18 @@ function populateStats(data) {
447484 // Populate the 3 cards
448485 for ( let i = 0 ; i < 3 ; i ++ ) {
449486 const num = i + 1 ;
450- if ( i < hotSpots . length ) {
487+ if ( i < hotSpots . length && hotSpots [ i ] ) {
451488 const hotspot = hotSpots [ i ] ;
452- const basename = hotspot . filename . split ( '/' ) . pop ( ) ;
453- let funcDisplay = hotspot . funcname ;
489+ // Safe extraction with fallbacks
490+ const filename = hotspot . filename || 'unknown' ;
491+ const basename = filename !== 'unknown' ? filename . split ( '/' ) . pop ( ) : 'unknown' ;
492+ const lineno = hotspot . lineno !== undefined && hotspot . lineno !== null ? hotspot . lineno : '?' ;
493+ let funcDisplay = hotspot . funcname || 'unknown' ;
454494 if ( funcDisplay . length > 35 ) {
455495 funcDisplay = funcDisplay . substring ( 0 , 32 ) + '...' ;
456496 }
457497
458- document . getElementById ( `hotspot-file-${ num } ` ) . textContent = `${ basename } :${ hotspot . lineno } ` ;
498+ document . getElementById ( `hotspot-file-${ num } ` ) . textContent = `${ basename } :${ lineno } ` ;
459499 document . getElementById ( `hotspot-func-${ num } ` ) . textContent = funcDisplay ;
460500 document . getElementById ( `hotspot-detail-${ num } ` ) . textContent = `${ hotspot . directPercent . toFixed ( 1 ) } % samples (${ hotspot . directSamples . toLocaleString ( ) } )` ;
461501 } else {
@@ -505,3 +545,130 @@ function clearSearch() {
505545 }
506546}
507547
548+ function initThreadFilter ( data ) {
549+ const threadFilter = document . getElementById ( 'thread-filter' ) ;
550+ const threadWrapper = document . querySelector ( '.thread-filter-wrapper' ) ;
551+
552+ if ( ! threadFilter || ! data . threads ) {
553+ // Hide thread filter if no thread data
554+ if ( threadWrapper ) {
555+ threadWrapper . style . display = 'none' ;
556+ }
557+ return ;
558+ }
559+
560+ // Clear existing options except "All Threads"
561+ threadFilter . innerHTML = '<option value="all">All Threads</option>' ;
562+
563+ // Add thread options
564+ const threads = data . threads || [ ] ;
565+ threads . forEach ( threadId => {
566+ const option = document . createElement ( 'option' ) ;
567+ option . value = threadId ;
568+ option . textContent = `Thread ${ threadId } ` ;
569+ threadFilter . appendChild ( option ) ;
570+ } ) ;
571+
572+ // Hide filter if only one thread or no threads
573+ if ( threads . length <= 1 && threadWrapper ) {
574+ threadWrapper . style . display = 'none' ;
575+ }
576+ }
577+
578+ function filterByThread ( ) {
579+ const threadFilter = document . getElementById ( 'thread-filter' ) ;
580+ if ( ! threadFilter || ! originalData ) return ;
581+
582+ const selectedThread = threadFilter . value ;
583+ currentThreadFilter = selectedThread ;
584+
585+ let filteredData ;
586+ if ( selectedThread === 'all' ) {
587+ // Show all data
588+ filteredData = originalData ;
589+ } else {
590+ // Filter data by thread
591+ const threadId = parseInt ( selectedThread ) ;
592+ filteredData = filterDataByThread ( originalData , threadId ) ;
593+
594+ // Ensure string indices are resolved for the filtered data
595+ if ( filteredData . strings ) {
596+ stringTable = filteredData . strings ;
597+ filteredData = resolveStringIndices ( filteredData ) ;
598+ }
599+ }
600+
601+ // Re-render flamegraph with filtered data
602+ const tooltip = createPythonTooltip ( filteredData ) ;
603+ const chart = createFlamegraph ( tooltip , filteredData . value ) ;
604+ renderFlamegraph ( chart , filteredData ) ;
605+ }
606+
607+ function filterDataByThread ( data , threadId ) {
608+ // Deep clone the data structure and filter by thread
609+ function filterNode ( node ) {
610+ // Check if this node contains the thread
611+ if ( ! node . threads || ! node . threads . includes ( threadId ) ) {
612+ return null ;
613+ }
614+
615+ // Create a filtered copy of the node, preserving all fields
616+ const filteredNode = {
617+ name : node . name ,
618+ value : node . value ,
619+ filename : node . filename ,
620+ funcname : node . funcname ,
621+ lineno : node . lineno ,
622+ threads : node . threads ,
623+ source : node . source ,
624+ children : [ ]
625+ } ;
626+
627+ // Copy any other properties that might exist
628+ Object . keys ( node ) . forEach ( key => {
629+ if ( ! ( key in filteredNode ) ) {
630+ filteredNode [ key ] = node [ key ] ;
631+ }
632+ } ) ;
633+
634+ // Recursively filter children
635+ if ( node . children && Array . isArray ( node . children ) ) {
636+ filteredNode . children = node . children
637+ . map ( child => filterNode ( child ) )
638+ . filter ( child => child !== null ) ;
639+ }
640+
641+ return filteredNode ;
642+ }
643+
644+ // Create filtered root, preserving all metadata
645+ const filteredRoot = {
646+ ...data ,
647+ children : [ ] ,
648+ strings : data . strings // Preserve string table
649+ } ;
650+
651+ // Filter children
652+ if ( data . children && Array . isArray ( data . children ) ) {
653+ filteredRoot . children = data . children
654+ . map ( child => filterNode ( child ) )
655+ . filter ( child => child !== null ) ;
656+ }
657+
658+ // Recalculate total value based on filtered children
659+ function recalculateValue ( node ) {
660+ if ( ! node . children || node . children . length === 0 ) {
661+ return node . value || 0 ;
662+ }
663+ const childrenValue = node . children . reduce ( ( sum , child ) => {
664+ return sum + recalculateValue ( child ) ;
665+ } , 0 ) ;
666+ node . value = Math . max ( node . value || 0 , childrenValue ) ;
667+ return node . value ;
668+ }
669+
670+ recalculateValue ( filteredRoot ) ;
671+
672+ return filteredRoot ;
673+ }
674+
0 commit comments