import { Protocol as Cdp } from 'devtools-protocol' ; import { Category , ILocation , IProfileModel } from 'vscode-js-profile-core/out/esm/cpu/model' ; import { ISourceLocation } from 'vscode-js-profile-core/out/esm/location-mapping' ; const enum Constants { GcFunction = '(garbage collector)' } export interface IColumnLocation extends ILocation { graphId : number ; } export interface IColumn { x1 : number ; x2 : number ; rows : ( IColumnLocation | number ) [ ] ; } /**
 * Accessor for querying columns in the flame graph.
 */ export class LocationAccessor implements ILocation { public readonly id : number ; public readonly selfTime : number ; public readonly aggregateTime : number ; public readonly ticks : number ; public readonly category : Category ; public readonly callFrame : Cdp . Runtime . CallFrame ; public readonly src ? : ISourceLocation ; /**
     * Gets children of the location.
     */ public get children ( ) { const children = [ ] ; let dx = this . x ; do { const rs = this . model [ dx ] . rows [ this . y + 1 ] ; if ( rs && typeof rs !== 'number' ) { children . push ( new LocationAccessor ( this . model , dx , this . y + 1 ) ) ; } } while ( ++ dx < this . model . length && this . model [ dx ] . rows [ this . y ] === this . x ) ; return children ; } /**
     * Gets root-level accessors for the list of columns.
     */ public static rootAccessors ( columns ) { const accessors = [ ] ; for ( let x = 0 ; x < columns . length ; x ++ ) { if ( typeof columns [ x ] . rows [ 0 ] === 'object' ) { accessors . push ( new LocationAccessor ( columns , x , 0 ) ) ; } } return accessors ; } /**
     * Gets a mapping of the maximum Y values of each column which
     * should be highlighted.
     */ public static getFilteredColumns ( columns , accessors ) { const mapping = new Array ( columns . length ) ; for ( const accessor of accessors ) { mapping [ accessor . x ] = Math . max ( mapping [ accessor . x ] || 0 , accessor . y ) ; } return mapping ; } constructor ( private readonly model , public readonly x , public readonly y ) { const cell = this . model [ x ] . rows [ y ] ; if ( typeof cell === 'number' ) { throw new Error ( 'Cannot create an accessor in a merged location' ) ; } this . id = cell . id ; this . selfTime = cell . selfTime ; this . aggregateTime = cell . aggregateTime ; this . ticks = cell . ticks ; this . category = cell . category ; this . callFrame = cell . callFrame ; this . src = cell . src ; } } interface ITopDownGraphNode { location : ILocation ; aggregateTime : number ; selfTime : number ; children : Map < number , ITopDownGraphNode > ; } const createTopDownGraph = ( model ) => { const graph = { children : new Map ( ) , location : ( null as unknown ) as ILocation , aggregateTime : 0 , selfTime : 0 , } ; for ( let i = 1 ; i < model . samples . length - 1 ; i ++ ) { const root = model . nodes [ model . samples [ i ] ] ; const selfTime = model . timeDeltas [ i - 1 ] ; const locations = [ root . locationId ] ; for ( let id = root . parent ; id ; id = model . nodes [ id ] . parent ) { locations . unshift ( model . nodes [ id ] . locationId ) ; } let node = graph ; for ( const location of locations ) { node . aggregateTime += selfTime ; let next = node . children . get ( location ) ; if ( ! next ) { next = { children : new Map ( ) , aggregateTime : 0 , selfTime : 0 , location : model . locations [ location ] , } ; node . children . set ( location , next ) ; } node = next ; } node . selfTime += selfTime ; } return graph . children ; } ; export const buildLeftHeavyColumns = ( model ) : IColumn [ ] => { const graph = createTopDownGraph ( model ) ; const columns = [ ] ; let x = 0 ; let graphIdCounter = 0 ; const addSelf = ( node , rows ) => { const x1 = x ; x += node . selfTime / model . duration ; columns . push ( { x1 , x2 : x , rows : [ ... rows . map ( row => ( { ... row , graphId : graphIdCounter ++ , aggregateTime : node . selfTime , selfTime : 0 , } ) ) , { ... node . location , graphId : graphIdCounter ++ , selfTime : node . selfTime , aggregateTime : 0 } , ] , } ) ; } ; const build = ( node , rows ) => { let addedSelf = node . selfTime === 0 ; const children = [ ... node . children . values ( ) ] . sort ( ( a , b ) => b . aggregateTime - a . aggregateTime ) ; for ( const child of children ) { if ( ! addedSelf && node . selfTime > child . aggregateTime + child . selfTime ) { addSelf ( node , rows ) ; addedSelf = true ; } build ( child , [ ... rows , node . location ] ) ; } if ( ! addedSelf ) { addSelf ( node , rows ) ; } } ; for ( const child of [ ... graph . values ( ) ] . sort ( ( a , b ) => b . aggregateTime - a . aggregateTime ) ) { build ( child , [ ] ) ; } mergeColumns ( columns ) ; return columns ; } ; /**
 * Builds a 2D array of flame graph entries. Returns the columns with nested
 * 'rows'. Each column includes a percentage width (0-1) of the screen space.
 * A number, instead of a node in a column, means it should be merged with
 * the node at the column at the given index.
 */ export const buildColumns = ( model ) => { const columns = [ ] ; let graphIdCounter = 0 ; let timeOffset = 0 ; for ( let i = 1 ; i < model . samples . length - 1 ; i ++ ) { const root = model . nodes [ model . samples [ i ] ] ; const selfTime = model . timeDeltas [ i - 1 ] ; const rows = [ { ... model . locations [ root . locationId ] , graphId : graphIdCounter ++ , selfTime , aggregateTime : 0 , } , ] ; for ( let id = root . parent ; id ; id = model . nodes [ id ! ] . parent ) { rows . unshift ( { ... model . locations [ model . nodes [ id ] . locationId ] , graphId : graphIdCounter ++ , selfTime : 0 , aggregateTime : selfTime , } ) ; } columns . push ( { x1 : timeOffset / model . duration , x2 : ( selfTime + timeOffset ) / model . duration , rows , } ) ; timeOffset += selfTime ; } mergeColumns ( columns ) ; return columns ; } ; const mergeColumns = ( columns ) => { let lastFrameWasGc = false ; for ( let x = 1 ; x < columns . length ; x ++ ) { const col = columns [ x ] ; const root = col . rows [ 0 ] as IColumnLocation ; if ( col . rows . length === 1 && x > 0 && root . callFrame . functionName === Constants . GcFunction ) { col . rows = columns [ x - 1 ] . rows . map ( row => ( typeof row === 'number' ? row : x - 1 ) ) ; if ( ! lastFrameWasGc ) { col . rows . push ( root ) ; lastFrameWasGc = true ; } continue ; } lastFrameWasGc = false ; for ( let y = 0 ; y < col . rows . length ; y ++ ) { const current = col . rows [ y ] as IColumnLocation ; const prevOrNumber = columns [ x - 1 ] ?. rows [ y ] ; if ( typeof prevOrNumber === 'number' ) { if ( current . id !== ( columns [ prevOrNumber ] . rows [ y ] as IColumnLocation ) . id ) { break ; } col . rows [ y ] = prevOrNumber ; } else if ( prevOrNumber ?. id === current . id ) { col . rows [ y ] = x - 1 ; } else { break ; } const prev = typeof prevOrNumber === 'number' ? ( columns [ prevOrNumber ] . rows [ y ] as ILocation ) : prevOrNumber ; prev . selfTime += current . selfTime ; prev . aggregateTime += current . aggregateTime ; } } } ;