@@ -31,22 +31,46 @@ import OverviewView from './views/overview_view';
3131import DominatorsView from './views/dominators_view' ;
3232import ObjectView from './views/object_view' ;
3333import AllObjectsView from './views/all_objects_view' ;
34- import InstancesView from './views/instances_view' ;
3534import BitmapGalleryView from './views/bitmap_gallery_view' ;
3635import ClassesView from './views/classes_view' ;
3736import StringsView from './views/strings_view' ;
3837import ArraysView from './views/arrays_view' ;
39- import FlamegraphObjectsView from './views/flamegraph_objects_view' ;
38+ import FlamegraphObjectsView , {
39+ flamegraphQuery ,
40+ } from './views/flamegraph_objects_view' ;
41+ import { SQL_PREAMBLE } from './components' ;
42+ import { NUM } from '../../trace_processor/query_result' ;
4043
4144// Each "Open in Heapdump Explorer" creates a closable flamegraph tab.
4245let nextFgId = 0 ;
43- const flamegraphTabs : Array < { id : number } & HeapdumpSelection > = [ ] ;
46+ const flamegraphTabs : Array <
47+ { id : number ; count : number | null } & HeapdumpSelection
48+ > = [ ] ;
4449let activeFgId = - 1 ;
4550
46- export function setFlamegraphSelection ( sel : HeapdumpSelection ) : void {
51+ export function setFlamegraphSelection (
52+ sel : HeapdumpSelection ,
53+ engine : Engine ,
54+ ) : void {
55+ const existing = flamegraphTabs . find (
56+ ( t ) => t . pathHashes === sel . pathHashes && t . isDominator === sel . isDominator ,
57+ ) ;
58+ if ( existing ) {
59+ activeFgId = existing . id ;
60+ navigate ( 'flamegraph-objects' ) ;
61+ return ;
62+ }
4763 const id = nextFgId ++ ;
48- flamegraphTabs . push ( { id, ...sel } ) ;
64+ const tab = { id, count : null as number | null , ...sel } ;
65+ flamegraphTabs . push ( tab ) ;
4966 activeFgId = id ;
67+ const q = flamegraphQuery ( sel . pathHashes , sel . isDominator ) ;
68+ engine
69+ . query ( `${ SQL_PREAMBLE } ; SELECT COUNT(*) AS c FROM (${ q } )` )
70+ . then ( ( r ) => {
71+ tab . count = Number ( r . firstRow ( { c : NUM } ) . c ) ;
72+ m . redraw ( ) ;
73+ } ) ;
5074}
5175
5276export function resetFlamegraphSelection ( ) : void {
@@ -65,14 +89,74 @@ export function resetCachedOverview(): void {
6589 overviewLoading = false ;
6690}
6791
68- // Maps drill-down views to their parent tab.
69- function activeTabKey ( view : string ) : string {
70- switch ( view ) {
71- case 'object' :
72- case 'instances' :
73- return 'classes' ;
74- default :
75- return view ;
92+ // Closable object tabs — clicking an object anywhere opens a new tab.
93+ interface InstanceTab {
94+ id : number ;
95+ objId : number ;
96+ label : string ;
97+ }
98+
99+ let nextInstanceTabId = 0 ;
100+ const instanceTabs : InstanceTab [ ] = [ ] ;
101+ let activeInstanceTabId = - 1 ;
102+
103+ function instanceTabKey ( id : number ) : string {
104+ return `inst-${ id } ` ;
105+ }
106+
107+ export function resetInstanceTabs ( ) : void {
108+ instanceTabs . length = 0 ;
109+ nextInstanceTabId = 0 ;
110+ activeInstanceTabId = - 1 ;
111+ }
112+
113+ function openInstanceTab ( objId : number , label ?: string ) : void {
114+ const existing = instanceTabs . find ( ( t ) => t . objId === objId ) ;
115+ if ( existing ) {
116+ activeInstanceTabId = existing . id ;
117+ return ;
118+ }
119+ const displayLabel = label ?? 'Instance' ;
120+ const tab : InstanceTab = {
121+ id : nextInstanceTabId ++ ,
122+ objId,
123+ label :
124+ displayLabel . length > 30
125+ ? displayLabel . slice ( 0 , 30 ) + '\u2026'
126+ : displayLabel ,
127+ } ;
128+ instanceTabs . push ( tab ) ;
129+ activeInstanceTabId = tab . id ;
130+ }
131+
132+ // Navigate wrapper: intercepts 'object' to open closable instance tabs.
133+ function navigateWithTabs (
134+ view : NavState [ 'view' ] ,
135+ params ?: Record < string , unknown > ,
136+ ) : void {
137+ if ( view === 'object' ) {
138+ openInstanceTab ( params ?. id as number , params ?. label as string | undefined ) ;
139+ navigate ( view , params ) ;
140+ return ;
141+ }
142+ activeInstanceTabId = - 1 ;
143+ navigate ( view , params ) ;
144+ }
145+
146+ // When nav state points to 'object' (e.g. after browser back), ensure
147+ // the matching instance tab exists and is active. When nav moves away
148+ // from 'object', clear the active instance tab so fixed tabs are shown.
149+ function syncInstanceTabFromNav ( ) : void {
150+ if ( nav . view !== 'object' ) {
151+ activeInstanceTabId = - 1 ;
152+ return ;
153+ }
154+ const objId = nav . params . id ;
155+ const existing = instanceTabs . find ( ( t ) => t . objId === objId ) ;
156+ if ( existing ) {
157+ activeInstanceTabId = existing . id ;
158+ } else {
159+ openInstanceTab ( objId , nav . params . label ) ;
76160 }
77161}
78162
@@ -92,54 +176,53 @@ function getActiveTabKey(): string {
92176 tab ? tab . id : flamegraphTabs [ flamegraphTabs . length - 1 ] . id ,
93177 ) ;
94178 }
95- return activeTabKey ( nav . view ) ;
179+ if ( activeInstanceTabId >= 0 ) {
180+ return instanceTabKey ( activeInstanceTabId ) ;
181+ }
182+ return nav . view ;
96183}
97184
98185function handleTabChange ( key : string ) : void {
99- const id = parseFgTabKey ( key ) ;
100- if ( id !== undefined ) {
101- activeFgId = id ;
186+ const fgId = parseFgTabKey ( key ) ;
187+ if ( fgId !== undefined ) {
188+ activeFgId = fgId ;
102189 navigate ( 'flamegraph-objects' ) ;
190+ } else if ( key . startsWith ( 'inst-' ) ) {
191+ activeInstanceTabId = parseInt ( key . slice ( 5 ) , 10 ) ;
192+ const tab = instanceTabs . find ( ( t ) => t . id === activeInstanceTabId ) ;
193+ if ( tab ) {
194+ navigate ( 'object' , { id : tab . objId } ) ;
195+ }
103196 } else {
104197 activeFgId = - 1 ;
198+ activeInstanceTabId = - 1 ;
105199 navigate ( key as NavState [ 'view' ] ) ;
106200 }
107201}
108202
109203function handleTabClose ( key : string ) : void {
110- const id = parseFgTabKey ( key ) ;
111- if ( id === undefined ) return ;
112- const idx = flamegraphTabs . findIndex ( ( t ) => t . id === id ) ;
204+ const fgId = parseFgTabKey ( key ) ;
205+ if ( fgId !== undefined ) {
206+ const idx = flamegraphTabs . findIndex ( ( t ) => t . id === fgId ) ;
207+ if ( idx === - 1 ) return ;
208+ flamegraphTabs . splice ( idx , 1 ) ;
209+ if ( activeFgId === fgId ) {
210+ activeFgId = - 1 ;
211+ navigate ( 'overview' ) ;
212+ }
213+ return ;
214+ }
215+ if ( ! key . startsWith ( 'inst-' ) ) return ;
216+ const id = parseInt ( key . slice ( 5 ) , 10 ) ;
217+ const idx = instanceTabs . findIndex ( ( t ) => t . id === id ) ;
113218 if ( idx === - 1 ) return ;
114- flamegraphTabs . splice ( idx , 1 ) ;
115- if ( activeFgId === id ) {
116- activeFgId = - 1 ;
219+ instanceTabs . splice ( idx , 1 ) ;
220+ if ( activeInstanceTabId === id ) {
221+ activeInstanceTabId = - 1 ;
117222 navigate ( 'overview' ) ;
118223 }
119224}
120225
121- // Renders the content for the "classes" tab, which also hosts drill-down
122- // views (instances, object detail) based on the current nav state.
123- function classesTabContent (
124- state : NavState ,
125- engine : Engine ,
126- overview : OverviewData ,
127- ) : m . Children {
128- switch ( state . view ) {
129- case 'object' :
130- return m ( ObjectView , {
131- engine,
132- heaps : overview . heaps ,
133- navigate,
134- params : state . params ,
135- } ) ;
136- case 'instances' :
137- return m ( InstancesView , { engine, navigate, params : state . params } ) ;
138- default :
139- return m ( ClassesView , { engine, navigate} ) ;
140- }
141- }
142-
143226function buildTabs (
144227 state : NavState ,
145228 engine : Engine ,
@@ -150,29 +233,33 @@ function buildTabs(
150233 {
151234 key : 'overview' ,
152235 title : 'Overview' ,
153- content : m ( OverviewView , { overview, navigate} ) ,
236+ content : m ( OverviewView , { overview, navigate : navigateWithTabs } ) ,
154237 } ,
155238 {
156239 key : 'classes' ,
157240 title : 'Classes' ,
158- content : classesTabContent ( state , engine , overview ) ,
241+ content : m ( ClassesView , { engine, navigate : navigateWithTabs } ) ,
159242 } ,
160243 {
161244 key : 'objects' ,
162245 title : 'Objects' ,
163- content : m ( AllObjectsView , { engine, navigate} ) ,
246+ content : m ( AllObjectsView , {
247+ engine,
248+ navigate : navigateWithTabs ,
249+ initialClass : state . view === 'objects' ? state . params . cls : undefined ,
250+ } ) ,
164251 } ,
165252 {
166253 key : 'dominators' ,
167254 title : 'Dominators' ,
168- content : m ( DominatorsView , { engine, navigate} ) ,
255+ content : m ( DominatorsView , { engine, navigate : navigateWithTabs } ) ,
169256 } ,
170257 {
171258 key : 'bitmaps' ,
172259 title : 'Bitmaps' ,
173260 content : m ( BitmapGalleryView , {
174261 engine,
175- navigate,
262+ navigate : navigateWithTabs ,
176263 hasFieldValues : overview . hasFieldValues ,
177264 filterKey :
178265 state . view === 'bitmaps' ? state . params . filterKey : undefined ,
@@ -183,7 +270,7 @@ function buildTabs(
183270 title : 'Strings' ,
184271 content : m ( StringsView , {
185272 engine,
186- navigate,
273+ navigate : navigateWithTabs ,
187274 initialQuery : state . view === 'strings' ? state . params . q : undefined ,
188275 hasFieldValues : overview . hasFieldValues ,
189276 } ) ,
@@ -192,9 +279,8 @@ function buildTabs(
192279 key : 'arrays' ,
193280 title : 'Arrays' ,
194281 content : m ( ArraysView , {
195- key : state . view === 'arrays' ? state . params . arrayHash ?? '' : '' ,
196282 engine,
197- navigate,
283+ navigate : navigateWithTabs ,
198284 initialArrayHash :
199285 state . view === 'arrays' ? state . params . arrayHash : undefined ,
200286 hasFieldValues : overview . hasFieldValues ,
@@ -206,11 +292,14 @@ function buildTabs(
206292 for ( const fg of flamegraphTabs ) {
207293 tabs . push ( {
208294 key : fgTabKey ( fg . id ) ,
209- title : 'Flamegraph' ,
295+ title :
296+ fg . count !== null
297+ ? `Flamegraph (${ fg . count . toLocaleString ( ) } )`
298+ : 'Flamegraph' ,
210299 closeButton : true ,
211300 content : m ( FlamegraphObjectsView , {
212301 engine,
213- navigate,
302+ navigate : navigateWithTabs ,
214303 pathHashes : fg . pathHashes ,
215304 isDominator : fg . isDominator ,
216305 onBackToTimeline : ( ) => {
@@ -220,6 +309,21 @@ function buildTabs(
220309 } ) ;
221310 }
222311
312+ // Append closable object instance tabs.
313+ for ( const obj of instanceTabs ) {
314+ tabs . push ( {
315+ key : instanceTabKey ( obj . id ) ,
316+ title : obj . label ,
317+ closeButton : true ,
318+ content : m ( ObjectView , {
319+ engine,
320+ heaps : overview . heaps ,
321+ navigate : navigateWithTabs ,
322+ params : { id : obj . objId } ,
323+ } ) ,
324+ } ) ;
325+ }
326+
223327 return tabs ;
224328}
225329
@@ -260,6 +364,7 @@ export class HeapDumpPage implements m.ClassComponent<HeapDumpPageAttrs> {
260364
261365 view ( vnode : m . Vnode < HeapDumpPageAttrs > ) {
262366 syncFromSubpage ( vnode . attrs . subpage ) ;
367+ syncInstanceTabFromNav ( ) ;
263368
264369 if ( ! HeapDumpPage . engine || ! HeapDumpPage . hasHeapData ) {
265370 return m (
0 commit comments