Timeline.js 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611
  1. import { Tab } from '../ui/Tab.js';
  2. import { Graph } from '../ui/Graph.js';
  3. import { getItem, setItem } from '../Inspector.js';
  4. import {
  5. ByteType,
  6. FloatType,
  7. HalfFloatType,
  8. IntType,
  9. ShortType,
  10. UnsignedByteType,
  11. UnsignedInt101111Type,
  12. UnsignedInt248Type,
  13. UnsignedInt5999Type,
  14. UnsignedIntType,
  15. UnsignedShort4444Type,
  16. UnsignedShort5551Type,
  17. UnsignedShortType,
  18. AlphaFormat,
  19. RGBFormat,
  20. RGBAFormat,
  21. DepthFormat,
  22. DepthStencilFormat,
  23. RedFormat,
  24. RedIntegerFormat,
  25. RGFormat,
  26. RGIntegerFormat,
  27. RGBIntegerFormat,
  28. RGBAIntegerFormat
  29. } from 'three';
  30. const LIMIT = 500;
  31. const TRIANGLES_GRAPH_LIMIT = 60;
  32. class Timeline extends Tab {
  33. constructor( options = {} ) {
  34. super( 'Timeline', options );
  35. this.isRecording = false;
  36. this.frames = []; // Array of { id: number, calls: [] }
  37. this.baseTriangles = 0;
  38. this.currentFrame = null;
  39. this.isHierarchicalView = true;
  40. this.callBlocks = new WeakMap();
  41. this.fallbackBlocks = [];
  42. this.originalBackend = null;
  43. this.originalMethods = new Map();
  44. this.renderer = null;
  45. this.graph = new Graph( LIMIT ); // Accommodate standard graph points
  46. // Make lines in timeline graph
  47. this.graph.addLine( 'fps', 'var( --color-fps )' );
  48. this.graph.addLine( 'calls', 'var( --color-call )' );
  49. this.graph.addLine( 'triangles', 'var( --color-red )' );
  50. const scrollWrapper = document.createElement( 'div' );
  51. scrollWrapper.className = 'list-scroll-wrapper';
  52. this.scrollWrapper = scrollWrapper;
  53. this.content.appendChild( scrollWrapper );
  54. this.buildHeader();
  55. this.buildUI();
  56. // Bind window resize to update graph bounds
  57. window.addEventListener( 'resize', () => {
  58. if ( ! this.isRecording && this.frames.length > 0 ) {
  59. this.renderSlider();
  60. }
  61. } );
  62. }
  63. buildHeader() {
  64. const header = document.createElement( 'div' );
  65. header.className = 'console-header';
  66. this.recordButton = document.createElement( 'button' );
  67. this.recordButton.className = 'console-copy-button'; // Reusing style
  68. this.recordButton.title = 'Record';
  69. 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>';
  70. this.recordButton.style.padding = '0 10px';
  71. this.recordButton.style.lineHeight = '24px'; // Match other buttons height
  72. this.recordButton.style.display = 'flex';
  73. this.recordButton.style.alignItems = 'center';
  74. this.recordButton.addEventListener( 'click', () => this.toggleRecording() );
  75. const clearButton = document.createElement( 'button' );
  76. clearButton.className = 'console-copy-button';
  77. clearButton.title = 'Clear';
  78. 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>';
  79. clearButton.style.padding = '0 10px';
  80. clearButton.style.lineHeight = '24px';
  81. clearButton.style.display = 'flex';
  82. clearButton.style.alignItems = 'center';
  83. clearButton.addEventListener( 'click', () => this.clear() );
  84. this.viewModeButton = document.createElement( 'button' );
  85. this.viewModeButton.className = 'console-copy-button';
  86. this.viewModeButton.title = 'Toggle View Mode';
  87. this.viewModeButton.textContent = 'Mode: Hierarchy';
  88. this.viewModeButton.style.padding = '0 10px';
  89. this.viewModeButton.style.lineHeight = '24px';
  90. this.viewModeButton.addEventListener( 'click', () => {
  91. this.isHierarchicalView = ! this.isHierarchicalView;
  92. this.viewModeButton.textContent = this.isHierarchicalView ? 'Mode: Hierarchy' : 'Mode: Counts';
  93. if ( this.selectedFrameIndex !== undefined && this.selectedFrameIndex !== - 1 ) {
  94. this.selectFrame( this.selectedFrameIndex );
  95. }
  96. } );
  97. this.recordRefreshButton = document.createElement( 'button' );
  98. this.recordRefreshButton.className = 'console-copy-button'; // Reusing style
  99. this.recordRefreshButton.title = 'Refresh & Record';
  100. 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>';
  101. this.recordRefreshButton.style.padding = '0 10px';
  102. this.recordRefreshButton.style.lineHeight = '24px';
  103. this.recordRefreshButton.style.display = 'flex';
  104. this.recordRefreshButton.style.alignItems = 'center';
  105. this.recordRefreshButton.addEventListener( 'click', () => {
  106. const timelineSettings = getItem( 'timeline' );
  107. timelineSettings.recording = true;
  108. setItem( 'timeline', timelineSettings );
  109. window.location.reload();
  110. } );
  111. this.exportButton = document.createElement( 'button' );
  112. this.exportButton.className = 'console-copy-button';
  113. this.exportButton.title = 'Export';
  114. 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>';
  115. this.exportButton.style.padding = '0 10px';
  116. this.exportButton.style.lineHeight = '24px';
  117. this.exportButton.style.display = 'flex';
  118. this.exportButton.style.alignItems = 'center';
  119. this.exportButton.addEventListener( 'click', () => this.exportData() );
  120. const buttonsGroup = document.createElement( 'div' );
  121. buttonsGroup.className = 'console-buttons-group';
  122. buttonsGroup.appendChild( this.viewModeButton );
  123. buttonsGroup.appendChild( this.recordButton );
  124. buttonsGroup.appendChild( this.recordRefreshButton );
  125. buttonsGroup.appendChild( this.exportButton );
  126. buttonsGroup.appendChild( clearButton );
  127. header.style.display = 'flex';
  128. header.style.justifyContent = 'space-between';
  129. header.style.padding = '6px';
  130. header.style.borderBottom = '1px solid var(--border-color)';
  131. const titleElement = document.createElement( 'div' );
  132. titleElement.textContent = 'Backend Calls';
  133. titleElement.style.display = 'flex';
  134. titleElement.style.alignItems = 'center';
  135. titleElement.style.color = 'var(--text-primary)';
  136. titleElement.style.alignSelf = 'center';
  137. titleElement.style.paddingLeft = '5px';
  138. this.frameInfo = document.createElement( 'span' );
  139. this.frameInfo.style.display = 'inline-flex';
  140. this.frameInfo.style.alignItems = 'center';
  141. this.frameInfo.style.marginLeft = '15px';
  142. this.frameInfo.style.fontFamily = 'monospace';
  143. this.frameInfo.style.color = 'var(--text-secondary)';
  144. this.frameInfo.style.fontSize = '12px';
  145. titleElement.appendChild( this.frameInfo );
  146. header.appendChild( titleElement );
  147. header.appendChild( buttonsGroup );
  148. this.scrollWrapper.appendChild( header );
  149. }
  150. buildUI() {
  151. const container = document.createElement( 'div' );
  152. container.style.display = 'flex';
  153. container.style.flexDirection = 'column';
  154. container.style.height = 'calc(100% - 37px)'; // Subtract header height
  155. container.style.width = '100%';
  156. // Top Player/Graph Slider using Graph.js SVG
  157. const graphContainer = document.createElement( 'div' );
  158. graphContainer.style.height = '60px';
  159. graphContainer.style.minHeight = '60px';
  160. graphContainer.style.borderBottom = '1px solid var(--border-color)';
  161. graphContainer.style.backgroundColor = 'var(--background-color)';
  162. this.graphSlider = document.createElement( 'div' );
  163. this.graphSlider.style.height = '100%';
  164. this.graphSlider.style.margin = '0 10px';
  165. this.graphSlider.style.position = 'relative';
  166. this.graphSlider.style.cursor = 'crosshair';
  167. graphContainer.appendChild( this.graphSlider );
  168. // Setup SVG from Graph
  169. this.graph.domElement.style.width = '100%';
  170. this.graph.domElement.style.height = '100%';
  171. this.graphSlider.appendChild( this.graph.domElement );
  172. // Hover indicator
  173. this.hoverIndicator = document.createElement( 'div' );
  174. this.hoverIndicator.style.position = 'absolute';
  175. this.hoverIndicator.style.top = '0';
  176. this.hoverIndicator.style.bottom = '0';
  177. this.hoverIndicator.style.width = '1px';
  178. this.hoverIndicator.style.backgroundColor = 'rgba(255, 255, 255, 0.3)';
  179. this.hoverIndicator.style.pointerEvents = 'none';
  180. this.hoverIndicator.style.display = 'none';
  181. this.hoverIndicator.style.zIndex = '9';
  182. this.hoverIndicator.style.transform = 'translateX(-50%)';
  183. this.graphSlider.appendChild( this.hoverIndicator );
  184. // Playhead indicator (vertical line)
  185. this.playhead = document.createElement( 'div' );
  186. this.playhead.style.position = 'absolute';
  187. this.playhead.style.top = '0';
  188. this.playhead.style.bottom = '0';
  189. this.playhead.style.width = '2px';
  190. this.playhead.style.backgroundColor = 'var(--color-red)';
  191. this.playhead.style.boxShadow = '0 0 4px rgba(255,0,0,0.5)';
  192. this.playhead.style.pointerEvents = 'none';
  193. this.playhead.style.display = 'none';
  194. this.playhead.style.zIndex = '10';
  195. this.playhead.style.transform = 'translateX(-50%)';
  196. this.graphSlider.appendChild( this.playhead );
  197. // Playhead handle (triangle/pointer)
  198. const playheadHandle = document.createElement( 'div' );
  199. playheadHandle.style.position = 'absolute';
  200. playheadHandle.style.top = '0';
  201. playheadHandle.style.left = '50%';
  202. playheadHandle.style.transform = 'translate(-50%, 0)';
  203. playheadHandle.style.width = '0';
  204. playheadHandle.style.height = '0';
  205. playheadHandle.style.borderLeft = '6px solid transparent';
  206. playheadHandle.style.borderRight = '6px solid transparent';
  207. playheadHandle.style.borderTop = '8px solid var(--color-red)';
  208. this.playhead.appendChild( playheadHandle );
  209. // Make it focusable to accept keyboard events
  210. this.graphSlider.tabIndex = 0;
  211. this.graphSlider.style.outline = 'none';
  212. // Mouse interactivity on the graph
  213. let isDragging = false;
  214. const updatePlayheadFromEvent = ( e ) => {
  215. if ( this.frames.length === 0 ) return;
  216. const rect = this.graphSlider.getBoundingClientRect();
  217. let x = e.clientX - rect.left;
  218. // Clamp
  219. x = Math.max( 0, Math.min( x, rect.width ) );
  220. this.fixedScreenX = x;
  221. // The graph stretches its points across the width
  222. // Find closest frame index based on exact point coordinates
  223. const pointCount = this.graph.lines[ 'calls' ].points.length;
  224. if ( pointCount === 0 ) return;
  225. const pointStep = rect.width / ( this.graph.maxPoints - 1 );
  226. const offset = rect.width - ( ( pointCount - 1 ) * pointStep );
  227. let localFrameIndex = Math.round( ( x - offset ) / pointStep );
  228. localFrameIndex = Math.max( 0, Math.min( localFrameIndex, pointCount - 1 ) );
  229. if ( localFrameIndex >= pointCount - 2 ) {
  230. this.isTrackingLatest = true;
  231. } else {
  232. this.isTrackingLatest = false;
  233. }
  234. let frameIndex = localFrameIndex;
  235. if ( this.frames.length > pointCount ) {
  236. frameIndex += this.frames.length - pointCount;
  237. }
  238. this.playhead.style.display = 'block';
  239. this.selectFrame( frameIndex );
  240. };
  241. this.graphSlider.addEventListener( 'mousedown', ( e ) => {
  242. isDragging = true;
  243. this.isManualScrubbing = true;
  244. this.graphSlider.focus();
  245. updatePlayheadFromEvent( e );
  246. } );
  247. this.graphSlider.addEventListener( 'mouseenter', () => {
  248. if ( this.frames.length > 0 && ! this.isRecording ) {
  249. this.hoverIndicator.style.display = 'block';
  250. }
  251. } );
  252. this.graphSlider.addEventListener( 'mouseleave', () => {
  253. this.hoverIndicator.style.display = 'none';
  254. } );
  255. this.graphSlider.addEventListener( 'mousemove', ( e ) => {
  256. if ( this.frames.length === 0 || this.isRecording ) return;
  257. const rect = this.graphSlider.getBoundingClientRect();
  258. let x = e.clientX - rect.left;
  259. x = Math.max( 0, Math.min( x, rect.width ) );
  260. const pointCount = this.graph.lines[ 'calls' ].points.length;
  261. if ( pointCount > 0 ) {
  262. const pointStep = rect.width / ( this.graph.maxPoints - 1 );
  263. const offset = rect.width - ( ( pointCount - 1 ) * pointStep );
  264. let localFrameIndex = Math.round( ( x - offset ) / pointStep );
  265. localFrameIndex = Math.max( 0, Math.min( localFrameIndex, pointCount - 1 ) );
  266. let snappedX = offset + localFrameIndex * pointStep;
  267. snappedX = Math.max( 1, Math.min( snappedX, rect.width - 1 ) );
  268. this.hoverIndicator.style.left = snappedX + 'px';
  269. } else {
  270. const clampedX = Math.max( 1, Math.min( x, rect.width - 1 ) );
  271. this.hoverIndicator.style.left = clampedX + 'px';
  272. }
  273. } );
  274. this.graphSlider.addEventListener( 'keydown', ( e ) => {
  275. if ( this.frames.length === 0 || this.isRecording ) return;
  276. let newIndex = this.selectedFrameIndex;
  277. if ( e.key === 'ArrowLeft' ) {
  278. newIndex = Math.max( 0, this.selectedFrameIndex - 1 );
  279. e.preventDefault();
  280. } else if ( e.key === 'ArrowRight' ) {
  281. newIndex = Math.min( this.frames.length - 1, this.selectedFrameIndex + 1 );
  282. e.preventDefault();
  283. }
  284. if ( newIndex !== this.selectedFrameIndex ) {
  285. this.selectFrame( newIndex );
  286. // Update playhead tracking state
  287. const pointCount = this.graph.lines[ 'calls' ].points.length;
  288. if ( pointCount > 0 ) {
  289. let localIndex = newIndex;
  290. if ( this.frames.length > pointCount ) {
  291. localIndex = newIndex - ( this.frames.length - pointCount );
  292. }
  293. if ( localIndex >= pointCount - 2 ) {
  294. this.isTrackingLatest = true;
  295. } else {
  296. this.isTrackingLatest = false;
  297. }
  298. const rect = this.graphSlider.getBoundingClientRect();
  299. const pointStep = rect.width / ( this.graph.maxPoints - 1 );
  300. const offset = rect.width - ( ( pointCount - 1 ) * pointStep );
  301. this.fixedScreenX = offset + localIndex * pointStep;
  302. }
  303. }
  304. } );
  305. window.addEventListener( 'mousemove', ( e ) => {
  306. if ( isDragging ) {
  307. updatePlayheadFromEvent( e );
  308. // Also move hover indicator to match playback
  309. const rect = this.graphSlider.getBoundingClientRect();
  310. let x = e.clientX - rect.left;
  311. x = Math.max( 0, Math.min( x, rect.width ) );
  312. const pointCount = this.graph.lines[ 'calls' ].points.length;
  313. if ( pointCount > 0 ) {
  314. const pointStep = rect.width / ( this.graph.maxPoints - 1 );
  315. const offset = rect.width - ( ( pointCount - 1 ) * pointStep );
  316. let localFrameIndex = Math.round( ( x - offset ) / pointStep );
  317. localFrameIndex = Math.max( 0, Math.min( localFrameIndex, pointCount - 1 ) );
  318. let snappedX = offset + localFrameIndex * pointStep;
  319. snappedX = Math.max( 1, Math.min( snappedX, rect.width - 1 ) );
  320. this.hoverIndicator.style.left = snappedX + 'px';
  321. } else {
  322. const clampedX = Math.max( 1, Math.min( x, rect.width - 1 ) );
  323. this.hoverIndicator.style.left = clampedX + 'px';
  324. }
  325. }
  326. } );
  327. window.addEventListener( 'mouseup', () => {
  328. isDragging = false;
  329. this.isManualScrubbing = false;
  330. } );
  331. container.appendChild( graphContainer );
  332. // Bottom Main Area (Timeline Sequence)
  333. const mainArea = document.createElement( 'div' );
  334. mainArea.style.flex = '1';
  335. mainArea.style.display = 'flex';
  336. mainArea.style.flexDirection = 'column';
  337. mainArea.style.overflow = 'hidden';
  338. // Timeline Track
  339. this.timelineTrack = document.createElement( 'div' );
  340. this.timelineTrack.style.flex = '1';
  341. this.timelineTrack.style.overflowY = 'auto';
  342. this.timelineTrack.style.margin = '10px';
  343. this.timelineTrack.style.backgroundColor = 'var(--background-color)';
  344. mainArea.appendChild( this.timelineTrack );
  345. container.appendChild( mainArea );
  346. this.scrollWrapper.appendChild( container );
  347. }
  348. setRenderer( renderer ) {
  349. this.renderer = renderer;
  350. const timelineSettings = getItem( 'timeline' );
  351. if ( timelineSettings.recording ) {
  352. timelineSettings.recording = false;
  353. setItem( 'timeline', timelineSettings );
  354. this.toggleRecording();
  355. }
  356. }
  357. toggleRecording() {
  358. if ( ! this.renderer ) {
  359. console.warn( 'Timeline: No renderer defined.' );
  360. return;
  361. }
  362. this.isRecording = ! this.isRecording;
  363. if ( this.isRecording ) {
  364. this.recordButton.title = 'Stop';
  365. 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>';
  366. this.recordButton.style.color = 'var(--color-red)';
  367. this.startRecording();
  368. } else {
  369. this.recordButton.title = 'Record';
  370. 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>';
  371. this.recordButton.style.color = '';
  372. this.stopRecording();
  373. this.renderSlider();
  374. }
  375. }
  376. startRecording() {
  377. this.frames = [];
  378. this.currentFrame = null;
  379. this.selectedFrameIndex = - 1;
  380. this.fixedScreenX = 0;
  381. this.isTrackingLatest = true;
  382. this.isManualScrubbing = false;
  383. this.clear();
  384. this.frameInfo.textContent = 'Recording...';
  385. const backend = this.renderer.backend;
  386. const methods = Object.getOwnPropertyNames( Object.getPrototypeOf( backend ) ).filter( prop => prop !== 'constructor' );
  387. for ( const prop of methods ) {
  388. const descriptor = Object.getOwnPropertyDescriptor( Object.getPrototypeOf( backend ), prop );
  389. if ( descriptor && ( descriptor.get || descriptor.set ) ) continue;
  390. const originalFunc = backend[ prop ];
  391. if ( typeof originalFunc === 'function' && typeof prop === 'string' ) {
  392. this.originalMethods.set( prop, originalFunc );
  393. backend[ prop ] = ( ...args ) => {
  394. if ( prop.toLowerCase().includes( 'timestamp' ) || prop.startsWith( 'get' ) || prop.startsWith( 'set' ) || prop.startsWith( 'has' ) || prop.startsWith( '_' ) || prop.startsWith( 'needs' ) ) {
  395. return originalFunc.apply( backend, args );
  396. }
  397. // Check for frame change
  398. const frameNumber = this.renderer.info.frame;
  399. if ( ! this.currentFrame || this.currentFrame.id !== frameNumber ) {
  400. if ( this.currentFrame ) {
  401. this.currentFrame.fps = this.renderer.inspector ? this.renderer.inspector.fps : 0;
  402. if ( ! isFinite( this.currentFrame.fps ) ) {
  403. this.currentFrame.fps = 0;
  404. }
  405. const t = this.currentFrame.triangles || 0;
  406. if ( t > this.baseTriangles ) {
  407. const oldBase = this.baseTriangles;
  408. this.baseTriangles = t;
  409. if ( oldBase > 0 ) {
  410. const ratio = oldBase / this.baseTriangles;
  411. const points = this.graph.lines[ 'triangles' ].points;
  412. for ( let i = 0; i < points.length; i ++ ) {
  413. points[ i ] *= ratio;
  414. }
  415. }
  416. }
  417. const normalizedTriangles = this.baseTriangles > 0 ? ( t / this.baseTriangles ) * TRIANGLES_GRAPH_LIMIT : 0;
  418. this.graph.addPoint( 'calls', this.currentFrame.calls.length );
  419. this.graph.addPoint( 'fps', this.currentFrame.fps );
  420. this.graph.addPoint( 'triangles', normalizedTriangles );
  421. this.graph.update();
  422. }
  423. this.currentFrame = { id: frameNumber, calls: [], fps: 0, triangles: 0 };
  424. this.frames.push( this.currentFrame );
  425. if ( this.frames.length > LIMIT ) {
  426. this.frames.shift();
  427. }
  428. // Sync playhead when new frames are added if user is actively watching a frame
  429. if ( ! this.isManualScrubbing ) {
  430. if ( this.isTrackingLatest ) {
  431. const targetIndex = this.frames.length > 1 ? this.frames.length - 2 : 0;
  432. this.selectFrame( targetIndex );
  433. } else if ( this.selectedFrameIndex !== - 1 ) {
  434. const pointCount = this.graph.lines[ 'calls' ].points.length;
  435. if ( pointCount > 0 ) {
  436. const rect = this.graphSlider.getBoundingClientRect();
  437. const pointStep = rect.width / ( this.graph.maxPoints - 1 );
  438. const offset = rect.width - ( ( pointCount - 1 ) * pointStep );
  439. let localFrameIndex = Math.round( ( this.fixedScreenX - offset ) / pointStep );
  440. localFrameIndex = Math.max( 0, Math.min( localFrameIndex, pointCount - 1 ) );
  441. let newFrameIndex = localFrameIndex;
  442. if ( this.frames.length > pointCount ) {
  443. newFrameIndex += this.frames.length - pointCount;
  444. }
  445. this.selectFrame( newFrameIndex );
  446. }
  447. }
  448. }
  449. }
  450. const call = { method: prop, target: args[ 0 ] };
  451. const details = this.getCallDetail( prop, args );
  452. if ( details ) {
  453. call.details = details;
  454. if ( details.triangles !== undefined ) {
  455. this.currentFrame.triangles += details.triangles;
  456. }
  457. }
  458. this.currentFrame.calls.push( call );
  459. return originalFunc.apply( backend, args );
  460. };
  461. }
  462. }
  463. }
  464. stopRecording() {
  465. if ( this.originalMethods.size > 0 ) {
  466. const backend = this.renderer.backend;
  467. for ( const [ prop, originalFunc ] of this.originalMethods.entries() ) {
  468. backend[ prop ] = originalFunc;
  469. }
  470. this.originalMethods.clear();
  471. if ( this.currentFrame ) {
  472. this.currentFrame.fps = this.renderer.inspector ? this.renderer.inspector.fps : 0;
  473. }
  474. }
  475. }
  476. clear() {
  477. this.frames = [];
  478. this.timelineTrack.innerHTML = '';
  479. this.playhead.style.display = 'none';
  480. this.frameInfo.textContent = '';
  481. this.baseTriangles = 0;
  482. this.graph.lines[ 'calls' ].points = [];
  483. this.graph.lines[ 'fps' ].points = [];
  484. this.graph.lines[ 'triangles' ].points = [];
  485. this.graph.resetLimit();
  486. this.graph.update();
  487. }
  488. exportData() {
  489. if ( this.frames.length === 0 ) return;
  490. const data = JSON.stringify( this.frames, null, '\t' );
  491. const blob = new Blob( [ data ], { type: 'application/json' } );
  492. const url = URL.createObjectURL( blob );
  493. const a = document.createElement( 'a' );
  494. a.href = url;
  495. a.download = 'threejs-timeline.json';
  496. a.click();
  497. URL.revokeObjectURL( url );
  498. }
  499. getRenderTargetDetails( renderTarget ) {
  500. const textures = renderTarget.textures;
  501. const attachments = [];
  502. const getBPC = ( texture ) => {
  503. switch ( texture.type ) {
  504. case ByteType:
  505. case UnsignedByteType:
  506. return '8';
  507. case ShortType:
  508. case UnsignedShortType:
  509. case HalfFloatType:
  510. case UnsignedShort4444Type:
  511. case UnsignedShort5551Type:
  512. return '16';
  513. case IntType:
  514. case UnsignedIntType:
  515. case FloatType:
  516. case UnsignedInt248Type:
  517. case UnsignedInt5999Type:
  518. case UnsignedInt101111Type:
  519. return '32';
  520. default:
  521. return '?';
  522. }
  523. };
  524. const getFormat = ( texture ) => {
  525. switch ( texture.format ) {
  526. case AlphaFormat:
  527. return 'a';
  528. case RedFormat:
  529. case RedIntegerFormat:
  530. return 'r';
  531. case RGFormat:
  532. case RGIntegerFormat:
  533. return 'rg';
  534. case RGBFormat:
  535. case RGBIntegerFormat:
  536. return 'rgb';
  537. case DepthFormat:
  538. return 'depth';
  539. case DepthStencilFormat:
  540. return 'depth-stencil';
  541. case RGBAFormat:
  542. case RGBAIntegerFormat:
  543. default:
  544. return 'rgba';
  545. }
  546. };
  547. for ( let i = 0; i < textures.length; i ++ ) {
  548. const texture = textures[ i ];
  549. const bpc = getBPC( texture );
  550. const format = getFormat( texture );
  551. let description = `[${ i }]`;
  552. if ( texture.name && ! ( texture.isDepthTexture && texture.name === 'depth' ) ) {
  553. description += ` ${ texture.name }`;
  554. }
  555. description += ` ${ format } ${ bpc } bpc`;
  556. attachments.push( description );
  557. }
  558. const details = {
  559. target: renderTarget.name || 'RenderTarget',
  560. [ `attachments(${ textures.length })` ]: attachments.join( ', ' )
  561. };
  562. if ( renderTarget.depthTexture ) {
  563. details.depth = `${ getBPC( renderTarget.depthTexture ) } bpc`;
  564. }
  565. return details;
  566. }
  567. getCallDetail( method, args ) {
  568. switch ( method ) {
  569. case 'draw': {
  570. const renderObject = args[ 0 ];
  571. const details = {
  572. object: renderObject.object.name || renderObject.object.type,
  573. material: renderObject.material.name || renderObject.material.type,
  574. geometry: renderObject.geometry.name || renderObject.geometry.type
  575. };
  576. if ( renderObject.getDrawParameters ) {
  577. const drawParams = renderObject.getDrawParameters();
  578. if ( drawParams ) {
  579. if ( renderObject.object.isMesh || renderObject.object.isSprite ) {
  580. details.triangles = drawParams.vertexCount / 3;
  581. if ( renderObject.object.count > 1 ) {
  582. details.instance = renderObject.object.count;
  583. details[ 'triangles per instance' ] = details.triangles;
  584. details.triangles *= details.instance;
  585. }
  586. }
  587. }
  588. }
  589. return details;
  590. }
  591. case 'beginRender': {
  592. const renderContext = args[ 0 ];
  593. const details = {
  594. scene: this.renderer.inspector.currentRender.name || 'unknown',
  595. camera: renderContext.camera.name || renderContext.camera.type
  596. };
  597. if ( renderContext.renderTarget && ! renderContext.renderTarget.isPostProcessingRenderTarget ) {
  598. Object.assign( details, this.getRenderTargetDetails( renderContext.renderTarget ) );
  599. } else {
  600. details.target = 'CanvasTarget';
  601. }
  602. return details;
  603. }
  604. case 'beginCompute': {
  605. const details = {
  606. compute: this.renderer.inspector.currentCompute.name || 'unknown'
  607. };
  608. return details;
  609. }
  610. case 'compute': {
  611. const computeNode = args[ 1 ];
  612. const bindings = args[ 2 ];
  613. const dispatchSize = args[ 4 ] || computeNode.dispatchSize || computeNode.count;
  614. const node = computeNode.name || computeNode.type || 'unknown';
  615. // bindings
  616. let bindingsCount = 0;
  617. if ( bindings ) {
  618. bindingsCount = bindings.length;
  619. }
  620. // dispatch
  621. let dispatch;
  622. if ( dispatchSize.isIndirectStorageBufferAttribute ) {
  623. dispatch = 'indirect';
  624. } else if ( Array.isArray( dispatchSize ) ) {
  625. dispatch = dispatchSize.join( ', ' );
  626. } else {
  627. dispatch = dispatchSize;
  628. }
  629. // details
  630. return {
  631. node,
  632. bindings: bindingsCount,
  633. dispatch
  634. };
  635. }
  636. case 'updateBinding': {
  637. const binding = args[ 0 ];
  638. return { group: binding.name || 'unknown' };
  639. }
  640. case 'clear': {
  641. const renderContext = args[ 3 ];
  642. const details = {
  643. color: args[ 0 ],
  644. depth: args[ 1 ],
  645. stencil: args[ 2 ]
  646. };
  647. if ( renderContext.renderTarget && ! renderContext.renderTarget.isPostProcessingRenderTarget ) {
  648. const renderTargetDetails = this.getRenderTargetDetails( renderContext.renderTarget );
  649. if ( renderTargetDetails.depth ) {
  650. renderTargetDetails[ 'depth texture' ] = renderTargetDetails.depth;
  651. delete renderTargetDetails.depth;
  652. }
  653. Object.assign( details, renderTargetDetails );
  654. } else {
  655. details.target = 'CanvasTarget';
  656. }
  657. return details;
  658. }
  659. case 'updateViewport': {
  660. const renderContext = args[ 0 ];
  661. const { x, y, width, height } = renderContext.viewportValue;
  662. return { x, y, width, height };
  663. }
  664. case 'updateScissor': {
  665. const renderContext = args[ 0 ];
  666. const { x, y, width, height } = renderContext.scissorValue;
  667. return { x, y, width, height };
  668. }
  669. case 'createProgram':
  670. case 'destroyProgram': {
  671. const program = args[ 0 ];
  672. return { stage: program.stage, name: program.name || 'unknown' };
  673. }
  674. case 'createRenderPipeline': {
  675. const renderObject = args[ 0 ];
  676. const details = {
  677. object: renderObject.object ? ( renderObject.object.name || renderObject.object.type || 'unknown' ) : 'unknown',
  678. material: renderObject.material ? ( renderObject.material.name || renderObject.material.type || 'unknown' ) : 'unknown'
  679. };
  680. return details;
  681. }
  682. case 'createComputePipeline':
  683. case 'destroyComputePipeline': {
  684. const pipeline = args[ 0 ];
  685. return { name: pipeline.name || 'unknown' };
  686. }
  687. case 'createBindings':
  688. case 'updateBindings': {
  689. const bindGroup = args[ 0 ];
  690. const details = { group: bindGroup.name || 'unknown' };
  691. if ( bindGroup.bindings ) {
  692. details.count = bindGroup.bindings.length;
  693. }
  694. return details;
  695. }
  696. case 'createNodeBuilder': {
  697. const object = args[ 0 ];
  698. const details = { object: object.name || object.type || 'unknown' };
  699. if ( object.material ) {
  700. details.material = object.material.name || object.material.type || 'unknown';
  701. }
  702. return details;
  703. }
  704. case 'createAttribute':
  705. case 'createIndexAttribute':
  706. case 'createStorageAttribute':
  707. case 'destroyAttribute':
  708. case 'destroyIndexAttribute':
  709. case 'destroyStorageAttribute': {
  710. const attribute = args[ 0 ];
  711. const details = {};
  712. if ( attribute.name ) details.name = attribute.name;
  713. if ( attribute.count !== undefined ) details.count = attribute.count;
  714. if ( attribute.itemSize !== undefined ) details.itemSize = attribute.itemSize;
  715. return details;
  716. }
  717. case 'copyFramebufferToTexture': {
  718. const target = args[ 0 ];
  719. const rectangle = args[ 2 ];
  720. const details = {
  721. target: this.getTextureName( target ),
  722. width: rectangle.z,
  723. height: rectangle.w
  724. };
  725. return details;
  726. }
  727. case 'copyTextureToTexture': {
  728. const srcTexture = args[ 0 ];
  729. const dstTexture = args[ 1 ];
  730. const details = {
  731. source: this.getTextureName( srcTexture ),
  732. destination: this.getTextureName( dstTexture )
  733. };
  734. return details;
  735. }
  736. case 'updateSampler': {
  737. const texture = args[ 0 ];
  738. const details = {
  739. magFilter: this.getTextureFilterName( texture.magFilter ),
  740. minFilter: this.getTextureFilterName( texture.minFilter ),
  741. wrapS: this.getTextureWrapName( texture.wrapS ),
  742. wrapT: this.getTextureWrapName( texture.wrapT ),
  743. anisotropy: texture.anisotropy
  744. };
  745. return details;
  746. }
  747. case 'updateTexture':
  748. case 'generateMipmaps':
  749. case 'createTexture':
  750. case 'destroyTexture': {
  751. const texture = args[ 0 ];
  752. const name = this.getTextureName( texture );
  753. const details = { texture: name };
  754. if ( texture.image ) {
  755. if ( texture.image.width !== undefined ) details.width = texture.image.width;
  756. if ( texture.image.height !== undefined ) details.height = texture.image.height;
  757. }
  758. return details;
  759. }
  760. }
  761. return null;
  762. }
  763. getTextureName( texture ) {
  764. if ( texture.name ) return texture.name;
  765. const types = [
  766. 'isFramebufferTexture', 'isDepthTexture', 'isDataArrayTexture',
  767. 'isData3DTexture', 'isDataTexture', 'isCompressedArrayTexture',
  768. 'isCompressedTexture', 'isCubeTexture', 'isVideoTexture',
  769. 'isCanvasTexture', 'isTexture'
  770. ];
  771. for ( const type of types ) {
  772. if ( texture[ type ] ) return type.replace( 'is', '' );
  773. }
  774. return 'Texture';
  775. }
  776. getTextureFilterName( filter ) {
  777. const filters = {
  778. 1003: 'Nearest',
  779. 1004: 'NearestMipmapNearest',
  780. 1005: 'NearestMipmapLinear',
  781. 1006: 'Linear',
  782. 1007: 'LinearMipmapNearest',
  783. 1008: 'LinearMipmapLinear'
  784. };
  785. return filters[ filter ] || filter;
  786. }
  787. getTextureWrapName( wrap ) {
  788. const wrappings = {
  789. 1000: 'Repeat',
  790. 1001: 'ClampToEdge',
  791. 1002: 'MirroredRepeat'
  792. };
  793. return wrappings[ wrap ] || wrap;
  794. }
  795. formatDetails( details ) {
  796. const parts = [];
  797. for ( const key in details ) {
  798. if ( details[ key ] !== undefined ) {
  799. parts.push( `<span style="opacity: 0.5">${key}:</span> <span style="color: var(--text-secondary); opacity: 1">${details[ key ]}</span>` );
  800. }
  801. }
  802. if ( parts.length === 0 ) return '';
  803. return `<span style="font-size: 11px; margin-left: 8px; color: var(--text-secondary); opacity: 1;">{ ${parts.join( '<span style="opacity: 0.5">, </span>' )} }</span>`;
  804. }
  805. renderSlider() {
  806. if ( this.frames.length === 0 ) {
  807. this.playhead.style.display = 'none';
  808. this.frameInfo.textContent = '';
  809. return;
  810. }
  811. // Reset graph safely to fit recorded frames exactly up to maxPoints
  812. this.graph.lines[ 'calls' ].points = [];
  813. this.graph.lines[ 'fps' ].points = [];
  814. this.graph.lines[ 'triangles' ].points = [];
  815. this.graph.resetLimit();
  816. // If recorded frames exceed SVG Graph maxPoints, we sample/slice it
  817. // (Graph.js inherently handles shifting for real-time,
  818. // but statically we want to visualize as much up to max bounds)
  819. let framesToRender = this.frames;
  820. if ( framesToRender.length > this.graph.maxPoints ) {
  821. framesToRender = framesToRender.slice( - this.graph.maxPoints );
  822. this.frames = framesToRender; // Adjust our internal array to match what's visible
  823. }
  824. let maxTriangles = 0;
  825. for ( let i = 0; i < framesToRender.length; i ++ ) {
  826. const t = framesToRender[ i ].triangles || 0;
  827. if ( t > maxTriangles ) {
  828. maxTriangles = t;
  829. }
  830. }
  831. for ( let i = 0; i < framesToRender.length; i ++ ) {
  832. const t = framesToRender[ i ].triangles || 0;
  833. const normalizedTriangles = maxTriangles > 0 ? ( t / maxTriangles ) * TRIANGLES_GRAPH_LIMIT : 0;
  834. // Adding calls length to the Graph SVG to visualize workload geometry
  835. this.graph.addPoint( 'calls', framesToRender[ i ].calls.length );
  836. this.graph.addPoint( 'fps', framesToRender[ i ].fps || 0 );
  837. this.graph.addPoint( 'triangles', normalizedTriangles );
  838. }
  839. this.graph.update();
  840. this.playhead.style.display = 'block';
  841. // Select the previously selected frame, or the last one if tracking, or 0
  842. let targetFrame = 0;
  843. if ( this.selectedFrameIndex !== - 1 && this.selectedFrameIndex < this.frames.length ) {
  844. targetFrame = this.selectedFrameIndex;
  845. } else if ( this.frames.length > 0 ) {
  846. targetFrame = this.frames.length - 1;
  847. }
  848. this.selectFrame( targetFrame );
  849. }
  850. selectFrame( index ) {
  851. if ( index < 0 || index >= this.frames.length ) return;
  852. this.selectedFrameIndex = index;
  853. const frame = this.frames[ index ];
  854. this.renderTimelineTrack( frame );
  855. // Update UI texts
  856. const group = ( c, text ) => `<span style="display:inline-flex;align-items:center;margin-left:12px;"><span style="width:6px;height:6px;border-radius:50%;background-color:${c};margin-right:6px;"></span>${text}</span>`;
  857. const maxTriangles = Math.max( this.baseTriangles, frame.triangles || 0 );
  858. this.frameInfo.innerHTML = 'Frame: ' + frame.id + group( 'var(--color-fps)', ( frame.fps || 0 ).toFixed( 1 ) + ' FPS' ) + group( 'var(--color-call)', frame.calls.length + ' calls' ) + group( 'var(--color-red)', ( frame.triangles || 0 ) + ' / ' + maxTriangles + ' triangles' );
  859. // Update playhead position
  860. const rect = this.graphSlider.getBoundingClientRect();
  861. const pointCount = this.graph.lines[ 'calls' ].points.length;
  862. if ( pointCount > 0 ) {
  863. // Calculate point width step
  864. const pointStep = rect.width / ( this.graph.maxPoints - 1 );
  865. let localIndex = index;
  866. if ( this.frames.length > pointCount ) {
  867. localIndex = index - ( this.frames.length - pointCount );
  868. }
  869. // x offset calculation from SVG update logic
  870. // The graph translates (slides) back if points length < maxPoints
  871. // which means point 0 is at offset
  872. const offset = rect.width - ( ( pointCount - 1 ) * pointStep );
  873. let xPos = offset + ( localIndex * pointStep );
  874. xPos = Math.max( 1, Math.min( xPos, rect.width - 1 ) );
  875. this.playhead.style.left = xPos + 'px';
  876. this.playhead.style.display = 'block';
  877. }
  878. }
  879. getCallBlock( call, fallbackIndex, instanceIndex = 0 ) {
  880. const target = call.target;
  881. let block;
  882. if ( target && typeof target === 'object' ) {
  883. let blocks = this.callBlocks.get( target );
  884. if ( ! blocks ) {
  885. blocks = [];
  886. this.callBlocks.set( target, blocks );
  887. }
  888. block = blocks[ instanceIndex ];
  889. } else {
  890. block = this.fallbackBlocks[ fallbackIndex ];
  891. }
  892. if ( ! block ) {
  893. block = document.createElement( 'div' );
  894. block.style.padding = '4px 8px';
  895. block.style.margin = '2px 0';
  896. block.style.backgroundColor = 'rgba(255, 255, 255, 0.03)';
  897. block.style.fontFamily = 'monospace';
  898. block.style.fontSize = '12px';
  899. block.style.color = 'var(--text-primary)';
  900. block.style.whiteSpace = 'nowrap';
  901. block.style.overflow = 'hidden';
  902. block.style.textOverflow = 'ellipsis';
  903. block.style.display = 'flex';
  904. block.style.alignItems = 'center';
  905. block.arrow = document.createElement( 'span' );
  906. block.arrow.style.fontSize = '10px';
  907. block.arrow.style.marginRight = '10px';
  908. block.arrow.style.cursor = 'pointer';
  909. block.arrow.style.width = '26px';
  910. block.arrow.style.textAlign = 'center';
  911. block.appendChild( block.arrow );
  912. block.titleSpan = document.createElement( 'span' );
  913. block.appendChild( block.titleSpan );
  914. block.addEventListener( 'click', ( e ) => {
  915. if ( ! block._groupId ) return;
  916. e.stopPropagation();
  917. if ( this.collapsedGroups.has( block._groupId ) ) {
  918. this.collapsedGroups.delete( block._groupId );
  919. } else {
  920. this.collapsedGroups.add( block._groupId );
  921. }
  922. this.renderTimelineTrack( this.frames[ this.selectedFrameIndex ] );
  923. } );
  924. if ( target && typeof target === 'object' ) {
  925. this.callBlocks.get( target )[ instanceIndex ] = block;
  926. } else {
  927. this.fallbackBlocks[ fallbackIndex ] = block;
  928. }
  929. }
  930. block.style.cursor = 'default';
  931. block._groupId = null;
  932. block.arrow.style.display = 'none';
  933. return block;
  934. }
  935. renderTimelineTrack( frame ) {
  936. if ( ! frame || frame.calls.length === 0 ) {
  937. this.timelineTrack.innerHTML = '';
  938. return;
  939. }
  940. // Track collapsed states
  941. if ( ! this.collapsedGroups ) {
  942. this.collapsedGroups = new Set();
  943. }
  944. let blockIndex = 0;
  945. const trackChildren = this.timelineTrack.children;
  946. let childIndex = 0;
  947. const instanceCounts = new WeakMap();
  948. if ( this.isHierarchicalView ) {
  949. const groupedCalls = [];
  950. let currentGroup = null;
  951. for ( let i = 0; i < frame.calls.length; i ++ ) {
  952. const call = frame.calls[ i ];
  953. const isStructural = call.method.startsWith( 'begin' ) || call.method.startsWith( 'finish' );
  954. const formatedDetails = call.details ? this.formatDetails( call.details ) : '';
  955. if ( currentGroup && currentGroup.method === call.method && currentGroup.formatedDetails === formatedDetails && ! isStructural ) {
  956. currentGroup.count ++;
  957. } else {
  958. currentGroup = { method: call.method, count: 1, formatedDetails, target: call.target };
  959. groupedCalls.push( currentGroup );
  960. }
  961. }
  962. let currentIndent = 0;
  963. const indentSize = 24;
  964. // Stack to keep track of parent elements and their collapsed state
  965. const elementStack = [ { element: this.timelineTrack, isCollapsed: false, id: '', beginCount: 0 } ];
  966. for ( let i = 0; i < groupedCalls.length; i ++ ) {
  967. const call = groupedCalls[ i ];
  968. let instanceIndex = 0;
  969. if ( call.target && typeof call.target === 'object' ) {
  970. instanceIndex = instanceCounts.get( call.target ) || 0;
  971. instanceCounts.set( call.target, instanceIndex + 1 );
  972. }
  973. const block = this.getCallBlock( call, blockIndex ++, instanceIndex );
  974. block.style.marginLeft = ( currentIndent * indentSize ) + 'px';
  975. block.style.borderLeft = '4px solid ' + this.getColorForMethod( call.method );
  976. const currentParent = elementStack[ elementStack.length - 1 ];
  977. // Only add to DOM if parent is not collapsed
  978. if ( ! currentParent.isCollapsed ) {
  979. if ( trackChildren[ childIndex ] !== block ) {
  980. this.timelineTrack.insertBefore( block, trackChildren[ childIndex ] );
  981. }
  982. childIndex ++;
  983. }
  984. if ( call.method.startsWith( 'begin' ) ) {
  985. const beginIndex = currentParent.beginCount ++;
  986. const groupId = currentParent.id + '/' + call.method + '-' + beginIndex;
  987. const isCollapsed = this.collapsedGroups.has( groupId );
  988. block._groupId = groupId;
  989. block.style.cursor = 'pointer';
  990. block.arrow.style.display = 'inline-block';
  991. block.arrow.textContent = isCollapsed ? '[ + ]' : '[ - ]';
  992. block.titleSpan.innerHTML = call.method + ( call.formatedDetails ? call.formatedDetails : '' ) + ( call.count > 1 ? ` <span style="opacity: 0.5">( ${call.count} )</span>` : '' );
  993. currentIndent ++;
  994. elementStack.push( { element: block, isCollapsed: currentParent.isCollapsed || isCollapsed, id: groupId, beginCount: 0 } );
  995. } else if ( call.method.startsWith( 'finish' ) ) {
  996. block.titleSpan.innerHTML = call.method + ( call.formatedDetails ? call.formatedDetails : '' ) + ( call.count > 1 ? ` <span style="opacity: 0.5">( ${call.count} )</span>` : '' );
  997. currentIndent = Math.max( 0, currentIndent - 1 );
  998. elementStack.pop();
  999. } else {
  1000. block.titleSpan.innerHTML = call.method + ( call.formatedDetails ? call.formatedDetails : '' ) + ( call.count > 1 ? ` <span style="opacity: 0.5">( ${call.count} )</span>` : '' );
  1001. }
  1002. }
  1003. } else {
  1004. const callCounts = {};
  1005. for ( let i = 0; i < frame.calls.length; i ++ ) {
  1006. const method = frame.calls[ i ].method;
  1007. if ( method.startsWith( 'finish' ) ) continue;
  1008. callCounts[ method ] = ( callCounts[ method ] || 0 ) + 1;
  1009. }
  1010. const sortedCalls = Object.keys( callCounts ).map( method => ( { method, count: callCounts[ method ] } ) );
  1011. sortedCalls.sort( ( a, b ) => b.count - a.count );
  1012. for ( let i = 0; i < sortedCalls.length; i ++ ) {
  1013. const call = sortedCalls[ i ];
  1014. const block = this.getCallBlock( call, blockIndex ++ );
  1015. block.style.marginLeft = '0px';
  1016. block.style.borderLeft = '4px solid ' + this.getColorForMethod( call.method );
  1017. block.titleSpan.innerHTML = call.method + ( call.count > 1 ? ` <span style="opacity: 0.5">( ${call.count} )</span>` : '' );
  1018. if ( trackChildren[ childIndex ] !== block ) {
  1019. this.timelineTrack.insertBefore( block, trackChildren[ childIndex ] );
  1020. }
  1021. childIndex ++;
  1022. }
  1023. }
  1024. while ( this.timelineTrack.children.length > childIndex ) {
  1025. this.timelineTrack.removeChild( this.timelineTrack.lastChild );
  1026. }
  1027. }
  1028. getColorForMethod( method ) {
  1029. if ( method.startsWith( 'begin' ) ) return 'var(--color-green)';
  1030. if ( method.startsWith( 'finish' ) || method.startsWith( 'destroy' ) ) return 'var(--color-red)';
  1031. if ( method.startsWith( 'draw' ) || method.startsWith( 'compute' ) || method.startsWith( 'create' ) || method.startsWith( 'generate' ) ) return 'var(--color-yellow)';
  1032. return 'var(--text-secondary)';
  1033. }
  1034. }
  1035. export { Timeline };
粤ICP备19079148号