@@ -2,6 +2,8 @@ const EMBEDDED_DATA = {{FLAMEGRAPH_DATA}};
2
2
3
3
// Global string table for resolving string indices
4
4
let stringTable = [ ] ;
5
+ let originalData = null ;
6
+ let currentThreadFilter = 'all' ;
5
7
6
8
// Function to resolve string indices to actual strings
7
9
function resolveString ( index ) {
@@ -374,6 +376,12 @@ function initFlamegraph() {
374
376
processedData = resolveStringIndices ( EMBEDDED_DATA ) ;
375
377
}
376
378
379
+ // Store original data for filtering
380
+ originalData = processedData ;
381
+
382
+ // Initialize thread filter dropdown
383
+ initThreadFilter ( processedData ) ;
384
+
377
385
const tooltip = createPythonTooltip ( processedData ) ;
378
386
const chart = createFlamegraph ( tooltip , processedData . value ) ;
379
387
renderFlamegraph ( chart , processedData ) ;
@@ -395,10 +403,26 @@ function populateStats(data) {
395
403
const functionMap = new Map ( ) ;
396
404
397
405
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
+ }
400
421
401
- if ( filename && funcname ) {
422
+ filename = filename || 'unknown' ;
423
+ funcname = funcname || 'unknown' ;
424
+
425
+ if ( filename !== 'unknown' && funcname !== 'unknown' && node . value > 0 ) {
402
426
// Calculate direct samples (this node's value minus children's values)
403
427
let childrenValue = 0 ;
404
428
if ( node . children ) {
@@ -447,15 +471,17 @@ function populateStats(data) {
447
471
// Populate the 3 cards
448
472
for ( let i = 0 ; i < 3 ; i ++ ) {
449
473
const num = i + 1 ;
450
- if ( i < hotSpots . length ) {
474
+ if ( i < hotSpots . length && hotSpots [ i ] ) {
451
475
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' ;
454
480
if ( funcDisplay . length > 35 ) {
455
481
funcDisplay = funcDisplay . substring ( 0 , 32 ) + '...' ;
456
482
}
457
483
458
- document . getElementById ( `hotspot-file-${ num } ` ) . textContent = `${ basename } :${ hotspot . lineno } ` ;
484
+ document . getElementById ( `hotspot-file-${ num } ` ) . textContent = `${ basename } :${ lineno } ` ;
459
485
document . getElementById ( `hotspot-func-${ num } ` ) . textContent = funcDisplay ;
460
486
document . getElementById ( `hotspot-detail-${ num } ` ) . textContent = `${ hotspot . directPercent . toFixed ( 1 ) } % samples (${ hotspot . directSamples . toLocaleString ( ) } )` ;
461
487
} else {
@@ -505,3 +531,102 @@ function clearSearch() {
505
531
}
506
532
}
507
533
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