| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684 |
- import { Tab } from '../ui/Tab.js';
- import { Graph } from '../ui/Graph.js';
- import { getItem, setItem } from '../Inspector.js';
- import { info } from '../ui/utils.js';
- import {
- ByteType,
- FloatType,
- HalfFloatType,
- IntType,
- ShortType,
- UnsignedByteType,
- UnsignedInt101111Type,
- UnsignedInt248Type,
- UnsignedInt5999Type,
- UnsignedIntType,
- UnsignedShort4444Type,
- UnsignedShort5551Type,
- UnsignedShortType,
- AlphaFormat,
- RGBFormat,
- RGBAFormat,
- DepthFormat,
- DepthStencilFormat,
- RedFormat,
- RedIntegerFormat,
- RGFormat,
- RGIntegerFormat,
- RGBIntegerFormat,
- RGBAIntegerFormat
- } from 'three';
- const LIMIT = 500;
- const TRIANGLES_GRAPH_LIMIT = 60;
- class Timeline extends Tab {
- constructor( options = {} ) {
- super( 'Timeline', options );
- this.content.style.overflow = 'hidden';
- this.isRecording = false;
- this.frames = []; // Array of { id: number, calls: [] }
- this.baseTriangles = 0;
- this.currentFrame = null;
- this.isHierarchicalView = true;
- this.callBlocks = new WeakMap();
- this.fallbackBlocks = [];
- this.originalBackend = null;
- this.originalMethods = new Map();
- this.renderer = null;
- this.graph = new Graph( LIMIT ); // Accommodate standard graph points
- // Make lines in timeline graph
- this.graph.addLine( 'fps', 'var( --color-fps )' );
- this.graph.addLine( 'calls', 'var( --color-call )' );
- this.graph.addLine( 'triangles', 'var( --color-red )' );
- this.buildHeader();
- this.buildUI();
- // Bind window resize to update graph bounds
- window.addEventListener( 'resize', () => {
- if ( ! this.isRecording && this.frames.length > 0 ) {
- this.renderSlider();
- }
- } );
- }
- init( inspector ) {
- super.init( inspector );
- this.profiler.addEventListener( 'resize', () => {
- if ( ! this.isRecording && this.frames.length > 0 ) {
- this.renderSlider();
- }
- } );
- }
- buildHeader() {
- const header = document.createElement( 'div' );
- header.className = 'toolbar';
- this.recordButton = document.createElement( 'button' );
- this.recordButton.className = 'console-copy-button'; // Reusing style
- this.recordButton.title = 'Record';
- this.recordButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="4" fill="currentColor"></circle></svg>';
- this.recordButton.style.padding = '0 10px';
- this.recordButton.style.lineHeight = '24px'; // Match other buttons height
- this.recordButton.style.display = 'flex';
- this.recordButton.style.alignItems = 'center';
- this.recordButton.addEventListener( 'click', () => this.toggleRecording() );
- const clearButton = document.createElement( 'button' );
- clearButton.className = 'console-copy-button';
- clearButton.title = 'Clear';
- clearButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>';
- clearButton.style.padding = '0 10px';
- clearButton.style.lineHeight = '24px';
- clearButton.style.display = 'flex';
- clearButton.style.alignItems = 'center';
- clearButton.addEventListener( 'click', () => this.clear() );
- this.viewModeSelect = document.createElement( 'select' );
- this.viewModeSelect.className = 'select';
- this.viewModeSelect.style.width = '120px';
- this.viewModeSelect.style.marginRight = '10px';
- const hierarchyOption = document.createElement( 'option' );
- hierarchyOption.value = 'hierarchy';
- hierarchyOption.textContent = 'Hierarchy';
- this.viewModeSelect.appendChild( hierarchyOption );
- const countsOption = document.createElement( 'option' );
- countsOption.value = 'counts';
- countsOption.textContent = 'Count';
- this.viewModeSelect.appendChild( countsOption );
- this.viewModeSelect.value = this.isHierarchicalView ? 'hierarchy' : 'counts';
- this.viewModeSelect.addEventListener( 'change', () => {
- this.isHierarchicalView = this.viewModeSelect.value === 'hierarchy';
- if ( this.selectedFrameIndex !== undefined && this.selectedFrameIndex !== - 1 ) {
- this.selectFrame( this.selectedFrameIndex );
- }
- } );
- this.recordRefreshButton = document.createElement( 'button' );
- this.recordRefreshButton.className = 'console-copy-button'; // Reusing style
- this.recordRefreshButton.title = 'Refresh & Record';
- this.recordRefreshButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"></path><path d="M21 3v5h-5"></path><circle cx="12" cy="12" r="3" fill="currentColor"></circle></svg>';
- this.recordRefreshButton.style.padding = '0 10px';
- this.recordRefreshButton.style.lineHeight = '24px';
- this.recordRefreshButton.style.display = 'flex';
- this.recordRefreshButton.style.alignItems = 'center';
- this.recordRefreshButton.addEventListener( 'click', () => {
- const timelineSettings = getItem( 'timeline' );
- timelineSettings.recording = true;
- setItem( 'timeline', timelineSettings );
- window.location.reload();
- } );
- this.exportButton = document.createElement( 'button' );
- this.exportButton.className = 'console-copy-button';
- this.exportButton.title = 'Export';
- this.exportButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>';
- this.exportButton.style.padding = '0 10px';
- this.exportButton.style.lineHeight = '24px';
- this.exportButton.style.display = 'flex';
- this.exportButton.style.alignItems = 'center';
- this.exportButton.addEventListener( 'click', () => this.exportData() );
- const buttonsGroup = document.createElement( 'div' );
- buttonsGroup.className = 'console-buttons-group';
- buttonsGroup.appendChild( this.recordButton );
- buttonsGroup.appendChild( this.recordRefreshButton );
- buttonsGroup.appendChild( this.exportButton );
- buttonsGroup.appendChild( clearButton );
- const titleElement = document.createElement( 'div' );
- titleElement.style.display = 'flex';
- titleElement.style.alignItems = 'center';
- titleElement.style.color = 'var(--text-primary)';
- titleElement.style.alignSelf = 'center';
- this.frameInfo = document.createElement( 'span' );
- this.frameInfo.style.display = 'inline-flex';
- this.frameInfo.style.alignItems = 'center';
- this.frameInfo.style.marginLeft = '5px';
- this.frameInfo.style.fontFamily = 'monospace';
- this.frameInfo.style.color = 'var(--text-secondary)';
- this.frameInfo.style.fontSize = '12px';
- titleElement.appendChild( this.viewModeSelect );
- titleElement.appendChild( this.frameInfo );
- header.appendChild( titleElement );
- header.appendChild( buttonsGroup );
- this.content.appendChild( header );
- }
- buildUI() {
- const container = document.createElement( 'div' );
- container.style.display = 'flex';
- container.style.flexDirection = 'column';
- container.style.flex = '1';
- container.style.minHeight = '0';
- container.style.marginTop = '10px';
- container.style.width = '100%';
- // Top Player/Graph Slider using Graph.js SVG
- const graphContainer = document.createElement( 'div' );
- graphContainer.style.height = '60px';
- graphContainer.style.minHeight = '60px';
- graphContainer.style.borderBottom = '1px solid var(--border-color)';
- graphContainer.style.backgroundColor = 'var(--background-color)';
- this.graphSlider = document.createElement( 'div' );
- this.graphSlider.style.height = '100%';
- this.graphSlider.style.margin = '0 10px';
- this.graphSlider.style.position = 'relative';
- this.graphSlider.style.cursor = 'crosshair';
- graphContainer.appendChild( this.graphSlider );
- // Setup SVG from Graph
- this.graph.domElement.style.width = '0';
- this.graph.domElement.style.minWidth = '100%';
- this.graph.domElement.style.height = '100%';
- this.graphSlider.appendChild( this.graph.domElement );
- // Hover indicator
- this.hoverIndicator = document.createElement( 'div' );
- this.hoverIndicator.style.position = 'absolute';
- this.hoverIndicator.style.top = '0';
- this.hoverIndicator.style.bottom = '0';
- this.hoverIndicator.style.width = '1px';
- this.hoverIndicator.style.backgroundColor = 'rgba(255, 255, 255, 0.3)';
- this.hoverIndicator.style.pointerEvents = 'none';
- this.hoverIndicator.style.display = 'none';
- this.hoverIndicator.style.zIndex = '9';
- this.hoverIndicator.style.transform = 'translateX(-50%)';
- this.graphSlider.appendChild( this.hoverIndicator );
- // Playhead indicator (vertical line)
- this.playhead = document.createElement( 'div' );
- this.playhead.style.position = 'absolute';
- this.playhead.style.top = '0';
- this.playhead.style.bottom = '0';
- this.playhead.style.width = '2px';
- this.playhead.style.backgroundColor = 'var(--color-red)';
- this.playhead.style.boxShadow = '0 0 4px rgba(255,0,0,0.5)';
- this.playhead.style.pointerEvents = 'none';
- this.playhead.style.display = 'none';
- this.playhead.style.zIndex = '10';
- this.playhead.style.transform = 'translateX(-50%)';
- this.graphSlider.appendChild( this.playhead );
- // Playhead handle (triangle/pointer)
- const playheadHandle = document.createElement( 'div' );
- playheadHandle.style.position = 'absolute';
- playheadHandle.style.top = '0';
- playheadHandle.style.left = '50%';
- playheadHandle.style.transform = 'translate(-50%, 0)';
- playheadHandle.style.width = '0';
- playheadHandle.style.height = '0';
- playheadHandle.style.borderLeft = '6px solid transparent';
- playheadHandle.style.borderRight = '6px solid transparent';
- playheadHandle.style.borderTop = '8px solid var(--color-red)';
- this.playhead.appendChild( playheadHandle );
- // Make it focusable to accept keyboard events
- this.graphSlider.tabIndex = 0;
- this.graphSlider.style.outline = 'none';
- // Mouse interactivity on the graph
- let isDragging = false;
- const updatePlayheadFromEvent = ( e ) => {
- if ( this.isRecording || this.frames.length === 0 ) return;
- const rect = this.graphSlider.getBoundingClientRect();
- let x = e.clientX - rect.left;
- // Clamp
- x = Math.max( 0, Math.min( x, rect.width ) );
- this.fixedScreenX = x;
- // The graph stretches its points across the width
- // Find closest frame index based on exact point coordinates
- const pointCount = this.graph.lines[ 'calls' ].points.length;
- if ( pointCount === 0 ) return;
- const pointStep = rect.width / ( this.graph.maxPoints - 1 );
- const offset = rect.width - ( ( pointCount - 1 ) * pointStep );
- let localFrameIndex = Math.round( ( x - offset ) / pointStep );
- localFrameIndex = Math.max( 0, Math.min( localFrameIndex, pointCount - 1 ) );
- if ( localFrameIndex >= pointCount - 2 ) {
- this.isTrackingLatest = true;
- } else {
- this.isTrackingLatest = false;
- }
- let frameIndex = localFrameIndex;
- if ( this.frames.length > pointCount ) {
- frameIndex += this.frames.length - pointCount;
- }
- this.playhead.style.display = 'block';
- this.selectFrame( frameIndex );
- };
- this.graphSlider.addEventListener( 'mousedown', ( e ) => {
- if ( this.isRecording ) return;
- isDragging = true;
- this.isManualScrubbing = true;
- this.graphSlider.focus();
- updatePlayheadFromEvent( e );
- } );
- this.graphSlider.addEventListener( 'mouseenter', () => {
- if ( this.frames.length > 0 && ! this.isRecording ) {
- this.hoverIndicator.style.display = 'block';
- }
- } );
- this.graphSlider.addEventListener( 'mouseleave', () => {
- this.hoverIndicator.style.display = 'none';
- } );
- this.graphSlider.addEventListener( 'mousemove', ( e ) => {
- if ( this.frames.length === 0 || this.isRecording ) return;
- const rect = this.graphSlider.getBoundingClientRect();
- let x = e.clientX - rect.left;
- x = Math.max( 0, Math.min( x, rect.width ) );
- const pointCount = this.graph.lines[ 'calls' ].points.length;
- if ( pointCount > 0 ) {
- const pointStep = rect.width / ( this.graph.maxPoints - 1 );
- const offset = rect.width - ( ( pointCount - 1 ) * pointStep );
- let localFrameIndex = Math.round( ( x - offset ) / pointStep );
- localFrameIndex = Math.max( 0, Math.min( localFrameIndex, pointCount - 1 ) );
- let snappedX = offset + localFrameIndex * pointStep;
- snappedX = Math.max( 1, Math.min( snappedX, rect.width - 1 ) );
- this.hoverIndicator.style.left = snappedX + 'px';
- } else {
- const clampedX = Math.max( 1, Math.min( x, rect.width - 1 ) );
- this.hoverIndicator.style.left = clampedX + 'px';
- }
- } );
- this.graphSlider.addEventListener( 'keydown', ( e ) => {
- if ( this.frames.length === 0 || this.isRecording ) return;
- let newIndex = this.selectedFrameIndex;
- if ( e.key === 'ArrowLeft' ) {
- newIndex = Math.max( 0, this.selectedFrameIndex - 1 );
- e.preventDefault();
- } else if ( e.key === 'ArrowRight' ) {
- newIndex = Math.min( this.frames.length - 1, this.selectedFrameIndex + 1 );
- e.preventDefault();
- }
- if ( newIndex !== this.selectedFrameIndex ) {
- this.selectFrame( newIndex );
- // Update playhead tracking state
- const pointCount = this.graph.lines[ 'calls' ].points.length;
- if ( pointCount > 0 ) {
- let localIndex = newIndex;
- if ( this.frames.length > pointCount ) {
- localIndex = newIndex - ( this.frames.length - pointCount );
- }
- if ( localIndex >= pointCount - 2 ) {
- this.isTrackingLatest = true;
- } else {
- this.isTrackingLatest = false;
- }
- const rect = this.graphSlider.getBoundingClientRect();
- const pointStep = rect.width / ( this.graph.maxPoints - 1 );
- const offset = rect.width - ( ( pointCount - 1 ) * pointStep );
- this.fixedScreenX = offset + localIndex * pointStep;
- }
- }
- } );
- window.addEventListener( 'mousemove', ( e ) => {
- if ( isDragging ) {
- updatePlayheadFromEvent( e );
- // Also move hover indicator to match playback
- const rect = this.graphSlider.getBoundingClientRect();
- let x = e.clientX - rect.left;
- x = Math.max( 0, Math.min( x, rect.width ) );
- const pointCount = this.graph.lines[ 'calls' ].points.length;
- if ( pointCount > 0 ) {
- const pointStep = rect.width / ( this.graph.maxPoints - 1 );
- const offset = rect.width - ( ( pointCount - 1 ) * pointStep );
- let localFrameIndex = Math.round( ( x - offset ) / pointStep );
- localFrameIndex = Math.max( 0, Math.min( localFrameIndex, pointCount - 1 ) );
- let snappedX = offset + localFrameIndex * pointStep;
- snappedX = Math.max( 1, Math.min( snappedX, rect.width - 1 ) );
- this.hoverIndicator.style.left = snappedX + 'px';
- } else {
- const clampedX = Math.max( 1, Math.min( x, rect.width - 1 ) );
- this.hoverIndicator.style.left = clampedX + 'px';
- }
- }
- } );
- window.addEventListener( 'mouseup', () => {
- isDragging = false;
- this.isManualScrubbing = false;
- } );
- container.appendChild( graphContainer );
- // Bottom Main Area (Timeline Sequence)
- const mainArea = document.createElement( 'div' );
- mainArea.style.flex = '1';
- mainArea.style.display = 'flex';
- mainArea.style.flexDirection = 'column';
- mainArea.style.overflow = 'hidden';
- // Timeline Track
- this.timelineTrack = document.createElement( 'div' );
- this.timelineTrack.style.flex = '1';
- this.timelineTrack.style.overflowY = 'auto';
- this.timelineTrack.style.margin = '10px';
- this.timelineTrack.style.marginTop = '8px';
- this.timelineTrack.style.backgroundColor = 'var(--background-color)';
- mainArea.appendChild( this.timelineTrack );
- container.appendChild( mainArea );
- this.content.appendChild( container );
- }
- setRenderer( renderer ) {
- this.renderer = renderer;
- const timelineSettings = getItem( 'timeline' );
- if ( timelineSettings.recording ) {
- timelineSettings.recording = false;
- setItem( 'timeline', timelineSettings );
- this.toggleRecording();
- }
- }
- toggleRecording() {
- if ( ! this.renderer ) {
- console.warn( 'Timeline: No renderer defined.' );
- return;
- }
- this.isRecording = ! this.isRecording;
- if ( this.isRecording ) {
- this.recordButton.title = 'Stop';
- this.recordButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></svg>';
- this.recordButton.style.color = 'var(--color-red)';
- this.startRecording();
- } else {
- this.recordButton.title = 'Record';
- this.recordButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="4" fill="currentColor"></circle></svg>';
- this.recordButton.style.color = '';
- this.stopRecording();
- this.renderSlider();
- }
- }
- startRecording() {
- this.frames = [];
- this.currentFrame = null;
- this.selectedFrameIndex = - 1;
- this.fixedScreenX = 0;
- this.isTrackingLatest = true;
- this.isManualScrubbing = false;
- this.clear();
- this.frameInfo.textContent = 'Recording...';
- const backend = this.renderer.backend;
- const methods = Object.getOwnPropertyNames( Object.getPrototypeOf( backend ) ).filter( prop => prop !== 'constructor' );
- for ( const prop of methods ) {
- const descriptor = Object.getOwnPropertyDescriptor( Object.getPrototypeOf( backend ), prop );
- if ( descriptor && ( descriptor.get || descriptor.set ) ) continue;
- const originalFunc = backend[ prop ];
- if ( typeof originalFunc === 'function' && typeof prop === 'string' ) {
- this.originalMethods.set( prop, originalFunc );
- backend[ prop ] = ( ...args ) => {
- if ( prop.toLowerCase().includes( 'timestamp' ) || prop.startsWith( 'get' ) || prop.startsWith( 'set' ) || prop.startsWith( 'has' ) || prop.startsWith( '_' ) || prop.startsWith( 'needs' ) ) {
- return originalFunc.apply( backend, args );
- }
- // Check for frame change
- const frameNumber = this.renderer.info.frame;
- if ( ! this.currentFrame || this.currentFrame.id !== frameNumber ) {
- if ( this.currentFrame ) {
- this.currentFrame.fps = this.renderer.inspector ? this.renderer.inspector.fps : 0;
- if ( ! isFinite( this.currentFrame.fps ) ) {
- this.currentFrame.fps = 0;
- }
- const t = this.currentFrame.triangles || 0;
- if ( t > this.baseTriangles ) {
- const oldBase = this.baseTriangles;
- this.baseTriangles = t;
- if ( oldBase > 0 ) {
- const ratio = oldBase / this.baseTriangles;
- const points = this.graph.lines[ 'triangles' ].points;
- for ( let i = 0; i < points.length; i ++ ) {
- points[ i ] *= ratio;
- }
- }
- }
- const normalizedTriangles = this.baseTriangles > 0 ? ( t / this.baseTriangles ) * TRIANGLES_GRAPH_LIMIT : 0;
- this.graph.addPoint( 'calls', this.currentFrame.calls.length );
- this.graph.addPoint( 'fps', this.currentFrame.fps );
- this.graph.addPoint( 'triangles', normalizedTriangles );
- this.graph.update();
- }
- this.currentFrame = { id: frameNumber, calls: [], fps: 0, triangles: 0 };
- this.frames.push( this.currentFrame );
- if ( this.frames.length > LIMIT ) {
- this.frames.shift();
- }
- }
- const call = { method: prop, target: args[ 0 ] };
- const details = this.getCallDetail( prop, args );
- if ( details ) {
- call.details = details;
- if ( details.triangles !== undefined ) {
- this.currentFrame.triangles += details.triangles;
- }
- }
- this.currentFrame.calls.push( call );
- return originalFunc.apply( backend, args );
- };
- }
- }
- }
- stopRecording() {
- if ( this.originalMethods.size > 0 ) {
- const backend = this.renderer.backend;
- for ( const [ prop, originalFunc ] of this.originalMethods.entries() ) {
- backend[ prop ] = originalFunc;
- }
- this.originalMethods.clear();
- if ( this.currentFrame ) {
- this.currentFrame.fps = this.renderer.inspector ? this.renderer.inspector.fps : 0;
- }
- }
- }
- clear() {
- this.frames = [];
- this.timelineTrack.innerHTML = '';
- this.playhead.style.display = 'none';
- this.frameInfo.textContent = '';
- this.baseTriangles = 0;
- this.graph.lines[ 'calls' ].points = [];
- this.graph.lines[ 'fps' ].points = [];
- this.graph.lines[ 'triangles' ].points = [];
- this.graph.resetLimit();
- this.graph.update();
- }
- exportData() {
- if ( this.frames.length === 0 ) return;
- const data = JSON.stringify( this.frames, null, '\t' );
- const blob = new Blob( [ data ], { type: 'application/json' } );
- const url = URL.createObjectURL( blob );
- const a = document.createElement( 'a' );
- a.href = url;
- a.download = 'threejs-timeline.json';
- a.click();
- URL.revokeObjectURL( url );
- }
- getRenderTargetDetails( renderTarget ) {
- const textures = renderTarget.textures;
- const attachments = [];
- const getBPC = ( texture ) => {
- switch ( texture.type ) {
- case ByteType:
- case UnsignedByteType:
- return '8';
- case ShortType:
- case UnsignedShortType:
- case HalfFloatType:
- case UnsignedShort4444Type:
- case UnsignedShort5551Type:
- return '16';
- case IntType:
- case UnsignedIntType:
- case FloatType:
- case UnsignedInt248Type:
- case UnsignedInt5999Type:
- case UnsignedInt101111Type:
- return '32';
- default:
- return '?';
- }
- };
- const getFormat = ( texture ) => {
- switch ( texture.format ) {
- case AlphaFormat:
- return 'a';
- case RedFormat:
- case RedIntegerFormat:
- return 'r';
- case RGFormat:
- case RGIntegerFormat:
- return 'rg';
- case RGBFormat:
- case RGBIntegerFormat:
- return 'rgb';
- case DepthFormat:
- return 'depth';
- case DepthStencilFormat:
- return 'depth-stencil';
- case RGBAFormat:
- case RGBAIntegerFormat:
- default:
- return 'rgba';
- }
- };
- for ( let i = 0; i < textures.length; i ++ ) {
- const texture = textures[ i ];
- const bpc = getBPC( texture );
- const format = getFormat( texture );
- let description = `[${ i }]`;
- if ( texture.name && ! ( texture.isDepthTexture && texture.name === 'depth' ) ) {
- description += ` ${ texture.name }`;
- }
- description += ` ${ format } ${ bpc } bpc`;
- attachments.push( description );
- }
- const details = {
- target: renderTarget.name || 'RenderTarget',
- [ `attachments(${ textures.length })` ]: '\n' + attachments.join( '\n' )
- };
- if ( renderTarget.depthTexture ) {
- details.depth = `${ getBPC( renderTarget.depthTexture ) } bpc`;
- }
- return details;
- }
- getCallDetail( method, args ) {
- switch ( method ) {
- case 'draw': {
- const renderObject = args[ 0 ];
- const details = {
- object: renderObject.object.name || renderObject.object.type,
- material: renderObject.material.name || renderObject.material.type,
- geometry: renderObject.geometry.name || renderObject.geometry.type
- };
- if ( renderObject.getDrawParameters ) {
- const drawParams = renderObject.getDrawParameters();
- if ( drawParams ) {
- if ( renderObject.object.isMesh || renderObject.object.isSprite ) {
- details.triangles = drawParams.vertexCount / 3;
- if ( renderObject.object.count > 1 ) {
- details.instance = renderObject.object.count;
- details[ 'triangles per instance' ] = details.triangles;
- details.triangles *= details.instance;
- }
- }
- }
- }
- return details;
- }
- case 'beginRender': {
- const renderContext = args[ 0 ];
- const details = {
- scene: this.renderer.inspector.currentRender.name || 'unknown',
- camera: renderContext.camera.name || renderContext.camera.type
- };
- if ( renderContext.renderTarget && ! renderContext.renderTarget.isPostProcessingRenderTarget ) {
- Object.assign( details, this.getRenderTargetDetails( renderContext.renderTarget ) );
- } else {
- details.target = 'CanvasTarget';
- }
- return details;
- }
- case 'beginCompute': {
- const details = {
- compute: this.renderer.inspector.currentCompute.name || 'unknown'
- };
- return details;
- }
- case 'compute': {
- const computeNode = args[ 1 ];
- const bindings = args[ 2 ];
- const dispatchSize = args[ 4 ] || computeNode.dispatchSize || computeNode.count;
- const node = computeNode.name || computeNode.type || 'unknown';
- // bindings
- let bindingsCount = 0;
- if ( bindings ) {
- bindingsCount = bindings.length;
- }
- // dispatch
- let dispatch;
- if ( dispatchSize.isIndirectStorageBufferAttribute ) {
- dispatch = 'indirect';
- } else if ( Array.isArray( dispatchSize ) ) {
- dispatch = dispatchSize.join( ', ' );
- } else {
- dispatch = dispatchSize;
- }
- // details
- return {
- node,
- bindings: bindingsCount,
- dispatch
- };
- }
- case 'updateBinding': {
- const binding = args[ 0 ];
- return { group: binding.name || 'unknown' };
- }
- case 'clear': {
- const renderContext = args[ 3 ];
- const details = {
- color: args[ 0 ],
- depth: args[ 1 ],
- stencil: args[ 2 ]
- };
- if ( renderContext.renderTarget && ! renderContext.renderTarget.isPostProcessingRenderTarget ) {
- const renderTargetDetails = this.getRenderTargetDetails( renderContext.renderTarget );
- if ( renderTargetDetails.depth ) {
- renderTargetDetails[ 'depth texture' ] = renderTargetDetails.depth;
- delete renderTargetDetails.depth;
- }
- Object.assign( details, renderTargetDetails );
- } else {
- details.target = 'CanvasTarget';
- }
- return details;
- }
- case 'updateViewport': {
- const renderContext = args[ 0 ];
- const { x, y, width, height } = renderContext.viewportValue;
- return { x, y, width, height };
- }
- case 'updateScissor': {
- const renderContext = args[ 0 ];
- const { x, y, width, height } = renderContext.scissorValue;
- return { x, y, width, height };
- }
- case 'createProgram':
- case 'destroyProgram': {
- const program = args[ 0 ];
- return { stage: program.stage, name: program.name || 'unknown' };
- }
- case 'createRenderPipeline': {
- const renderObject = args[ 0 ];
- const details = {
- object: renderObject.object ? ( renderObject.object.name || renderObject.object.type || 'unknown' ) : 'unknown',
- material: renderObject.material ? ( renderObject.material.name || renderObject.material.type || 'unknown' ) : 'unknown'
- };
- return details;
- }
- case 'createComputePipeline':
- case 'destroyComputePipeline': {
- const pipeline = args[ 0 ];
- return { name: pipeline.name || 'unknown' };
- }
- case 'createBindings':
- case 'updateBindings': {
- const bindGroup = args[ 0 ];
- const details = {
- group: bindGroup.name || 'unknown',
- count: bindGroup.bindings.length
- };
- return details;
- }
- case 'createUniformBuffer':
- case 'destroyUniformBuffer': {
- const binding = args[ 0 ];
- const details = {
- group: binding.groupNode.name || 'unknown',
- size: binding.byteLength + ' bytes'
- };
- if ( binding.name !== details.group ) {
- details.name = binding.name;
- }
- return details;
- }
- case 'createNodeBuilder': {
- const object = args[ 0 ];
- const details = { object: object.name || object.type || 'unknown' };
- if ( object.material ) {
- details.material = object.material.name || object.material.type || 'unknown';
- }
- return details;
- }
- case 'createAttribute':
- case 'createIndexAttribute':
- case 'createStorageAttribute':
- case 'destroyAttribute':
- case 'destroyIndexAttribute':
- case 'destroyStorageAttribute': {
- const attribute = args[ 0 ];
- const details = {};
- if ( attribute.name ) details.name = attribute.name;
- if ( attribute.count !== undefined ) details.count = attribute.count;
- if ( attribute.itemSize !== undefined ) details.itemSize = attribute.itemSize;
- return details;
- }
- case 'copyFramebufferToTexture': {
- const target = args[ 0 ];
- const rectangle = args[ 2 ];
- const details = {
- target: this.getTextureName( target ),
- width: rectangle.z,
- height: rectangle.w
- };
- return details;
- }
- case 'copyTextureToTexture': {
- const srcTexture = args[ 0 ];
- const dstTexture = args[ 1 ];
- const details = {
- source: this.getTextureName( srcTexture ),
- destination: this.getTextureName( dstTexture )
- };
- return details;
- }
- case 'updateSampler': {
- const texture = args[ 0 ];
- const details = {
- magFilter: this.getTextureFilterName( texture.magFilter ),
- minFilter: this.getTextureFilterName( texture.minFilter ),
- wrapS: this.getTextureWrapName( texture.wrapS ),
- wrapT: this.getTextureWrapName( texture.wrapT ),
- anisotropy: texture.anisotropy
- };
- return details;
- }
- case 'updateTexture':
- case 'generateMipmaps':
- case 'createTexture':
- case 'destroyTexture': {
- const texture = args[ 0 ];
- const name = this.getTextureName( texture );
- const details = { texture: name };
- if ( texture.image ) {
- if ( texture.image.width !== undefined ) details.width = texture.image.width;
- if ( texture.image.height !== undefined ) details.height = texture.image.height;
- }
- return details;
- }
- }
- return null;
- }
- getTextureName( texture ) {
- if ( texture.name ) return texture.name;
- const types = [
- 'isFramebufferTexture', 'isDepthTexture', 'isDataArrayTexture',
- 'isData3DTexture', 'isDataTexture', 'isCompressedArrayTexture',
- 'isCompressedTexture', 'isCubeTexture', 'isVideoTexture',
- 'isCanvasTexture', 'isTexture'
- ];
- for ( const type of types ) {
- if ( texture[ type ] ) return type.replace( 'is', '' );
- }
- return 'Texture';
- }
- getTextureFilterName( filter ) {
- const filters = {
- 1003: 'Nearest',
- 1004: 'NearestMipmapNearest',
- 1005: 'NearestMipmapLinear',
- 1006: 'Linear',
- 1007: 'LinearMipmapNearest',
- 1008: 'LinearMipmapLinear'
- };
- return filters[ filter ] || filter;
- }
- getTextureWrapName( wrap ) {
- const wrappings = {
- 1000: 'Repeat',
- 1001: 'ClampToEdge',
- 1002: 'MirroredRepeat'
- };
- return wrappings[ wrap ] || wrap;
- }
- formatDetails( details ) {
- const parts = [];
- for ( const key in details ) {
- if ( details[ key ] !== undefined ) {
- parts.push( `<span style="opacity: 0.5">${key}:</span> <span style="color: var(--text-secondary); opacity: 1">${details[ key ]}</span>` );
- }
- }
- if ( parts.length === 0 ) return '';
- return `<span style="font-size: 11px; margin-left: 8px; color: var(--text-secondary); opacity: 1;">{ ${parts.join( '<span style="opacity: 0.5">, </span>' )} }</span>`;
- }
- renderSlider() {
- if ( this.frames.length === 0 ) {
- this.playhead.style.display = 'none';
- this.frameInfo.textContent = '';
- return;
- }
- // Reset graph safely to fit recorded frames exactly up to maxPoints
- this.graph.lines[ 'calls' ].points = [];
- this.graph.lines[ 'fps' ].points = [];
- this.graph.lines[ 'triangles' ].points = [];
- this.graph.resetLimit();
- // If recorded frames exceed SVG Graph maxPoints, we sample/slice it
- // (Graph.js inherently handles shifting for real-time,
- // but statically we want to visualize as much up to max bounds)
- let framesToRender = this.frames;
- if ( framesToRender.length > this.graph.maxPoints ) {
- framesToRender = framesToRender.slice( - this.graph.maxPoints );
- this.frames = framesToRender; // Adjust our internal array to match what's visible
- }
- let maxTriangles = 0;
- for ( let i = 0; i < framesToRender.length; i ++ ) {
- const t = framesToRender[ i ].triangles || 0;
- if ( t > maxTriangles ) {
- maxTriangles = t;
- }
- }
- for ( let i = 0; i < framesToRender.length; i ++ ) {
- const t = framesToRender[ i ].triangles || 0;
- const normalizedTriangles = maxTriangles > 0 ? ( t / maxTriangles ) * TRIANGLES_GRAPH_LIMIT : 0;
- // Adding calls length to the Graph SVG to visualize workload geometry
- this.graph.addPoint( 'calls', framesToRender[ i ].calls.length );
- this.graph.addPoint( 'fps', framesToRender[ i ].fps || 0 );
- this.graph.addPoint( 'triangles', normalizedTriangles );
- }
- this.graph.update();
- this.playhead.style.display = 'block';
- // Select the previously selected frame, or the last one if tracking, or 0
- let targetFrame = 0;
- if ( this.selectedFrameIndex !== - 1 && this.selectedFrameIndex < this.frames.length ) {
- targetFrame = this.selectedFrameIndex;
- } else if ( this.frames.length > 0 ) {
- targetFrame = this.frames.length - 1;
- }
- this.selectFrame( targetFrame );
- }
- selectFrame( index ) {
- if ( this.isRecording ) return;
- if ( index < 0 || index >= this.frames.length ) return;
- this.selectedFrameIndex = index;
- const frame = this.frames[ index ];
- this.renderTimelineTrack( frame );
- // Update UI texts
- const isCompact = this.profiler.panel.offsetWidth < 800;
- const frameLabel = isCompact ? '' : 'Frame: ';
- const fpsSuffix = isCompact ? '' : ' FPS';
- const callsSuffix = isCompact ? '' : ' calls';
- const trianglesSuffix = isCompact ? '' : ' triangles';
- const group = ( c, text ) => `<span style="display:inline-flex;align-items:center;margin-left:12px;flex-shrink:0;"><span style="width:6px;height:6px;border-radius:50%;background-color:${c};margin-right:6px;flex-shrink:0;"></span>${text}</span>`;
- const maxTriangles = Math.max( this.baseTriangles, frame.triangles || 0 );
- const trianglesText = isCompact ? ( frame.triangles || 0 ) : ( frame.triangles || 0 ) + ' / ' + maxTriangles + trianglesSuffix;
- this.frameInfo.innerHTML = frameLabel + frame.id + group( 'var(--color-fps)', ( frame.fps || 0 ).toFixed( 1 ) + fpsSuffix ) + group( 'var(--color-call)', frame.calls.length + callsSuffix ) + group( 'var(--color-red)', trianglesText );
- // Update playhead position
- const rect = this.graphSlider.getBoundingClientRect();
- const pointCount = this.graph.lines[ 'calls' ].points.length;
- if ( pointCount > 0 ) {
- // Calculate point width step
- const pointStep = rect.width / ( this.graph.maxPoints - 1 );
- let localIndex = index;
- if ( this.frames.length > pointCount ) {
- localIndex = index - ( this.frames.length - pointCount );
- }
- // x offset calculation from SVG update logic
- // The graph translates (slides) back if points length < maxPoints
- // which means point 0 is at offset
- const offset = rect.width - ( ( pointCount - 1 ) * pointStep );
- let xPos = offset + ( localIndex * pointStep );
- xPos = Math.max( 1, Math.min( xPos, rect.width - 1 ) );
- this.playhead.style.left = xPos + 'px';
- this.playhead.style.display = 'block';
- }
- }
- getCallBlock( call, fallbackIndex, instanceIndex = 0 ) {
- const target = call.target;
- let block;
- if ( target && typeof target === 'object' ) {
- let blocks = this.callBlocks.get( target );
- if ( ! blocks ) {
- blocks = [];
- this.callBlocks.set( target, blocks );
- }
- block = blocks[ instanceIndex ];
- } else {
- block = this.fallbackBlocks[ fallbackIndex ];
- }
- if ( ! block ) {
- block = document.createElement( 'div' );
- block.style.display = 'flex';
- block.style.alignItems = 'center';
- block.style.padding = '4px 8px';
- block.style.margin = '2px 0';
- block.style.backgroundColor = 'rgba(255, 255, 255, 0.03)';
- block.style.fontFamily = 'monospace';
- block.style.fontSize = '12px';
- block.style.color = 'var(--text-primary)';
- block.style.overflow = 'hidden';
- block.arrow = document.createElement( 'span' );
- block.arrow.style.fontSize = '10px';
- block.arrow.style.marginRight = '8px';
- block.arrow.style.cursor = 'pointer';
- block.arrow.style.width = '35px';
- block.arrow.style.textAlign = 'center';
- block.arrow.style.flexShrink = '0';
- block.arrow.style.whiteSpace = 'nowrap';
- block.appendChild( block.arrow );
- block.titleSpan = document.createElement( 'span' );
- block.titleSpan.style.flex = '1';
- block.titleSpan.style.minWidth = '0';
- block.titleSpan.style.overflow = 'hidden';
- block.titleSpan.style.textOverflow = 'ellipsis';
- block.titleSpan.style.whiteSpace = 'nowrap';
- block.appendChild( block.titleSpan );
- block.addEventListener( 'click', ( e ) => {
- if ( ! block._groupId ) return;
- e.stopPropagation();
- if ( this.collapsedGroups.has( block._groupId ) ) {
- this.collapsedGroups.delete( block._groupId );
- } else {
- this.collapsedGroups.add( block._groupId );
- }
- this.renderTimelineTrack( this.frames[ this.selectedFrameIndex ] );
- } );
- if ( target && typeof target === 'object' ) {
- this.callBlocks.get( target )[ instanceIndex ] = block;
- } else {
- this.fallbackBlocks[ fallbackIndex ] = block;
- }
- }
- block.style.cursor = 'default';
- block._groupId = null;
- block.arrow.style.display = 'none';
- return block;
- }
- renderTimelineTrack( frame ) {
- if ( this.isRecording ) return;
- if ( ! frame || frame.calls.length === 0 ) {
- this.timelineTrack.innerHTML = '';
- return;
- }
- // Track collapsed states
- if ( ! this.collapsedGroups ) {
- this.collapsedGroups = new Set();
- }
- let blockIndex = 0;
- const trackChildren = this.timelineTrack.children;
- let childIndex = 0;
- const instanceCounts = new WeakMap();
- if ( this.isHierarchicalView ) {
- const groupedCalls = [];
- let currentGroup = null;
- for ( let i = 0; i < frame.calls.length; i ++ ) {
- const call = frame.calls[ i ];
- const isStructural = call.method.startsWith( 'begin' ) || call.method.startsWith( 'finish' );
- const formatedDetails = call.details ? this.formatDetails( call.details ) : '';
- if ( currentGroup && currentGroup.method === call.method && currentGroup.formatedDetails === formatedDetails && ! isStructural ) {
- currentGroup.count ++;
- } else {
- currentGroup = { method: call.method, count: 1, formatedDetails, target: call.target, details: call.details };
- groupedCalls.push( currentGroup );
- }
- }
- let currentIndent = 0;
- const indentSize = 24;
- // Stack to keep track of parent elements and their collapsed state
- const elementStack = [ { element: this.timelineTrack, isCollapsed: false, id: '', beginCount: 0 } ];
- for ( let i = 0; i < groupedCalls.length; i ++ ) {
- const call = groupedCalls[ i ];
- let instanceIndex = 0;
- if ( call.target && typeof call.target === 'object' ) {
- instanceIndex = instanceCounts.get( call.target ) || 0;
- instanceCounts.set( call.target, instanceIndex + 1 );
- }
- const block = this.getCallBlock( call, blockIndex ++, instanceIndex );
- block.style.marginLeft = ( currentIndent * indentSize ) + 'px';
- block.style.borderLeft = '4px solid ' + this.getColorForMethod( call.method );
- // Clean up any old info-icon directly under block
- const directInfoIcon = block.querySelector( ':scope > .info-icon' );
- if ( directInfoIcon ) {
- directInfoIcon.remove();
- }
- // Build titleSpan content
- block.titleSpan.textContent = '';
- const methodSpan = document.createElement( 'span' );
- methodSpan.textContent = call.method;
- block.titleSpan.appendChild( methodSpan );
- if ( call.details ) {
- let tooltipText = `### ${call.method}\n`;
- for ( const key in call.details ) {
- if ( call.details[ key ] !== undefined ) {
- tooltipText += `**${key}**: ${call.details[ key ]}\n`;
- }
- }
- const infoIcon = info( block.titleSpan, tooltipText );
- infoIcon.style.flexShrink = '0';
- infoIcon.style.marginLeft = '6px';
- infoIcon.style.display = 'inline-flex';
- infoIcon.style.verticalAlign = 'middle';
- }
- const detailsAndCountSpan = document.createElement( 'span' );
- let detailsAndCountHTML = ( call.formatedDetails ? call.formatedDetails : '' );
- if ( call.count > 1 ) {
- detailsAndCountHTML += ` <span style="opacity: 0.5">( ${call.count} )</span>`;
- }
- if ( detailsAndCountHTML ) {
- detailsAndCountSpan.innerHTML = detailsAndCountHTML;
- block.titleSpan.appendChild( detailsAndCountSpan );
- }
- const currentParent = elementStack[ elementStack.length - 1 ];
- // Only add to DOM if parent is not collapsed
- if ( ! currentParent.isCollapsed ) {
- if ( trackChildren[ childIndex ] !== block ) {
- this.timelineTrack.insertBefore( block, trackChildren[ childIndex ] );
- }
- childIndex ++;
- }
- if ( call.method.startsWith( 'begin' ) ) {
- const beginIndex = currentParent.beginCount ++;
- const groupId = currentParent.id + '/' + call.method + '-' + beginIndex;
- const isCollapsed = this.collapsedGroups.has( groupId );
- block._groupId = groupId;
- block.style.cursor = 'pointer';
- block.arrow.style.display = 'inline-block';
- block.arrow.textContent = isCollapsed ? '[ + ]' : '[ - ]';
- currentIndent ++;
- elementStack.push( { element: block, isCollapsed: currentParent.isCollapsed || isCollapsed, id: groupId, beginCount: 0 } );
- } else if ( call.method.startsWith( 'finish' ) ) {
- currentIndent = Math.max( 0, currentIndent - 1 );
- elementStack.pop();
- }
- }
- } else {
- const callCounts = {};
- for ( let i = 0; i < frame.calls.length; i ++ ) {
- const method = frame.calls[ i ].method;
- if ( method.startsWith( 'finish' ) ) continue;
- callCounts[ method ] = ( callCounts[ method ] || 0 ) + 1;
- }
- const sortedCalls = Object.keys( callCounts ).map( method => ( { method, count: callCounts[ method ] } ) );
- sortedCalls.sort( ( a, b ) => b.count - a.count );
- for ( let i = 0; i < sortedCalls.length; i ++ ) {
- const call = sortedCalls[ i ];
- const block = this.getCallBlock( call, blockIndex ++ );
- block.style.marginLeft = '0px';
- block.style.borderLeft = '4px solid ' + this.getColorForMethod( call.method );
- const infoIcon = block.querySelector( '.info-icon' );
- if ( infoIcon ) {
- infoIcon.remove();
- }
- block.titleSpan.innerHTML = call.method + ( call.count > 1 ? ` <span style="opacity: 0.5">( ${call.count} )</span>` : '' );
- if ( trackChildren[ childIndex ] !== block ) {
- this.timelineTrack.insertBefore( block, trackChildren[ childIndex ] );
- }
- childIndex ++;
- }
- }
- while ( this.timelineTrack.children.length > childIndex ) {
- this.timelineTrack.removeChild( this.timelineTrack.lastChild );
- }
- }
- getColorForMethod( method ) {
- if ( method.startsWith( 'begin' ) ) return 'var(--color-green)';
- if ( method.startsWith( 'finish' ) || method.startsWith( 'destroy' ) ) return 'var(--color-red)';
- if ( method.startsWith( 'draw' ) || method.startsWith( 'compute' ) || method.startsWith( 'create' ) || method.startsWith( 'generate' ) ) return 'var(--color-yellow)';
- return 'var(--text-secondary)';
- }
- }
- export { Timeline };
|