1- import { AfterViewInit , Component , inject , Injector , NgZone , OnDestroy , ViewChild } from '@angular/core' ;
1+ import { Component , ElementRef , inject , Injector , NgZone , OnDestroy , Renderer2 , ViewChild } from '@angular/core' ;
22import { FormControl } from '@angular/forms' ;
33import { ActivatedRoute , Params } from '@angular/router' ;
44import { Store } from '@ngrx/store' ;
55import { Throbber } from 'org_xprof/frontend/app/common/classes/throbber' ;
66import { OpType } from 'org_xprof/frontend/app/common/constants/enums' ;
77import { ChartDataInfo } from 'org_xprof/frontend/app/common/interfaces/chart' ;
88import { SimpleDataTable } from 'org_xprof/frontend/app/common/interfaces/data_table' ;
9- import { parseSourceInfo } from 'org_xprof/frontend/app/common/utils/source_info_utils' ;
109import { setLoadingState } from 'org_xprof/frontend/app/common/utils/utils' ;
1110import { CategoryTableDataProcessor } from 'org_xprof/frontend/app/components/chart/category_table_data_processor' ;
1211import { Chart } from 'org_xprof/frontend/app/components/chart/chart' ;
@@ -41,14 +40,10 @@ const TOTAL_TIME_ID = 'total_time';
4140 templateUrl : './hlo_stats.ng.html' ,
4241 styleUrls : [ './hlo_stats.css' ] ,
4342} )
44- export class HloStats extends Dashboard implements OnDestroy , AfterViewInit {
43+ export class HloStats extends Dashboard implements OnDestroy {
4544 tool = 'hlo_op_stats' ;
4645 sessionId = '' ;
4746 host = '' ;
48- // Info for selected op.
49- selectedProgramId = '' ;
50- selectedOpName = '' ;
51- selectedOpCategory = '' ;
5247 private readonly injector = inject ( Injector ) ;
5348 private readonly dataService : DataServiceV2Interface =
5449 inject ( DATA_SERVICE_INTERFACE_TOKEN ) ;
@@ -116,8 +111,15 @@ export class HloStats extends Dashboard implements OnDestroy, AfterViewInit {
116111 tableColumnsControl = new FormControl < number [ ] > ( [ ] ) ;
117112 tableColumns : Array < { index : number ; label : string } > = [ ] ;
118113
114+ // We add a listener to `chart` and manipulate multiple elements of
115+ // `chartElement`. Knowing that `Chart.elementRef` is private, we use
116+ // `ViewChild` twice to access both. See `addSourceInfoClickListener` for
117+ // more details.
119118 @ViewChild ( 'table' , { read : Chart , static : false } )
120- tableRef : Chart | undefined = undefined ;
119+ chartRef : Chart | undefined = undefined ;
120+ @ViewChild ( 'table' , { read : ElementRef , static : false } )
121+ chartElementRef : ElementRef | undefined = undefined ;
122+ private readonly renderer : Renderer2 = inject ( Renderer2 ) ;
121123 sourceFileAndLineNumber = '' ;
122124 stackTrace = '' ;
123125 showStackTrace = false ;
@@ -144,6 +146,9 @@ export class HloStats extends Dashboard implements OnDestroy, AfterViewInit {
144146 this . injector . get ( SOURCE_CODE_SERVICE_INTERFACE_TOKEN , null ) ;
145147 this . sourceCodeServiceIsAvailable =
146148 sourceCodeService ?. isAvailable ( ) === true ;
149+ if ( this . sourceCodeServiceIsAvailable ) {
150+ this . addSourceInfoClickListener ( ) ;
151+ }
147152 }
148153
149154 processQuery ( params : Params ) {
@@ -225,63 +230,48 @@ export class HloStats extends Dashboard implements OnDestroy, AfterViewInit {
225230 return updatedData ;
226231 }
227232
228- ngAfterViewInit ( ) {
229- if ( this . sourceCodeServiceIsAvailable ) {
230- this . addTableRowSelectListener ( ) ;
231- }
232- }
233-
234- private addTableRowSelectListener ( ) {
235- const chart = this . tableRef ?. chart ;
236- if ( ! chart ) {
233+ /**
234+ * Adds a click listener to the source info cells.
235+ *
236+ * If "Show Source Code" is checked, then whenever user clicks on the source
237+ * info cell, we show snippets of source code around the stack trace.
238+ *
239+ * Unfortunately, `google.visualization.Table` does not provide any API to
240+ * listen to click events on *cells*. So we manually add the click listener
241+ * to the items in this table (see
242+ * https://developers.google.com/chart/interactive/docs/gallery/table#events
243+ * as a reference).
244+ *
245+ * Unfortunately, `google.visualization.Table` does not provide enough
246+ * extension points to add interactive elements to cells. Therefore, we go
247+ * to the native elements of the table and add the click listener to the
248+ * cells with class `source-info-cell`.
249+ */
250+ private addSourceInfoClickListener ( ) {
251+ const chart = this . chartRef ?. chart ;
252+ const chartElement = this . chartElementRef ?. nativeElement ;
253+ if ( ! chart || ! chartElement ) {
254+ // TODO: b/429036372 - Using setTimeout to detect change is inefficient.
237255 setTimeout ( ( ) => {
238- this . addTableRowSelectListener ( ) ;
256+ this . addSourceInfoClickListener ( ) ;
239257 } , 100 ) ;
240258 return ;
241259 }
242- google . visualization . events . addListener ( chart , 'select' , ( ) => {
243- this . zone . run ( ( ) => {
244- const selection = chart . getSelection ( ) ;
245- if ( selection && selection . length > 0 && selection [ 0 ] . row != null ) {
246- const rowIndex = selection [ 0 ] . row ;
247- const rowData : Array < string | number | boolean | Date | null | undefined > = [ ] ;
248- if ( this . dataView ) {
249- for ( let i = 0 ; i < this . dataView . getNumberOfColumns ( ) ; i ++ ) {
250- rowData . push ( this . dataView . getValue ( rowIndex , i ) ) ;
251- }
252- this . handleRowSelection ( rowIndex , rowData ) ;
260+ google . visualization . events . addListener ( chart , 'ready' , ( ) => {
261+ this . renderer . listen ( chartElement , 'click' , ( event : Event ) => {
262+ const target = event . target ;
263+ if ( target instanceof HTMLElement ) {
264+ if ( target . classList . contains ( 'source-info-cell' ) ) {
265+ this . zone . run ( ( ) => {
266+ this . sourceFileAndLineNumber = target . textContent || '' ;
267+ this . stackTrace = target . getAttribute ( 'title' ) || '' ;
268+ } ) ;
253269 }
254270 }
255271 } ) ;
256272 } ) ;
257273 }
258274
259- handleRowSelection (
260- rowIndex : number ,
261- rowData : Array < string | number | boolean | Date | null | undefined > ) {
262- if ( ! this . dataView || ! this . dataTable ) return ;
263- const programId =
264- ( rowData [ this . dataTable . getColumnIndex ( PROGRAM_ID ) ] as string ||
265- '' ) . trim ( ) ;
266- const opName =
267- ( rowData [ this . dataTable . getColumnIndex ( OP_NAME_ID ) ] as string ||
268- '' ) . trim ( ) ;
269- const opCategory =
270- ( rowData [ this . dataTable . getColumnIndex ( OP_CATEGORY_ID ) ] as string ||
271- '' ) . trim ( ) ;
272- const sourceInfoHtmlString =
273- ( rowData [ this . dataTable . getColumnIndex ( SOURCE_INFO_ID ) ] as string ||
274- '' ) . trim ( ) ;
275- this . selectedProgramId = programId ;
276- this . selectedOpName = opName ;
277- this . selectedOpCategory = opCategory ;
278-
279- const { sourceFileAndLineNumber, stackTrace} =
280- parseSourceInfo ( sourceInfoHtmlString ) ;
281- this . sourceFileAndLineNumber = sourceFileAndLineNumber ;
282- this . stackTrace = stackTrace ;
283- }
284-
285275 toggleShowStackTrace ( ) {
286276 this . showStackTrace = ! this . showStackTrace ;
287277 }
@@ -301,7 +291,6 @@ export class HloStats extends Dashboard implements OnDestroy, AfterViewInit {
301291 }
302292
303293 override updateView ( ) {
304- super . updateView ( ) ;
305294 this . dataInfoForTable = {
306295 ...this . dataInfoForTable ,
307296 filters : this . getFilters ( ) ,
0 commit comments