├── README.md ├── assets ├── download.svg ├── icon.svg ├── icon128.png ├── icon16.png ├── icon48.png ├── record.svg ├── reload.svg └── stop.svg ├── background.js ├── canvas-instrument 2.js ├── canvas-instrument.js ├── common.js ├── content-script.js ├── css ├── Roboto_Mono │ ├── LICENSE.txt │ ├── RobotoMono-Bold.ttf │ ├── RobotoMono-BoldItalic.ttf │ ├── RobotoMono-Italic.ttf │ ├── RobotoMono-Light.ttf │ ├── RobotoMono-LightItalic.ttf │ ├── RobotoMono-Medium.ttf │ ├── RobotoMono-MediumItalic.ttf │ ├── RobotoMono-Regular.ttf │ ├── RobotoMono-Thin.ttf │ └── RobotoMono-ThinItalic.ttf └── styles.css ├── devtools.html ├── devtools.js ├── graph ├── dennis-video.json ├── graph.js ├── index.html ├── obsidian.json ├── rome.json ├── sample.json └── sample2.json ├── libs ├── d3.v4.min.js ├── graph.js ├── metricsgraphics.css ├── metricsgraphics.min.js ├── mg_line_brushing.css └── mg_line_brushing.js ├── manifest.json ├── panel.css ├── panel.html ├── panel.js ├── script-common.js ├── src ├── CanvasRenderingContext2DWrapper.js ├── ContextWrapper.js ├── WebGLRenderingContextWrapper.js ├── Wrapper.js ├── canvas-instrument.js ├── extensions │ ├── ANGLEInstancedArraysExtensionWrapper.js │ ├── EXTDisjointTimerQueryExtensionWrapper.js │ └── WebGLDebugShadersExtensionWrapper.js ├── lib.js ├── main.js ├── package.json ├── utils.js └── widget.js └── utils.js /README.md: -------------------------------------------------------------------------------- 1 | # PerfMeter 2 | 3 | ### Work In Progress | User discretion avised! 4 | 5 | How to install: 6 | 7 | 1. Download or clone the repo: `git clone https://github.com/spite/PerfMeter.git` 8 | 2. Go to `/src`, run `npm install`, then `npm run build` 9 | 3. Open `chrome://extensions` and make sure `Developer Mode` checkbox is ticked 10 | 4. Click `Load unpacked extension` and select the folder you've downloaded the repo to 11 | 5. Open a page with WebGL 12 | 6. Open DevTools 13 | 7. Go to PerfMeter tab 14 | 8. Hit `Reload` to instrument the tab 15 | 16 | ### About the library 17 | 18 | Even though this is intended as a Chrome Extension -and in the future, via de Web Extensions, a Firefox one-, the instrumentation library itself (`src/lib.js`) is designed so it can be used in other browsers. 19 | -------------------------------------------------------------------------------- /assets/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spite/PerfMeter/5f3a8d1d45df515d4d9d8d487524e26ae44c2a11/assets/icon128.png -------------------------------------------------------------------------------- /assets/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spite/PerfMeter/5f3a8d1d45df515d4d9d8d487524e26ae44c2a11/assets/icon16.png -------------------------------------------------------------------------------- /assets/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spite/PerfMeter/5f3a8d1d45df515d4d9d8d487524e26ae44c2a11/assets/icon48.png -------------------------------------------------------------------------------- /assets/record.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/reload.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/stop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var extensionId = chrome.runtime.id; 4 | log( 'Background', extensionId ); 5 | 6 | var settings = {}; 7 | var script = ''; 8 | var instrumentScript = ''; 9 | var commonScript = ''; 10 | var stylesheet = ''; 11 | 12 | var defaultSettings = { 13 | 14 | showGPUInfo: false, 15 | log: false, 16 | autoinstrument: true, 17 | profileShaders: false 18 | 19 | }; 20 | 21 | chrome.system.cpu.getInfo( res => log( res ) ); 22 | chrome.system.display.getInfo( res => log( res ) ); 23 | chrome.system.memory.getInfo( res => log( res ) ); 24 | 25 | function buildScript( s ) { 26 | 27 | settings.cssPath = chrome.extension.getURL( 'css/styles.css' ); 28 | settings.fontPath = chrome.extension.getURL( 'css/Roboto_Mono/RobotoMono-Regular.ttf' ); 29 | settings.stylesheet = stylesheet; 30 | 31 | script = ` 32 | "use strict"; 33 | 34 | var verbose = true; 35 | var settings = ${JSON.stringify(settings)}; 36 | 37 | if( !window[ '${extensionId}_instrumented' ] ) { 38 | 39 | window[ '${extensionId}_instrumented' ] = true; 40 | 41 | ${commonScript} 42 | 43 | ${instrumentScript} 44 | 45 | log( 'Canvas Instrumentation', document.location.href, settings ); 46 | post( { method: 'ready' } ); 47 | 48 | } else { 49 | log( 'Already instrumented. Skipping', document.location.href ); 50 | }`; 51 | 52 | } 53 | 54 | Promise.all( [ 55 | loadSettings().then( res => { settings = res; } ), 56 | fetch( chrome.extension.getURL( './css/styles.css' ) ).then( res => res.text() ).then( res => stylesheet = res ), 57 | fetch( chrome.extension.getURL( './src/lib.js' ) ).then( res => res.text() ).then( res => instrumentScript = res ), 58 | fetch( chrome.extension.getURL( 'script-common.js' ) ).then( res => res.text() ).then( res => commonScript = res ), 59 | ] ).then( () => { 60 | buildScript(); 61 | log( 'Script and settings loaded', settings ); 62 | } ); 63 | 64 | function notifySettings() { 65 | 66 | log( 'settings', settings ); 67 | 68 | Object.keys( connections ).forEach( tab => { 69 | var port = connections[ tab ].devtools; 70 | port.postMessage( { 71 | action: 'settings', 72 | settings: settings 73 | } ); 74 | inject( port ); 75 | } ); 76 | 77 | } 78 | 79 | function inject( port ) { 80 | 81 | port.postMessage( { 82 | action: 'script', 83 | source: script 84 | } ); 85 | 86 | } 87 | 88 | var connections = {}; 89 | var reloadTriggered = false; 90 | 91 | // Post back to Devtools from content 92 | chrome.runtime.onMessage.addListener( ( message, sender, sendResponse ) => { 93 | 94 | //log( 'onMessage', message, sender ); 95 | if ( sender.tab && connections[ sender.tab.id ] ) { 96 | var port = connections[ sender.tab.id ].devtools; 97 | port.postMessage( { action: 'fromScript', data: message } ); 98 | } 99 | 100 | return true; 101 | 102 | } ); 103 | 104 | chrome.runtime.onConnect.addListener( port => { 105 | 106 | log( 'New connection (chrome.runtime.onConnect) from', port.name, port.sender.frameId, port ); 107 | 108 | var name = port.name; 109 | 110 | function listener( msg, sender, reply ) { 111 | 112 | var tabId; 113 | 114 | if( msg.tabId ) tabId = msg.tabId; 115 | else tabId = sender.sender.tab.id; 116 | 117 | if( !connections[ tabId ] ) connections[ tabId ] = {}; 118 | connections[ tabId ][ name ] = port; 119 | 120 | //log( sender ); 121 | //log( 'port.onMessage', port.name, msg ); 122 | 123 | if( name === 'contentScript' ) { 124 | 125 | var fwd = connections[ tabId ].devtools; 126 | if( fwd ) { 127 | fwd.postMessage( { action: 'fromScript', data: msg } ); 128 | } else { 129 | //console.warn( 'No DevTools port for tab ', tabId ); 130 | } 131 | 132 | } 133 | 134 | switch( msg.action ) { 135 | 136 | case 'reload': 137 | reloadTriggered = true; 138 | break; 139 | 140 | case 'start': 141 | port.postMessage( { 142 | action: 'settings', 143 | settings: settings 144 | } ); 145 | break; 146 | 147 | case 'getScript': 148 | inject( port ); 149 | break; 150 | 151 | case 'setSettings': 152 | settings = msg.settings; 153 | //log( settings ); 154 | saveSettings( settings ).then( res => { 155 | buildScript(); 156 | notifySettings(); 157 | } ); 158 | 159 | break; 160 | } 161 | 162 | } 163 | 164 | port.onMessage.addListener( listener ); 165 | 166 | port.onDisconnect.addListener( _ => { 167 | 168 | port.onMessage.removeListener( listener ); 169 | 170 | log( name, 'disconnect (chrome.runtime.onDisconnect)' ); 171 | 172 | Object.keys( connections ).forEach( c => { 173 | if( connections[ c ][ name ] === port ) { 174 | connections[ c ][ name ] = null; 175 | delete connections[ c ][ name ]; 176 | } 177 | if ( Object.keys( connections[ c ] ).length === 0 ) { 178 | connections[ c ] = null; 179 | delete connections[ c ]; 180 | } 181 | } ); 182 | 183 | } ); 184 | 185 | port.postMessage( { action: 'ack' } ); 186 | 187 | return true; 188 | 189 | }); 190 | 191 | chrome.webRequest.onBeforeRequest.addListener( details => { 192 | 193 | if( reloadTriggered ) { 194 | return; 195 | } 196 | 197 | if( settings.autoinstrument ) { 198 | if( connections[ details.tabId ] && connections[ details.tabId ].devtools ) { 199 | //log( 'webRequest', 'inject' ) 200 | connections[ details.tabId ].devtools.postMessage( { action: 'inject' } ); 201 | } 202 | } 203 | 204 | }, {urls: [""]} ); 205 | 206 | chrome.tabs.onUpdated.addListener( ( tabId, info, tab ) => { 207 | 208 | //log( 'onUpdate', tabId, info, tab ); 209 | 210 | if( info.status === 'complete' ) { 211 | reloadTriggered = false; 212 | log( 'finished reload' ); 213 | } 214 | 215 | }); 216 | -------------------------------------------------------------------------------- /canvas-instrument 2.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var verbose = false; 4 | 5 | if( !window.__PerfMeterInstrumented ) { 6 | 7 | window.__PerfMeterInstrumented = true; 8 | 9 | window.__PerfMeterSettings = function( s ) { 10 | settings = s; 11 | }; 12 | 13 | var recording = false; 14 | 15 | window.__PerfMeterStartRecording = function() { 16 | 17 | recording = true; 18 | 19 | }; 20 | 21 | window.__PerfMeterStopRecording = function() { 22 | 23 | recording = false; 24 | 25 | }; 26 | 27 | log( 'Canvas Instrumentation', document.location.href, settings ); 28 | 29 | var instrumented = false; 30 | 31 | var glInfo = { 32 | versions: [], 33 | WebGLAvailable: 'WebGLRenderingContext' in window, 34 | WebGL2Available: 'WebGL2RenderingContext' in window 35 | }; 36 | 37 | var getGLInfo = function( context ) { 38 | var gl = document.createElement( 'canvas' ).getContext( context ); 39 | if( !gl ) return; 40 | var debugInfo = gl.getExtension( 'WEBGL_debug_renderer_info' ); 41 | var version = { 42 | type: context, 43 | vendor: gl.getParameter( debugInfo.UNMASKED_VENDOR_WEBGL ), 44 | renderer: gl.getParameter( debugInfo.UNMASKED_RENDERER_WEBGL ), 45 | glVersion: gl.getParameter( gl.VERSION ), 46 | glslVersion: gl.getParameter( gl.SHADING_LANGUAGE_VERSION ) 47 | }; 48 | glInfo.versions.push( version ); 49 | }; 50 | 51 | getGLInfo( 'webgl' ); 52 | getGLInfo( 'webgl2' ); 53 | 54 | var webGLInfo = ''; 55 | glInfo.versions.forEach( v => { 56 | var glInfo = `GL Version: ${v.glVersion} 57 | GLSL Version: ${v.glslVersion} 58 | Vendor: ${v.vendor} 59 | Renderer: ${v.renderer} 60 | `; 61 | webGLInfo += glInfo; 62 | } ); 63 | 64 | 65 | 66 | post( { method: 'ready' } ); 67 | 68 | var getTime = function(){ 69 | 70 | return performance.now(); 71 | 72 | }; 73 | 74 | var text = document.createElement( 'div' ); 75 | text.setAttribute( 'id', 'perfmeter-panel' ); 76 | 77 | var _wrap = function( f, pre, post ) { 78 | 79 | return function _wrap() { 80 | 81 | var args = [ ...arguments ]; 82 | args = pre.call( this, args ) || args; 83 | var res = f.apply( this, args ); 84 | var r; 85 | return post ? ( r = post.apply( this, [ res, args ] ), r ? r : res ) : res; 86 | 87 | }; 88 | 89 | }; 90 | 91 | /*function guid() { 92 | function s4() { 93 | return Math.floor((1 + Math.random()) * 0x10000) 94 | .toString(16) 95 | .substring(1); 96 | } 97 | return s4() + s4() + '-' + s4() + '-' + s4() + '-' + 98 | s4() + '-' + s4() + s4() + s4(); 99 | }*/ 100 | var guid = ( function() { 101 | var counter = 0; 102 | return function() { 103 | counter++; 104 | return counter; 105 | } 106 | } )(); 107 | 108 | var contexts = new Map(); 109 | var allCanvasAre2D = true; 110 | 111 | HTMLCanvasElement.prototype.getContext = _wrap( 112 | HTMLCanvasElement.prototype.getContext, 113 | function() { 114 | this.style.border = '1px solid #9EFD38'; 115 | this.style.boxSizing = 'border-box'; 116 | 117 | log( 'getContext', arguments ); 118 | }, 119 | function( res, args ) { 120 | 121 | if( !res ) return; 122 | 123 | var ctx = { 124 | ctx: res, 125 | type: '2d', 126 | queryExt: null, 127 | queries: new Map(), 128 | frames: {}, 129 | programCount: 0, 130 | textureCount: 0, 131 | frameBufferCount: 0, 132 | disjointTime: 0, 133 | drawCount: 0, 134 | instancedDrawCount: 0, 135 | instanceCount: 0, 136 | pointCount: 0, 137 | lineCount: 0, 138 | triangleCount: 0, 139 | useProgramCount: 0, 140 | bindTextureCount: 0, 141 | JavaScriptTime: 0, 142 | points: 0, 143 | lines: 0, 144 | triangles: 0, 145 | log: [], 146 | programs: new Map(), 147 | timerQueries: [], 148 | currentQuery: null, 149 | nestedQueries: new Map(), 150 | allQueries: {} 151 | }; 152 | 153 | contexts.set( res, ctx ); 154 | 155 | if( [ 'webgl', 'experimental-webgl', 'webgl2', 'experimental-webgl2' ].some( id => id == args[ 0 ] ) ) { 156 | 157 | allCanvasAre2D = false; 158 | ctx.type = '3d'; 159 | 160 | var queryExt = res.getExtension( 'EXT_disjoint_timer_query' ); 161 | if( queryExt ) { 162 | ctx.queryExt = queryExt; 163 | } 164 | } 165 | 166 | instrumentCanvas(); 167 | 168 | } 169 | ); 170 | 171 | var updateDrawCount = function( gl, ctx, mode, count ) { 172 | 173 | switch( mode ){ 174 | case gl.POINTS: 175 | ctx.points += count; 176 | break; 177 | case gl.LINE_STRIP: 178 | ctx.lines += count - 1; 179 | break; 180 | case gl.LINE_LOOP: 181 | ctx.lines += count; 182 | break; 183 | case gl.LINES: 184 | ctx.lines += count / 2; 185 | break; 186 | case gl.TRIANGLE_STRIP: 187 | case gl.TRIANGLE_FAN: 188 | ctx.triangles += count - 2; 189 | break; 190 | case gl.TRIANGLES: 191 | ctx.triangles += count / 3; 192 | break; 193 | } 194 | 195 | }; 196 | 197 | var instrumentContext = function( proto ) { 198 | 199 | proto.prototype.drawElements = _wrap( 200 | proto.prototype.drawElements, 201 | function _preDrawElements() { 202 | var ctx = contexts.get( this ); 203 | ctx.drawCount ++; 204 | updateDrawCount( this, ctx, arguments[ 0 ], arguments[ 1 ] ); 205 | //log( 'DrawElements start query' ); 206 | //var query = ctx.queryExt.createQueryEXT(); 207 | //ctx.queryExt.beginQueryEXT( ctx.queryExt.TIME_ELAPSED_EXT, query ); 208 | }, 209 | function _postDrawElements() { 210 | //log( 'DrawElements end query' ); 211 | //var ctx = contexts.get( this ); 212 | //ctx.queryExt.endQueryEXT( ctx.queryExt.TIME_ELAPSED_EXT ); 213 | } 214 | ); 215 | 216 | proto.prototype.drawArrays = _wrap( 217 | proto.prototype.drawArrays, 218 | function _preDrawArrays() { 219 | var ctx = contexts.get( this ); 220 | ctx.drawCount ++; 221 | updateDrawCount( this, ctx, arguments[ 0 ], arguments[ 1 ] ); 222 | //log( 'DrawArrays start query' ); 223 | //var query = ctx.queryExt.createQueryEXT(); 224 | //ctx.queryExt.beginQueryEXT( ctx.queryExt.TIME_ELAPSED_EXT, query ); 225 | }, 226 | function _postDrawArrays() { 227 | //log( 'DrawArrays end query' ); 228 | //var ctx = contexts.get( this ); 229 | //ctx.queryExt.endQueryEXT( ctx.queryExt.TIME_ELAPSED_EXT ); 230 | } 231 | ); 232 | 233 | var useProgram = proto.prototype.useProgram; 234 | proto.prototype.useProgram = function() { 235 | 236 | var ctx = contexts.get( this ); 237 | ctx.useProgramCount++; 238 | ctx.programs.get( arguments[ 0 ] ).calls++; 239 | return useProgram.apply( this, arguments ); 240 | 241 | }; 242 | 243 | var bindTexture = proto.prototype.bindTexture; 244 | proto.prototype.bindTexture = function() { 245 | 246 | if( arguments[ 0 ] !== null ) contexts.get( this ).bindTextureCount++; 247 | 248 | return bindTexture.apply( this, arguments ); 249 | 250 | }; 251 | 252 | var createProgram = proto.prototype.createProgram; 253 | proto.prototype.createProgram = function() { 254 | 255 | var ctx = contexts.get( this ); 256 | ctx.programCount++; 257 | var res = createProgram.apply( this, arguments ); 258 | ctx.programs.set( res, { queries: [], calls: 0 } ); 259 | return res; 260 | 261 | }; 262 | 263 | var deleteProgram = proto.prototype.deleteProgram; 264 | proto.prototype.deleteProgram = function() { 265 | 266 | contexts.get( this ).programCount--; 267 | return deleteProgram.apply( this, arguments ); 268 | 269 | }; 270 | 271 | var createTexture = proto.prototype.createTexture; 272 | proto.prototype.createTexture = function() { 273 | 274 | contexts.get( this ).textureCount++; 275 | return createTexture.apply( this, arguments ); 276 | 277 | }; 278 | 279 | var deleteTexture = proto.prototype.deleteTexture; 280 | proto.prototype.deleteTexture = function() { 281 | 282 | contexts.get( this ).textureCount--; 283 | return deleteTexture.apply( this, arguments ); 284 | 285 | }; 286 | 287 | Object.keys( proto.prototype ).filter( v => { 288 | try{ 289 | if( typeof proto.prototype[ v ] === 'function' ) return true; 290 | } catch( e ) { 291 | } 292 | return false; 293 | } ).forEach( fn => { 294 | var startTime; 295 | proto.prototype[ fn ] = _wrap( 296 | proto.prototype[ fn ], 297 | function _pre() { 298 | startTime = getTime(); 299 | //log( fn ); 300 | }, 301 | function _post() { 302 | var ctx = contexts.get( this ); 303 | var endTime = getTime(); 304 | if( settings.log ) ctx.log.push( [ startTime, endTime, fn, endTime - startTime ] ); 305 | ctx.JavaScriptTime += endTime - startTime; 306 | //log( fn, ctx.JavaScriptTime ); 307 | } 308 | ); 309 | } ); 310 | 311 | }; 312 | 313 | var instrumentCanvas = function() { 314 | 315 | if( instrumented ) return; 316 | 317 | instrumented = true; 318 | 319 | var fileref = document.createElement("link"); 320 | fileref.rel = "stylesheet"; 321 | fileref.type = "text/css"; 322 | fileref.href = settings.cssPath; 323 | 324 | window.document.getElementsByTagName("head")[0].appendChild(fileref); 325 | 326 | if( !window.document.body ) { 327 | window.addEventListener( 'load', function() { 328 | window.document.body.appendChild( text ); 329 | } ); 330 | } else { 331 | window.document.body.appendChild( text ); 332 | } 333 | 334 | instrumentContext( CanvasRenderingContext2D ); 335 | instrumentContext( WebGLRenderingContext ); 336 | if( glInfo.WebGL2Available ) instrumentContext( WebGL2RenderingContext ); 337 | 338 | }; 339 | 340 | // WebGL with ANGLE_instanced_arrays Extension 341 | // There isn't an available ANGLEInstancedArrays constructor 342 | // This way feels hacky 343 | 344 | var getExtension = WebGLRenderingContext.prototype.getExtension; 345 | WebGLRenderingContext.prototype.getExtension = function() { 346 | 347 | log( 'Extension', arguments[ 0 ] ); 348 | var res = getExtension.apply( this, arguments ); 349 | var gl = this; 350 | var ctx = contexts.get( gl ); 351 | 352 | if( arguments[ 0 ] === 'ANGLE_instanced_arrays' ){ 353 | 354 | var drawArraysInstancedANGLE = res.drawArraysInstancedANGLE; 355 | res.drawArraysInstancedANGLE = function() { 356 | 357 | ctx.instancedDrawCount++; 358 | ctx.instanceCount += arguments[ 3 ]; 359 | updateDrawCount( gl, ctx, arguments[ 0 ], arguments[ 2 ] ); 360 | return drawArraysInstancedANGLE.apply( this, arguments ); 361 | 362 | }; 363 | 364 | var drawElementsInstancedANGLE = res.drawElementsInstancedANGLE; 365 | res.drawElementsInstancedANGLE = function() { 366 | 367 | ctx.instancedDrawCount++; 368 | ctx.instanceCount += arguments[ 4 ]; 369 | updateDrawCount( gl, ctx, arguments[ 0 ], arguments[ 1 ] ); 370 | return drawElementsInstancedANGLE.apply( this, arguments ); 371 | 372 | }; 373 | 374 | } 375 | 376 | if( arguments[ 0 ] === 'EXT_disjoint_timer_query' ) { 377 | 378 | var createQueryEXT = res.createQueryEXT; 379 | var beginQueryEXT = res.beginQueryEXT; 380 | var endQueryEXT = res.endQueryEXT; 381 | var getQueryObjectEXT = res.getQueryObjectEXT; 382 | 383 | var n = { 384 | GPU_DISJOINT_EXT: res.GPU_DISJOINT_EXT, 385 | CURRENT_QUERY_EXT: res.CURRENT_QUERY_EXT, 386 | QUERY_COUNTER_BITS_EXT: res.QUERY_COUNTER_BITS_EXT, 387 | QUERY_RESULT_AVAILABLE_EXT: res.QUERY_RESULT_AVAILABLE_EXT, 388 | QUERY_RESULT_EXT: res.QUERY_RESULT_EXT, 389 | TIMESTAMP_EXT: res.TIMESTAMP_EXT, 390 | TIME_ELAPSED_EXT: res.TIME_ELAPSED_EXT 391 | }; 392 | 393 | n.createQueryEXT = function() { 394 | 395 | var createRes = createQueryEXT.apply( res, arguments ); 396 | createRes.guid = guid(); 397 | createRes.originalQuery = null; 398 | ctx.nestedQueries.set( createRes, [] ); 399 | ctx.allQueries[ createRes.guid ] = createRes; 400 | log( 'New', createRes.guid ); 401 | return createRes; 402 | } 403 | 404 | // ext.beginQueryEXT( ext.TIME_ELAPSED_EXT, query ); 405 | n.beginQueryEXT = function() { 406 | 407 | if( arguments[ 0 ] === res.TIME_ELAPSED_EXT ){ 408 | if( ctx.currentQuery ) { 409 | ctx.nestedQueries.get( ctx.currentQuery ).push( arguments[ 1 ] ); 410 | log( 'Ending', ctx.currentQuery.guid, 'because', arguments[ 1 ].guid, 'begins' ); 411 | endQueryEXT.apply( res, [ res.TIME_ELAPSED_EXT ] ); 412 | } 413 | arguments[ 1 ].originalQuery = ctx.currentQuery; 414 | ctx.currentQuery = arguments[ 1 ]; 415 | } 416 | log( 'Begin', arguments[ 1 ].guid ); 417 | return beginQueryEXT.apply( res, arguments ); 418 | } 419 | 420 | // ext.endQueryEXT( ext.TIME_ELAPSED_EXT ); 421 | n.endQueryEXT = function() { 422 | 423 | log( 'End', ctx.currentQuery.guid, ctx.currentQuery.originalQuery ); 424 | var endRes = endQueryEXT.apply( res, arguments ); 425 | if( arguments[ 0 ] === res.TIME_ELAPSED_EXT ){ 426 | if( ctx.currentQuery.originalQuery ) { 427 | var newQuery = createQueryEXT.apply( res ); 428 | newQuery.guid = guid(); 429 | newQuery.originalQuery = ctx.currentQuery.originalQuery; 430 | beginQueryEXT.apply( res, [ res.TIME_ELAPSED_EXT, newQuery ] ); 431 | ctx.currentQuery = newQuery; 432 | log( 'Starting', newQuery.guid, 'on behalf of', newQuery.originalQuery.guid ); 433 | ctx.nestedQueries.set( newQuery, [] ); 434 | ctx.nestedQueries.get( newQuery.originalQuery ).push( newQuery ); 435 | newQuery.originalQuery = newQuery.originalQuery.originalQuery 436 | } else { 437 | ctx.currentQuery = null; 438 | } 439 | } 440 | return endRes; 441 | } 442 | 443 | function extractNestedQueries( collection ) { 444 | if( !collection || collection.length === 0 ) return []; 445 | return collection.filter( c => { 446 | return extractNestedQueries( ctx.nestedQueries.get( c ) ); 447 | } ); 448 | } 449 | // queryExt.getQueryObjectEXT( query, queryExt.QUERY_RESULT_AVAILABLE_EXT ); 450 | // queryExt.getQueryObjectEXT( query, queryExt.QUERY_RESULT_EXT ); 451 | n.getQueryObjectEXT = function() { 452 | 453 | var nestedQueries = [ arguments[ 0 ], extractNestedQueries( ctx.nestedQueries.get( arguments[ 0 ] ) ) ]; 454 | nestedQueries = [].concat.apply([], nestedQueries ); 455 | 456 | //debugger; 457 | 458 | if( arguments[ 1 ] === res.QUERY_RESULT_AVAILABLE_EXT ) { 459 | var result = true; 460 | nestedQueries.forEach( q => { 461 | var available = getQueryObjectEXT.apply( res, [ q, res.QUERY_RESULT_AVAILABLE_EXT ] ); 462 | //log( 'Available for', q.guid, ':', available ); 463 | result = result && available; 464 | } ); 465 | return result; 466 | } 467 | 468 | if( arguments[ 1 ] === res.QUERY_RESULT_EXT ) { 469 | var result = 0; 470 | nestedQueries.forEach( q => { 471 | var timeResult = getQueryObjectEXT.apply( res, [ q, res.QUERY_RESULT_EXT ]); 472 | result += timeResult; 473 | //log( 'Result for', q.guid, ':', timeResult ); 474 | } ); 475 | return result; 476 | } 477 | 478 | } 479 | 480 | return n; 481 | 482 | } 483 | 484 | return res; 485 | 486 | }; 487 | 488 | // WebGL2 489 | 490 | if( glInfo.WebGL2Available ) { 491 | 492 | var drawElementsInstanced = WebGL2RenderingContext.prototype.drawElementsInstanced; 493 | WebGL2RenderingContext.prototype.drawElementsInstanced = function() { 494 | 495 | var ctx = contexts.get( this ); 496 | ctx.instancedDrawCount ++; 497 | ctx.instanceCount += arguments[ 3 ]; 498 | updateDrawCount( this, ctx, arguments[ 0 ], arguments[ 1 ] ); 499 | return drawElementsInstanced.apply( this, arguments ); 500 | 501 | }; 502 | 503 | var drawArraysInstanced = WebGL2RenderingContext.prototype.drawArraysInstanced; 504 | WebGL2RenderingContext.prototype.drawArraysInstanced = function() { 505 | 506 | var ctx = contexts.get( this ); 507 | ctx.instancedDrawCount ++; 508 | ctx.instanceCount += arguments[ 4 ]; 509 | updateDrawCount( this, ctx, arguments[ 0 ], arguments[ 2 ] ); 510 | return drawArraysInstanced.apply( this, arguments ); 511 | 512 | }; 513 | 514 | } 515 | 516 | /*var methods = [ 517 | 'uniform1f', 'uniform1fv', 'uniform1i', 'uniform1iv', 518 | 'uniform2f', 'uniform2fv', 'uniform2i', 'uniform2iv', 519 | 'uniform3f', 'uniform3fv', 'uniform3i', 'uniform3iv', 520 | 'uniform4f', 'uniform4fv', 'uniform4i', 'uniform4iv', 521 | 'uniformMatrix2fv', 'uniformMatrix3fv', 'uniformMatrix4fv' 522 | ]; 523 | 524 | methods.forEach( function( f ) { 525 | 526 | var prev = WebGLRenderingContext.prototype[ f ]; 527 | WebGLRenderingContext.prototype[ f ] = function() { 528 | 529 | //post( { method: f } ); 530 | return prev.apply( this, arguments ); 531 | 532 | } 533 | 534 | } );*/ 535 | 536 | var originalRAF = requestAnimationFrame; 537 | var rAFs = []; 538 | var oTime = getTime(); 539 | var frameCount = 0; 540 | var lastTime = getTime(); 541 | var frameId = 0; 542 | var framesQueue = {}; 543 | var disjointFrames = new Map(); 544 | 545 | var framerate = 0; 546 | var JavaScriptTime = 0; 547 | var frameTime = 0; 548 | var rAFCount = 0; 549 | 550 | window.requestAnimationFrame = function( c ) { 551 | 552 | if( typeof c === 'function' ) rAFs.push( c ); // some pages pass null (?) 553 | 554 | }; 555 | 556 | var process = function( timestamp ) { 557 | 558 | originalRAF( process ); 559 | 560 | oTime = getTime(); 561 | rAFCount = rAFs.length; 562 | 563 | disjointFrames.forEach( frame => { 564 | 565 | frame.queries.forEach( q => { 566 | 567 | var query = q.query; 568 | var queryExt = q.context.queryExt; 569 | var available = queryExt.getQueryObjectEXT( query, queryExt.QUERY_RESULT_AVAILABLE_EXT ); 570 | var disjoint = q.context.ctx.getParameter( queryExt.GPU_DISJOINT_EXT ); 571 | 572 | if( available === null && disjoint === null ) { // Android? 573 | q.resolved = true; 574 | } 575 | 576 | if( available && !disjoint ) { 577 | q.time = queryExt.getQueryObjectEXT( query, queryExt.QUERY_RESULT_EXT ); 578 | q.resolved = true; 579 | } 580 | 581 | } ); 582 | 583 | } ); 584 | 585 | disjointFrames.forEach( frame => { 586 | 587 | var time = 0; 588 | var resolved = true; 589 | 590 | frame.queries.forEach( q => { 591 | if( q.resolved ) { 592 | time += q.time; 593 | q.context.disjointTime += q.time; 594 | } 595 | else resolved = false; 596 | } ); 597 | 598 | if( resolved ) { 599 | if( recording && framesQueue[ frame.frameId ] ) { 600 | framesQueue[ frame.frameId ].disjointTime = time; 601 | framesQueue[ frame.frameId ].completed = true; 602 | } 603 | disjointFrames.delete( frame.frameId ); 604 | } 605 | 606 | } ); 607 | 608 | disjointFrames.set( frameId, { frameId: frameId, queries: [] } ); 609 | 610 | contexts.forEach( function _contexts( context ) { 611 | 612 | var queryExt = context.queryExt; 613 | 614 | if( queryExt ) { 615 | 616 | log( 'Perf Start Query' ); 617 | var query = queryExt.createQueryEXT(); 618 | queryExt.beginQueryEXT( queryExt.TIME_ELAPSED_EXT, query ); 619 | var f = disjointFrames.get( frameId ); 620 | if( f ) { 621 | f.queries.push( { context: context, query: query, resolved: false, time: 0 } ); 622 | } 623 | } 624 | 625 | } ); 626 | 627 | JavaScriptTime = 0; 628 | var s = getTime(); 629 | var rAFQueue = rAFs.slice(); 630 | rAFs = []; 631 | rAFQueue.forEach( function _raf( c ) { 632 | c( timestamp ); 633 | } ); 634 | JavaScriptTime = getTime() - s; 635 | 636 | contexts.forEach( function( context ) { 637 | var queryExt = context.queryExt; 638 | if( queryExt ) { 639 | log( 'Perf End Query' ); 640 | queryExt.endQueryEXT( queryExt.TIME_ELAPSED_EXT ); 641 | } 642 | } ); 643 | 644 | frameCount++; 645 | if( getTime() > lastTime + 1000 ) { 646 | framerate = frameCount * 1000 / ( getTime() - lastTime ); 647 | frameCount = 0; 648 | lastTime = getTime(); 649 | } 650 | 651 | frameId++; 652 | 653 | /*if( useProgramCount === 0 ) { 654 | contexts.forEach( gl => { 655 | var res = gl.getParameter( gl.CURRENT_PROGRAM ); 656 | debugger; 657 | } ) 658 | }*/ 659 | 660 | frameTime = getTime() - oTime; 661 | 662 | update(); 663 | 664 | if( recording ) { 665 | 666 | var frameInfo = { 667 | frameId: frameId, 668 | framerate: framerate, 669 | timestamp: oTime, 670 | frameTime: frameTime, 671 | completed: allCanvasAre2D, 672 | contexts: new Map() 673 | }; 674 | 675 | contexts.forEach( function( context ) { 676 | frameInfo.contexts.set( context, { 677 | useProgramCount: context.useProgramCount, 678 | drawCount: context.drawCount, 679 | instancedDrawCount: context.instancedDrawCount, 680 | bindTextureCount: context.bindTextureCount, 681 | JavaScriptTime: context.JavaScriptTime, 682 | disjointTime: context.disjointTime, 683 | points: context.points, 684 | lines: context.lines, 685 | triangles: context.triangles, 686 | log: context.log 687 | } ); 688 | } ); 689 | 690 | framesQueue[ frameId ] = frameInfo; 691 | 692 | } 693 | 694 | contexts.forEach( function( context ) { 695 | context.useProgramCount = 0; 696 | context.drawCount = 0; 697 | context.instancedDrawCount = 0; 698 | context.instanceCount = 0; 699 | context.bindTextureCount = 0; 700 | context.JavaScriptTime = 0; 701 | context.disjointTime = 0; 702 | context.points = 0; 703 | context.lines = 0; 704 | context.triangles = 0; 705 | context.log = []; 706 | } ); 707 | 708 | }; 709 | 710 | var compileFrame = function( contexts ) { 711 | 712 | var drawCount = 0; 713 | var instancedDrawCount = 0; 714 | var instanceCount = 0; 715 | var JavaScriptTime = 0; 716 | var disjointTime = 0; 717 | var useProgramCount = 0; 718 | var bindTextureCount = 0; 719 | var hasWebGL = false; 720 | var totalPoints = 0; 721 | var totalLines = 0; 722 | var totalTriangles = 0; 723 | var programCount = 0; 724 | var textureCount = 0; 725 | 726 | var canvasLog = []; 727 | 728 | contexts.forEach( function( context ) { 729 | drawCount += context.drawCount; 730 | instancedDrawCount += context.instancedDrawCount; 731 | JavaScriptTime += context.JavaScriptTime; 732 | disjointTime += context.disjointTime; 733 | useProgramCount += context.useProgramCount; 734 | bindTextureCount += context.bindTextureCount; 735 | totalPoints += context.points; 736 | totalLines += context.lines; 737 | totalTriangles += context.triangles; 738 | programCount += context.programCount; 739 | textureCount += context.textureCount; 740 | instanceCount += context.instanceCount; 741 | canvasLog.push( { 742 | disjointTime: context.disjointTime, 743 | log: context.log, 744 | JavaScriptTime: context.JavaScriptTime 745 | } ); 746 | if( context.type === '3d' ) hasWebGL = true; 747 | } ); 748 | 749 | return { 750 | drawCount: drawCount, 751 | instancedDrawCount: instancedDrawCount, 752 | instanceCount: instanceCount, 753 | JavaScriptTime: JavaScriptTime, 754 | disjointTime: disjointTime, 755 | useProgramCount: useProgramCount, 756 | bindTextureCount: bindTextureCount, 757 | totalPoints: totalPoints, 758 | totalLines: totalLines, 759 | totalTriangles: totalTriangles, 760 | programCount: programCount, 761 | textureCount: textureCount, 762 | totalDrawCount: drawCount + instancedDrawCount, 763 | hasWebGL: hasWebGL, 764 | log: canvasLog 765 | }; 766 | 767 | }; 768 | 769 | var update = function(){ 770 | 771 | if( contexts.size === 0 ) return; 772 | 773 | if( recording ) { 774 | 775 | Object.keys( framesQueue ).forEach( n => { 776 | 777 | var frame = framesQueue[ n ]; 778 | if( frame.completed ) { 779 | 780 | var res = compileFrame( frame.contexts ); 781 | 782 | post( { 783 | method: 'frame', 784 | data: { 785 | frame: frame.frameId, 786 | timestamp: frame.timestamp, 787 | framerate: frame.framerate, 788 | frameTime: frame.frameTime, 789 | JavaScriptTime: res.JavaScriptTime, 790 | disjointTime: res.disjointTime, 791 | drawCount: res.drawCount, 792 | log: res.log 793 | } 794 | 795 | } ); 796 | 797 | framesQueue[ n ] = null; 798 | delete framesQueue[ n ]; 799 | 800 | } 801 | 802 | } ); 803 | 804 | } 805 | 806 | var frame = compileFrame( contexts ); 807 | 808 | //console.log( frameTime.toFixed(2), JavaScriptTime.toFixed(2) ) 809 | //if( frame.JavaScriptTime > frameTime ) debugger; 810 | 811 | var general = `FPS: ${framerate.toFixed( 2 )} 812 | Frame JS Time: ${frameTime.toFixed(2)} 813 | Canvas: ${contexts.size} 814 | Canvas JS time: ${frame.JavaScriptTime.toFixed( 2 )} 815 | `; 816 | 817 | var webgl = `WebGL 818 | GPU Time: ${( frame.disjointTime / 1000000 ).toFixed( 2 )} 819 | programs: ${frame.programCount} 820 | textures: ${frame.textureCount} 821 | useProgram: ${frame.useProgramCount} 822 | bindTexture: ${frame.bindTextureCount} 823 | Draw: ${frame.drawCount} 824 | Instanced: ${frame.instancedDrawCount} (${frame.instanceCount}) 825 | Total: ${frame.totalDrawCount} 826 | Points: ${frame.totalPoints} 827 | Lines: ${frame.totalLines} 828 | Triangles: ${frame.totalTriangles} 829 | `; 830 | 831 | var browser = `Browser 832 | Mem: ${(performance.memory.usedJSHeapSize/(1024*1024)).toFixed(2)}/${(performance.memory.totalJSHeapSize/(1024*1024)).toFixed(2)} 833 | `; 834 | 835 | text.innerHTML = general + ( frame.hasWebGL ? webgl : '' ) + browser + ( settings.showGPUInfo ? webGLInfo : '' ); 836 | 837 | }; 838 | 839 | originalRAF( process ); 840 | 841 | } else { 842 | if( verbose ) log( 'Already instrumented. Skipping', document.location.href ); 843 | } 844 | -------------------------------------------------------------------------------- /canvas-instrument.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | "use strict"; 4 | 5 | function createUUID(){ 6 | 7 | function s4(){ 8 | return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); 9 | } 10 | 11 | return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`; 12 | 13 | } 14 | 15 | function FrameData( id ){ 16 | 17 | this.frameId = id; 18 | 19 | this.framerate = 0; 20 | this.frameTime = 0; 21 | this.JavaScriptTime = 0; 22 | 23 | this.contexts = new Map(); 24 | 25 | } 26 | 27 | function ContextFrameData( type ){ 28 | 29 | this.type = type; 30 | 31 | this.JavaScriptTime = 0; 32 | this.GPUTime = 0; 33 | this.log = []; 34 | 35 | this.createProgram = 0; 36 | this.createTexture = 0; 37 | 38 | this.useProgram = 0; 39 | this.bindTexture = 0; 40 | 41 | this.triangles = 0; 42 | this.lines = 0; 43 | this.points = 0; 44 | 45 | this.startTime = 0; 46 | 47 | } 48 | 49 | function ContextData( contextWrapper ){ 50 | 51 | this.id = createUUID(); 52 | this.queryExt = null; 53 | this.contextWrapper = contextWrapper; 54 | this.extQueries = []; 55 | 56 | } 57 | 58 | function Wrapper( context ){ 59 | 60 | this.id = createUUID(); 61 | this.context = context; 62 | 63 | this.count = 0; 64 | this.JavaScriptTime = 0; 65 | 66 | this.log = []; 67 | 68 | } 69 | 70 | Wrapper.prototype.run = function( fName, fArgs, fn ){ 71 | 72 | this.incrementCount(); 73 | this.beginProfile( fName, fArgs ); 74 | var res = fn(); 75 | this.endProfile(); 76 | return res; 77 | 78 | } 79 | 80 | Wrapper.prototype.resetFrame = function(){ 81 | 82 | this.resetCount(); 83 | this.resetJavaScriptTime(); 84 | this.resetLog(); 85 | 86 | } 87 | 88 | Wrapper.prototype.resetCount = function(){ 89 | 90 | this.count = 0; 91 | 92 | } 93 | 94 | Wrapper.prototype.incrementCount = function(){ 95 | 96 | this.count++; 97 | 98 | } 99 | 100 | Wrapper.prototype.resetLog = function(){ 101 | 102 | this.log.length = 0; 103 | 104 | } 105 | 106 | Wrapper.prototype.resetJavaScriptTime = function(){ 107 | 108 | this.JavaScriptTime = 0; 109 | 110 | } 111 | 112 | Wrapper.prototype.incrementJavaScriptTime = function( time ){ 113 | 114 | this.JavaScriptTime += time; 115 | 116 | } 117 | 118 | Wrapper.prototype.beginProfile = function( fn, args ){ 119 | 120 | var t = performance.now(); 121 | this.log.push( { function: fn, arguments: args, start: t, end: 0 } ); 122 | this.startTime = t; 123 | 124 | } 125 | 126 | Wrapper.prototype.endProfile = function(){ 127 | 128 | var t = performance.now(); 129 | this.log[ this.log.length - 1 ].end = t; 130 | this.incrementJavaScriptTime( t - this.startTime ); 131 | 132 | } 133 | 134 | function CanvasRenderingContext2DWrapper( context ){ 135 | 136 | Wrapper.call( this, context ); 137 | 138 | } 139 | 140 | CanvasRenderingContext2DWrapper.prototype = Object.create( Wrapper.prototype ); 141 | 142 | CanvasRenderingContext2DWrapper.prototype.resetFrame = function(){ 143 | 144 | Wrapper.prototype.resetFrame.call( this ); 145 | 146 | } 147 | 148 | Object.keys( CanvasRenderingContext2D.prototype ).forEach( key => { 149 | 150 | if( key !== 'canvas' ){ 151 | 152 | try{ 153 | if( typeof CanvasRenderingContext2D.prototype[ key ] === 'function' ){ 154 | CanvasRenderingContext2DWrapper.prototype[ key ] = function(){ 155 | var args = new Array(arguments.length); 156 | for (var i = 0, l = arguments.length; i < l; i++){ 157 | args[i] = arguments[i]; 158 | } 159 | return this.run( key, args, _ => { 160 | return CanvasRenderingContext2D.prototype[ key ].apply( this.context, args ); 161 | }); 162 | } 163 | } else { 164 | CanvasRenderingContext2DWrapper.prototype[ key ] = CanvasRenderingContext2D.prototype[ key ]; 165 | } 166 | } catch( e ){ 167 | Object.defineProperty( CanvasRenderingContext2DWrapper.prototype, key, { 168 | get: function (){ return this.context[ key ]; }, 169 | set: function ( v ){ this.context[ key ] = v; } 170 | }); 171 | } 172 | 173 | } 174 | 175 | }); 176 | 177 | function WebGLRenderingContextWrapper( context ){ 178 | 179 | Wrapper.call( this, context ); 180 | 181 | this.queryStack = []; 182 | this.activeQuery = null; 183 | this.queryExt = null; 184 | 185 | this.drawQueries = []; 186 | 187 | this.programCount = 0; 188 | this.textureCount = 0; 189 | 190 | this.useProgramCount = 0; 191 | this.bindTextureCount = 0; 192 | 193 | this.drawArrayCalls = 0; 194 | this.drawElementsCalls = 0; 195 | 196 | this.pointsCount = 0; 197 | this.linesCount = 0; 198 | this.trianglesCount = 0; 199 | 200 | } 201 | 202 | WebGLRenderingContextWrapper.prototype = Object.create( Wrapper.prototype ); 203 | 204 | WebGLRenderingContextWrapper.prototype.cloned = false; 205 | 206 | cloneWebGLRenderingContextPrototype(); 207 | 208 | WebGLRenderingContextWrapper.prototype.resetFrame = function(){ 209 | 210 | Wrapper.prototype.resetFrame.call( this ); 211 | 212 | this.useProgramCount = 0; 213 | this.bindTextureCount = 0; 214 | 215 | this.drawArrayCalls = 0; 216 | this.drawElementsCalls = 0; 217 | 218 | this.pointsCount = 0; 219 | this.linesCount = 0; 220 | this.trianglesCount = 0; 221 | 222 | } 223 | 224 | function cloneWebGLRenderingContextPrototype(){ 225 | 226 | // some sites (e.g. http://codeflow.org/webgl/deferred-irradiance-volumes/www/) 227 | // modify the prototype, and they do it after the initial check for support 228 | 229 | // if( WebGLRenderingContextWrapper.prototype.cloned ) return; 230 | // WebGLRenderingContextWrapper.prototype.cloned = true; 231 | 232 | Object.keys( WebGLRenderingContext.prototype ).forEach( key => { 233 | 234 | // .canvas is weird, so it's directly assigned when creating the wrapper 235 | 236 | if( key !== 'canvas' ){ 237 | 238 | try{ 239 | if( typeof WebGLRenderingContext.prototype[ key ] === 'function' ){ 240 | WebGLRenderingContextWrapper.prototype[ key ] = function(){ 241 | var args = new Array(arguments.length); 242 | for (var i = 0, l = arguments.length; i < l; i++){ 243 | args[i] = arguments[i]; 244 | } 245 | return this.run( key, args, _ => { 246 | return WebGLRenderingContext.prototype[ key ].apply( this.context, args ); 247 | }); 248 | } 249 | } else { 250 | WebGLRenderingContextWrapper.prototype[ key ] = WebGLRenderingContext.prototype[ key ]; 251 | } 252 | } catch( e ){ 253 | Object.defineProperty( WebGLRenderingContext.prototype, key, { 254 | get: function (){ return this.context[ key ]; }, 255 | set: function ( v ){ this.context[ key ] = v; } 256 | }); 257 | } 258 | 259 | } 260 | 261 | }); 262 | 263 | instrumentWebGLRenderingContext(); 264 | 265 | } 266 | 267 | function WebGLDebugShadersExtensionWrapper( contextWrapper ){ 268 | 269 | this.id = createUUID(); 270 | this.contextWrapper = contextWrapper; 271 | this.extension = WebGLRenderingContext.prototype.getExtension.apply( this.contextWrapper.context, [ 'WEBGL_debug_shaders' ] ); 272 | 273 | } 274 | 275 | WebGLDebugShadersExtensionWrapper.prototype.getTranslatedShaderSource = function( shaderWrapper ){ 276 | 277 | return this.extension.getTranslatedShaderSource( shaderWrapper.shader ); 278 | 279 | } 280 | 281 | WebGLRenderingContextWrapper.prototype.getExtension = function(){ 282 | 283 | this.incrementCount(); 284 | 285 | var extensionName = arguments[ 0 ]; 286 | 287 | switch( extensionName ){ 288 | 289 | case 'WEBGL_debug_shaders': 290 | return new WebGLDebugShadersExtensionWrapper( this ); 291 | break; 292 | 293 | case 'EXT_disjoint_timer_query': 294 | return new EXTDisjointTimerQueryExtensionWrapper( this ); 295 | break; 296 | 297 | } 298 | 299 | return this.context.getExtension( extensionName ); 300 | 301 | } 302 | 303 | WebGLRenderingContextWrapper.prototype.updateDrawCount = function( mode, count ){ 304 | 305 | var gl = this.context; 306 | 307 | switch( mode ){ 308 | case gl.POINTS: 309 | this.pointsCount += count; 310 | break; 311 | case gl.LINE_STRIP: 312 | this.linesCount += count - 1; 313 | break; 314 | case gl.LINE_LOOP: 315 | this.linesCount += count; 316 | break; 317 | case gl.LINES: 318 | this.linesCount += count / 2; 319 | break; 320 | case gl.TRIANGLE_STRIP: 321 | case gl.TRIANGLE_FAN: 322 | this.trianglesCount += count - 2; 323 | break; 324 | case gl.TRIANGLES: 325 | this.trianglesCount += count / 3; 326 | break; 327 | } 328 | 329 | }; 330 | 331 | WebGLRenderingContextWrapper.prototype.drawElements = function(){ 332 | 333 | this.drawElementsCalls++; 334 | this.updateDrawCount( arguments[ 0 ], arguments[ 1 ] ); 335 | 336 | return this.run( 'drawElements', arguments, _ => { 337 | 338 | /*var ext = this.queryExt; 339 | var query = ext.createQueryEXT(); 340 | ext.beginQueryEXT( ext.TIME_ELAPSED_EXT, query ); 341 | this.drawQueries.push( query );*/ 342 | 343 | var res = WebGLRenderingContext.prototype.drawElements.apply( this.context, arguments ); 344 | 345 | //ext.endQueryEXT( ext.TIME_ELAPSED_EXT ); 346 | 347 | return res; 348 | 349 | }); 350 | 351 | } 352 | 353 | WebGLRenderingContextWrapper.prototype.drawArrays = function(){ 354 | 355 | this.drawArrayCalls++; 356 | this.updateDrawCount( arguments[ 0 ], arguments[ 2 ] ); 357 | 358 | return this.run( 'drawArrays', arguments, _ => { 359 | 360 | /*var ext = this.queryExt; 361 | var query = ext.createQueryEXT(); 362 | ext.beginQueryEXT( ext.TIME_ELAPSED_EXT, query ); 363 | this.drawQueries.push( query );*/ 364 | 365 | var res = WebGLRenderingContext.prototype.drawArrays.apply( this.context, arguments ); 366 | 367 | //ext.endQueryEXT( ext.TIME_ELAPSED_EXT ); 368 | 369 | return res; 370 | 371 | }); 372 | 373 | } 374 | 375 | var contexts = []; 376 | var canvasContexts = new WeakMap(); 377 | 378 | var getContext = HTMLCanvasElement.prototype.getContext; 379 | 380 | HTMLCanvasElement.prototype.getContext = function(){ 381 | 382 | setupUI(); 383 | 384 | var c = canvasContexts.get( this ); 385 | if( c ){ 386 | log( arguments, '(CACHED)' ); 387 | return c; 388 | } else { 389 | log( arguments ); 390 | } 391 | 392 | var context = getContext.apply( this, arguments ); 393 | 394 | if( arguments[ 0 ] === 'webgl' || arguments[ 0 ] === 'experimental-webgl' ){ 395 | 396 | var wrapper = new WebGLRenderingContextWrapper( context ); 397 | wrapper.canvas = this; 398 | var cData = new ContextData( wrapper ); 399 | cData.queryExt = wrapper.getExtension( 'EXT_disjoint_timer_query' ); 400 | wrapper.queryExt = cData.queryExt; 401 | contexts.push( cData ); 402 | canvasContexts.set( this, wrapper ); 403 | return wrapper; 404 | 405 | } 406 | 407 | if( arguments[ 0 ] === '2d' ){ 408 | 409 | var wrapper = new CanvasRenderingContext2DWrapper( context ); 410 | wrapper.canvas = this; 411 | var cData = new ContextData( wrapper ); 412 | contexts.push( cData ); 413 | canvasContexts.set( this, wrapper ); 414 | return wrapper; 415 | 416 | } 417 | 418 | canvasContexts.set( this, context ); 419 | return context; 420 | 421 | } 422 | 423 | function WebGLShaderWrapper( contextWrapper, type ){ 424 | 425 | this.id = createUUID(); 426 | this.contextWrapper = contextWrapper; 427 | this.shader = WebGLRenderingContext.prototype.createShader.apply( this.contextWrapper.context, [ type ] ); 428 | this.version = 1; 429 | this.source = null; 430 | this.type = type; 431 | 432 | } 433 | 434 | WebGLShaderWrapper.prototype.shaderSource = function( source ){ 435 | 436 | this.source = source; 437 | return WebGLRenderingContext.prototype.shaderSource.apply( this.contextWrapper.context, [ this.shader, source ] ); 438 | 439 | } 440 | 441 | function WebGLUniformLocationWrapper( contextWrapper, program, name ){ 442 | 443 | this.id = createUUID(); 444 | this.contextWrapper = contextWrapper; 445 | this.program = program; 446 | this.name = name; 447 | this.getUniformLocation(); 448 | 449 | this.program.uniformLocations[ this.name ] = this; 450 | 451 | log( 'Location for uniform', name, 'on program', this.program.id ); 452 | 453 | } 454 | 455 | WebGLUniformLocationWrapper.prototype.getUniformLocation = function(){ 456 | 457 | this.uniformLocation = WebGLRenderingContext.prototype.getUniformLocation.apply( this.contextWrapper, [ this.program.program, this.name ] ); 458 | 459 | } 460 | 461 | function WebGLProgramWrapper( contextWrapper ){ 462 | 463 | this.id = createUUID(); 464 | this.contextWrapper = contextWrapper; 465 | this.program = WebGLRenderingContext.prototype.createProgram.apply( this.contextWrapper.context ); 466 | this.version = 1; 467 | this.vertexShaderWrapper = null; 468 | this.fragmentShaderWrapper = null; 469 | 470 | this.uniformLocations = {}; 471 | 472 | } 473 | 474 | WebGLProgramWrapper.prototype.attachShader = function(){ 475 | 476 | var shaderWrapper = arguments[ 0 ]; 477 | 478 | if( shaderWrapper.type == this.contextWrapper.context.VERTEX_SHADER ) this.vertexShaderWrapper = shaderWrapper; 479 | if( shaderWrapper.type == this.contextWrapper.context.FRAGMENT_SHADER ) this.fragmentShaderWrapper = shaderWrapper; 480 | 481 | return this.contextWrapper.run( 'attachShader', arguments, _ => { 482 | return WebGLRenderingContext.prototype.attachShader.apply( this.contextWrapper.context, [ this.program, shaderWrapper.shader ] ); 483 | }); 484 | 485 | } 486 | 487 | WebGLProgramWrapper.prototype.highlight = function(){ 488 | 489 | detachShader.apply( this.contextWrapper.context, [ this.program, this.fragmentShaderWrapper.shader ] ); 490 | 491 | var fs = this.fragmentShaderWrapper.source; 492 | fs = fs.replace( /\s+main\s*\(/, ' ShaderEditorInternalMain(' ); 493 | fs += '\r\n' + 'void main(){ ShaderEditorInternalMain(); gl_FragColor.rgb *= vec3(1.,0.,1.); }'; 494 | 495 | var highlightShaderWrapper = new WebGLShaderWrapper( this.contextWrapper, this.contextWrapper.context.FRAGMENT_SHADER ); 496 | highlightShaderWrapper.shaderSource( fs ); 497 | WebGLRenderingContext.prototype.compileShader.apply( this.contextWrapper.context, [ highlightShaderWrapper.shader ] ); 498 | WebGLRenderingContext.prototype.attachShader.apply( this.contextWrapper.context, [ this.program, highlightShaderWrapper.shader ] ); 499 | WebGLRenderingContext.prototype.linkProgram.apply( this.contextWrapper.context, [ this.program ] ); 500 | 501 | Object.keys( this.uniformLocations ).forEach( name => { 502 | this.uniformLocations[ name ].getUniformLocation(); 503 | }); 504 | 505 | } 506 | 507 | function instrumentWebGLRenderingContext(){ 508 | 509 | WebGLRenderingContextWrapper.prototype.createShader = function(){ 510 | 511 | log( 'create shader' ); 512 | return this.run( 'createShader', arguments, _ => { 513 | return new WebGLShaderWrapper( this, arguments[ 0 ] ); 514 | }); 515 | 516 | } 517 | 518 | WebGLRenderingContextWrapper.prototype.shaderSource = function(){ 519 | 520 | return this.run( 'shaderSource', arguments, _ => { 521 | return arguments[ 0 ].shaderSource( arguments[ 1 ] ); 522 | }); 523 | 524 | } 525 | 526 | WebGLRenderingContextWrapper.prototype.compileShader = function(){ 527 | 528 | return this.run( 'compileShader', arguments, _ => { 529 | return WebGLRenderingContext.prototype.compileShader.apply( this.context, [ arguments[ 0 ].shader ] ); 530 | }); 531 | 532 | } 533 | 534 | WebGLRenderingContextWrapper.prototype.getShaderParameter = function(){ 535 | 536 | return this.run( 'getShaderParameter', arguments, _ => { 537 | return WebGLRenderingContext.prototype.getShaderParameter.apply( this.context, [ arguments[ 0 ].shader, arguments[ 1 ] ] ); 538 | }); 539 | 540 | } 541 | 542 | WebGLRenderingContextWrapper.prototype.getShaderInfoLog = function(){ 543 | 544 | return this.run( 'getShaderInfoLog', arguments, _ => { 545 | return WebGLRenderingContext.prototype.getShaderInfoLog.apply( this.context, [ arguments[ 0 ].shader ] ); 546 | }); 547 | 548 | } 549 | 550 | WebGLRenderingContextWrapper.prototype.deleteShader = function(){ 551 | 552 | return this.run( 'deleteShader', arguments, _ => { 553 | return WebGLRenderingContext.prototype.deleteShader.apply( this.context, [ arguments[ 0 ].shader ] ); 554 | }); 555 | 556 | } 557 | 558 | WebGLRenderingContextWrapper.prototype.createProgram = function(){ 559 | 560 | log( 'create program' ); 561 | this.programCount++; 562 | return this.run( 'createProgram', arguments, _ => { 563 | return new WebGLProgramWrapper( this ); 564 | }); 565 | 566 | } 567 | 568 | WebGLRenderingContextWrapper.prototype.deleteProgram = function( programWrapper ){ 569 | 570 | this.incrementCount(); 571 | this.programCount--; 572 | return this.run( 'deleteProgram', arguments, _ => { 573 | return WebGLRenderingContext.prototype.deleteProgram.apply( this.context, [ programWrapper.program ] ); 574 | }); 575 | 576 | } 577 | 578 | WebGLRenderingContextWrapper.prototype.attachShader = function(){ 579 | 580 | return arguments[ 0 ].attachShader( arguments[ 1 ] ); 581 | 582 | } 583 | 584 | WebGLRenderingContextWrapper.prototype.linkProgram = function(){ 585 | 586 | return this.run( 'linkProgram', arguments, _ => { 587 | return WebGLRenderingContext.prototype.linkProgram.apply( this.context, [ arguments[ 0 ].program ] ); 588 | }); 589 | } 590 | 591 | WebGLRenderingContextWrapper.prototype.getProgramParameter = function(){ 592 | 593 | return this.run( 'getProgramParameter', arguments, _ => { 594 | return WebGLRenderingContext.prototype.getProgramParameter.apply( this.context, [ arguments[ 0 ].program, arguments[ 1 ] ] ); 595 | }); 596 | 597 | } 598 | 599 | WebGLRenderingContextWrapper.prototype.getProgramInfoLog = function(){ 600 | 601 | return this.run( 'getProgramInfoLog', arguments, _ => { 602 | return WebGLRenderingContext.prototype.getProgramInfoLog.apply( this.context, [ arguments[ 0 ].program ] ); 603 | }); 604 | 605 | } 606 | 607 | WebGLRenderingContextWrapper.prototype.getActiveAttrib = function(){ 608 | 609 | return this.run( 'getActiveAttrib', arguments, _ => { 610 | return WebGLRenderingContext.prototype.getActiveAttrib.apply( this.context, [ arguments[ 0 ].program, arguments[ 1 ] ] ); 611 | }); 612 | 613 | } 614 | 615 | WebGLRenderingContextWrapper.prototype.getAttribLocation = function(){ 616 | 617 | return this.run( 'getAttribLocation', arguments, _ => { 618 | return WebGLRenderingContext.prototype.getAttribLocation.apply( this.context, [ arguments[ 0 ].program, arguments[ 1 ] ] ); 619 | }); 620 | 621 | } 622 | 623 | WebGLRenderingContextWrapper.prototype.bindAttribLocation = function(){ 624 | 625 | return this.run( 'bindAttribLocation', arguments, _ => { 626 | return WebGLRenderingContext.prototype.bindAttribLocation.apply( this.context, [ arguments[ 0 ].program, arguments[ 1 ], arguments[ 2 ] ] ); 627 | }); 628 | 629 | } 630 | 631 | WebGLRenderingContextWrapper.prototype.getActiveUniform = function(){ 632 | 633 | return this.run( 'getActiveUniform', arguments, _ => { 634 | return WebGLRenderingContext.prototype.getActiveUniform.apply( this.context, [ arguments[ 0 ].program, arguments[ 1 ] ] ); 635 | }); 636 | 637 | } 638 | 639 | WebGLRenderingContextWrapper.prototype.getUniformLocation = function(){ 640 | 641 | return this.run( 'getUniformLocation', arguments, _ => { 642 | return new WebGLUniformLocationWrapper( this.context, arguments[ 0 ], arguments[ 1 ] ); 643 | }); 644 | 645 | } 646 | 647 | WebGLRenderingContextWrapper.prototype.useProgram = function(){ 648 | 649 | this.useProgramCount++; 650 | return this.run( 'useProgram', arguments, _ => { 651 | return WebGLRenderingContext.prototype.useProgram.apply( this.context, [ arguments[ 0 ] ? arguments[ 0 ].program : null ] ); 652 | }); 653 | 654 | } 655 | 656 | var methods = [ 657 | 'uniform1f', 'uniform1fv', 'uniform1i', 'uniform1iv', 658 | 'uniform2f', 'uniform2fv', 'uniform2i', 'uniform2iv', 659 | 'uniform3f', 'uniform3fv', 'uniform3i', 'uniform3iv', 660 | 'uniform4f', 'uniform4fv', 'uniform4i', 'uniform4iv', 661 | 'uniformMatrix2fv', 'uniformMatrix3fv', 'uniformMatrix4fv' 662 | ]; 663 | 664 | var originalMethods = {}; 665 | 666 | methods.forEach( method => { 667 | 668 | var original = WebGLRenderingContext.prototype[ method ]; 669 | originalMethods[ method ] = original; 670 | 671 | WebGLRenderingContextWrapper.prototype[ method ] = function(){ 672 | 673 | var args = new Array(arguments.length); 674 | for (var i = 0, l = arguments.length; i < l; i++){ 675 | args[i] = arguments[i]; 676 | } 677 | if( !args[ 0 ] ) return; 678 | args[ 0 ] = args[ 0 ].uniformLocation; 679 | return this.run( method, args, _ => { 680 | return original.apply( this.context, args ); 681 | }); 682 | 683 | } 684 | 685 | }); 686 | 687 | } 688 | 689 | function WebGLTimerQueryEXTWrapper( contextWrapper, extension ){ 690 | 691 | this.contextWrapper = contextWrapper; 692 | this.extension = extension; 693 | this.query = this.extension.createQueryEXT(); 694 | this.time = 0; 695 | this.available = false; 696 | this.nested = []; 697 | 698 | } 699 | 700 | WebGLTimerQueryEXTWrapper.prototype.getTimes = function(){ 701 | 702 | var time = this.getTime(); 703 | this.nested.forEach( q => { 704 | time += q.getTimes(); 705 | }); 706 | 707 | return time; 708 | 709 | } 710 | 711 | WebGLTimerQueryEXTWrapper.prototype.getTime = function(){ 712 | 713 | this.time = this.extension.getQueryObjectEXT( this.query, this.extension.QUERY_RESULT_EXT ); 714 | 715 | return this.time; 716 | 717 | } 718 | 719 | WebGLTimerQueryEXTWrapper.prototype.getResultsAvailable = function(){ 720 | 721 | var res = true; 722 | this.nested.forEach( q => { 723 | res = res && q.getResultsAvailable(); 724 | }); 725 | 726 | return res; 727 | 728 | } 729 | 730 | WebGLTimerQueryEXTWrapper.prototype.getResultsAvailable = function(){ 731 | 732 | this.available = this.extension.getQueryObjectEXT( this.query, this.extension.QUERY_RESULT_AVAILABLE_EXT ); 733 | return this.available; 734 | 735 | } 736 | 737 | function EXTDisjointTimerQueryExtensionWrapper( contextWrapper ){ 738 | 739 | this.contextWrapper = contextWrapper; 740 | this.extension = WebGLRenderingContext.prototype.getExtension.apply( this.contextWrapper.context, [ 'EXT_disjoint_timer_query' ] ); 741 | 742 | this.QUERY_COUNTER_BITS_EXT = this.extension.QUERY_COUNTER_BITS_EXT; 743 | this.CURRENT_QUERY_EXT = this.extension.CURRENT_QUERY_EXT; 744 | this.QUERY_RESULT_AVAILABLE_EXT = this.extension.QUERY_RESULT_AVAILABLE_EXT; 745 | this.GPU_DISJOINT_EXT = this.extension.GPU_DISJOINT_EXT; 746 | this.QUERY_RESULT_EXT = this.extension.QUERY_RESULT_EXT; 747 | this.TIME_ELAPSED_EXT = this.extension.TIME_ELAPSED_EXT; 748 | this.TIMESTAMP_EXT = this.extension.TIMESTAMP_EXT; 749 | 750 | } 751 | 752 | EXTDisjointTimerQueryExtensionWrapper.prototype.createQueryEXT = function(){ 753 | 754 | return new WebGLTimerQueryEXTWrapper( this.contextWrapper, this.extension ); 755 | 756 | } 757 | 758 | EXTDisjointTimerQueryExtensionWrapper.prototype.beginQueryEXT = function( type, query ){ 759 | 760 | if( this.contextWrapper.activeQuery ){ 761 | this.extension.endQueryEXT( type ); 762 | this.contextWrapper.activeQuery.nested.push( query ); 763 | this.contextWrapper.queryStack.push( this.contextWrapper.activeQuery ); 764 | } 765 | 766 | this.contextWrapper.activeQuery = query; 767 | 768 | return this.extension.beginQueryEXT( type, query.query ); 769 | 770 | } 771 | 772 | EXTDisjointTimerQueryExtensionWrapper.prototype.endQueryEXT = function( type ){ 773 | 774 | this.contextWrapper.activeQuery = this.contextWrapper.queryStack.pop(); 775 | var res = this.extension.endQueryEXT( type ); 776 | if( this.contextWrapper.activeQuery ){ 777 | var newQuery = new WebGLTimerQueryEXTWrapper( this.contextWrapper, this.extension ); 778 | this.contextWrapper.activeQuery.nested.push( newQuery ); 779 | this.extension.beginQueryEXT( type, newQuery.query ); 780 | } 781 | return res; 782 | 783 | } 784 | 785 | EXTDisjointTimerQueryExtensionWrapper.prototype.getQueryObjectEXT = function( query, pname ){ 786 | 787 | if( pname === this.extension.QUERY_RESULT_AVAILABLE_EXT ){ 788 | return query.getResultsAvailable(); 789 | } 790 | 791 | if( pname === this.extension.QUERY_RESULT_EXT ){ 792 | return query.getTimes(); 793 | } 794 | 795 | return this.extension.getQueryObjectEXT( query.query, pname ); 796 | 797 | } 798 | 799 | EXTDisjointTimerQueryExtensionWrapper.prototype.getQueryEXT = function( target, pname ){ 800 | 801 | return this.extension.getQueryEXT( target, pname ); 802 | 803 | } 804 | 805 | // 806 | // This is the UI 807 | // 808 | 809 | var text; 810 | var uiIsSetup = false; 811 | 812 | function setupUI() { 813 | 814 | if( uiIsSetup ) return; 815 | uiIsSetup = true; 816 | 817 | text = document.createElement( 'div' ); 818 | text.setAttribute( 'id', 'perfmeter-panel' ); 819 | 820 | var fileref = document.createElement("link"); 821 | fileref.rel = "stylesheet"; 822 | fileref.type = "text/css"; 823 | fileref.href = settings.cssPath; 824 | 825 | window.document.getElementsByTagName("head")[0].appendChild(fileref); 826 | 827 | if( !window.document.body ) { 828 | window.addEventListener( 'load', function() { 829 | window.document.body.appendChild( text ); 830 | } ); 831 | } else { 832 | window.document.body.appendChild( text ); 833 | } 834 | 835 | } 836 | 837 | // 838 | // This is the rAF queue processing 839 | // 840 | 841 | var originalRequestAnimationFrame = window.requestAnimationFrame; 842 | var rAFQueue = []; 843 | var frameCount = 0; 844 | var frameId = 0; 845 | var framerate = 0; 846 | var lastTime = 0; 847 | 848 | window.requestAnimationFrame = function( c ){ 849 | 850 | rAFQueue.push( c ); 851 | 852 | } 853 | 854 | function processRequestAnimationFrames( timestamp ){ 855 | 856 | contexts.forEach( ctx => { 857 | 858 | ctx.contextWrapper.resetFrame(); 859 | 860 | var ext = ctx.queryExt; 861 | 862 | if( ext ){ 863 | 864 | var query = ext.createQueryEXT(); 865 | ext.beginQueryEXT( ext.TIME_ELAPSED_EXT, query ); 866 | ctx.extQueries.push( query ); 867 | 868 | } 869 | 870 | }); 871 | 872 | var startTime = performance.now(); 873 | 874 | var queue = rAFQueue.slice( 0 ); 875 | rAFQueue.length = 0; 876 | queue.forEach( rAF => { 877 | rAF( timestamp ); 878 | }); 879 | 880 | var endTime = performance.now(); 881 | var frameTime = endTime - startTime; 882 | 883 | frameCount++; 884 | if( endTime > lastTime + 1000 ) { 885 | framerate = frameCount * 1000 / ( endTime - lastTime ); 886 | frameCount = 0; 887 | lastTime = endTime; 888 | } 889 | 890 | frameId++; 891 | 892 | var logs = []; 893 | 894 | contexts.forEach( ctx => { 895 | 896 | var ext = ctx.queryExt; 897 | 898 | if( ext ){ 899 | 900 | ext.endQueryEXT( ext.TIME_ELAPSED_EXT ); 901 | 902 | ctx.extQueries.forEach( ( query, i ) => { 903 | 904 | var available = ext.getQueryObjectEXT( query, ext.QUERY_RESULT_AVAILABLE_EXT ); 905 | var disjoint = ctx.contextWrapper.context.getParameter( ext.GPU_DISJOINT_EXT ); 906 | 907 | if (available && !disjoint){ 908 | 909 | var queryTime = ext.getQueryObjectEXT( query, ext.QUERY_RESULT_EXT ); 910 | var time = queryTime; 911 | if (ctx.contextWrapper.count ){ 912 | logs.push( { 913 | id: ctx.contextWrapper.id, 914 | count: ctx.contextWrapper.count, 915 | time: ( time / 1000000 ).toFixed( 2 ), 916 | jstime: ctx.contextWrapper.JavaScriptTime.toFixed(2), 917 | drawArrays: ctx.contextWrapper.drawArrayCalls, 918 | drawElements: ctx.contextWrapper.drawElementsCalls, 919 | points: ctx.contextWrapper.pointsCount, 920 | lines: ctx.contextWrapper.linesCount, 921 | triangles: ctx.contextWrapper.trianglesCount, 922 | programs: ctx.contextWrapper.programCount, 923 | usePrograms: ctx.contextWrapper.useProgramCount 924 | } ); 925 | } 926 | ctx.extQueries.splice( i, 1 ); 927 | 928 | } 929 | 930 | }); 931 | 932 | /*ctx.contextWrapper.drawQueries.forEach( ( query, i ) => { 933 | 934 | var available = ext.getQueryObjectEXT( query, ext.QUERY_RESULT_AVAILABLE_EXT ); 935 | var disjoint = ctx.contextWrapper.context.getParameter( ext.GPU_DISJOINT_EXT ); 936 | 937 | if (available && !disjoint){ 938 | 939 | var queryTime = ext.getQueryObjectEXT( query, ext.QUERY_RESULT_EXT ); 940 | var time = queryTime; 941 | if (ctx.contextWrapper.count ){ 942 | log( 'Draw ', time ); 943 | } 944 | ctx.contextWrapper.drawQueries.splice( i, 1 ); 945 | 946 | } 947 | 948 | });*/ 949 | 950 | } 951 | 952 | }); 953 | 954 | var str = `Framerate: ${framerate.toFixed(2)} FPS 955 | Frame JS time: ${frameTime.toFixed(2)} ms 956 | 957 | `; 958 | logs.forEach( l => { 959 | str += `Canvas 960 | ID: ${l.id} 961 | Count: ${l.count} 962 | Canvas time: ${l.jstime} ms 963 | WebGL 964 | GPU time: ${l.time} ms 965 | Programs: ${l.programs} 966 | usePrograms: ${l.usePrograms} 967 | dArrays: ${l.drawArrays} 968 | dElems: ${l.drawElements} 969 | Points: ${l.points} 970 | Lines: ${l.lines} 971 | Triangles: ${l.triangles} 972 | 973 | `; 974 | }); 975 | if( text ) text.innerHTML = str; 976 | 977 | originalRequestAnimationFrame( processRequestAnimationFrames ); 978 | 979 | } 980 | 981 | processRequestAnimationFrames(); 982 | 983 | })(); 984 | -------------------------------------------------------------------------------- /common.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function log() { 4 | 5 | console.log.apply( 6 | console, [ 7 | `%c PerfMeter `, 8 | 'background: #1E9433; color: #ffffff; text-shadow: 0 -1px #000; padding: 4px 0 4px 0; line-height: 0', 9 | ...arguments 10 | ] 11 | ); 12 | 13 | } 14 | 15 | function fixSettings( settings ) { 16 | 17 | if( settings === undefined ) return defaultSettings; 18 | 19 | var res = {}; 20 | 21 | Object.keys( defaultSettings ).forEach( f => { 22 | 23 | res[ f ] = ( settings[ f ] !== undefined ) ? settings[ f ] : defaultSettings[ f ]; 24 | 25 | } ); 26 | 27 | return res; 28 | 29 | } 30 | 31 | // chrome.storage can store Objects directly 32 | 33 | function loadSettings() { 34 | 35 | return new Promise( ( resolve, reject ) => { 36 | 37 | chrome.storage.sync.get( 'settings', obj => { 38 | resolve( fixSettings( obj.settings ) ); 39 | } ); 40 | 41 | } ); 42 | 43 | } 44 | 45 | function saveSettings( settings ) { 46 | 47 | return new Promise( ( resolve, reject ) => { 48 | 49 | chrome.storage.sync.set( { 'settings': settings }, obj => { 50 | resolve( obj ); 51 | } ); 52 | 53 | } ); 54 | 55 | } 56 | -------------------------------------------------------------------------------- /content-script.js: -------------------------------------------------------------------------------- 1 | var verbose = false; 2 | 3 | function log() { 4 | 5 | console.log.apply( 6 | console, [ 7 | `%c PerfMeter `, 8 | 'background: #1E9433; color: #ffffff; text-shadow: 0 -1px #000; padding: 4px 0 4px 0; line-height: 0', 9 | ...arguments 10 | ] 11 | ); 12 | 13 | } 14 | 15 | log( 'content script', window.location.toString() ); 16 | 17 | var port = chrome.runtime.connect( { name: 'contentScript' } ); 18 | port.postMessage( { method: 'ready' } ); 19 | 20 | port.onDisconnect.addListener( function() { 21 | port = null; 22 | log( 'Port disconnected' ); 23 | }) 24 | 25 | window.addEventListener( 'perfmeter-message', e => { 26 | 27 | if( verbose ) log( e.detail ); 28 | port.postMessage( e.detail ); 29 | 30 | } ); 31 | 32 | window.addEventListener( 'perfmeter-check-content-script', e => { 33 | 34 | var response = new Event( 'perfmeter-content-script-available' ); 35 | window.dispatchEvent( response ); 36 | 37 | } ); 38 | -------------------------------------------------------------------------------- /css/Roboto_Mono/LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /css/Roboto_Mono/RobotoMono-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spite/PerfMeter/5f3a8d1d45df515d4d9d8d487524e26ae44c2a11/css/Roboto_Mono/RobotoMono-Bold.ttf -------------------------------------------------------------------------------- /css/Roboto_Mono/RobotoMono-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spite/PerfMeter/5f3a8d1d45df515d4d9d8d487524e26ae44c2a11/css/Roboto_Mono/RobotoMono-BoldItalic.ttf -------------------------------------------------------------------------------- /css/Roboto_Mono/RobotoMono-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spite/PerfMeter/5f3a8d1d45df515d4d9d8d487524e26ae44c2a11/css/Roboto_Mono/RobotoMono-Italic.ttf -------------------------------------------------------------------------------- /css/Roboto_Mono/RobotoMono-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spite/PerfMeter/5f3a8d1d45df515d4d9d8d487524e26ae44c2a11/css/Roboto_Mono/RobotoMono-Light.ttf -------------------------------------------------------------------------------- /css/Roboto_Mono/RobotoMono-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spite/PerfMeter/5f3a8d1d45df515d4d9d8d487524e26ae44c2a11/css/Roboto_Mono/RobotoMono-LightItalic.ttf -------------------------------------------------------------------------------- /css/Roboto_Mono/RobotoMono-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spite/PerfMeter/5f3a8d1d45df515d4d9d8d487524e26ae44c2a11/css/Roboto_Mono/RobotoMono-Medium.ttf -------------------------------------------------------------------------------- /css/Roboto_Mono/RobotoMono-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spite/PerfMeter/5f3a8d1d45df515d4d9d8d487524e26ae44c2a11/css/Roboto_Mono/RobotoMono-MediumItalic.ttf -------------------------------------------------------------------------------- /css/Roboto_Mono/RobotoMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spite/PerfMeter/5f3a8d1d45df515d4d9d8d487524e26ae44c2a11/css/Roboto_Mono/RobotoMono-Regular.ttf -------------------------------------------------------------------------------- /css/Roboto_Mono/RobotoMono-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spite/PerfMeter/5f3a8d1d45df515d4d9d8d487524e26ae44c2a11/css/Roboto_Mono/RobotoMono-Thin.ttf -------------------------------------------------------------------------------- /css/Roboto_Mono/RobotoMono-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spite/PerfMeter/5f3a8d1d45df515d4d9d8d487524e26ae44c2a11/css/Roboto_Mono/RobotoMono-ThinItalic.ttf -------------------------------------------------------------------------------- /css/styles.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Roboto Mono'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: url('./Roboto_Mono/RobotoMono-Regular.ttf') format('truetype'); 6 | } 7 | 8 | @font-face { 9 | font-family: 'Roboto Mono'; 10 | font-style: normal; 11 | font-weight: 700; 12 | src: url('./Roboto_Mono/RobotoMono-Bold.ttf') format('truetype'); 13 | } 14 | 15 | #perfmeter-panel{ 16 | all: initial; 17 | text-align: right; 18 | pointer-events: none; 19 | font-family: "Roboto mono"; 20 | font-size: 10px; 21 | position: fixed; 22 | background-color: rgba( 0, 0, 0, .5 ); 23 | padding: 10px; 24 | right: 5px; 25 | top: 5px; 26 | color: white; 27 | text-shadow: 0 -1px 0 #000; 28 | z-index: 2147483647; 29 | white-space: pre; 30 | } 31 | 32 | #perfmeter-panel b{ 33 | font: inherit; 34 | font-weight: 700; 35 | color: #9EFD38; 36 | } 37 | -------------------------------------------------------------------------------- /devtools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerfMeter DevTools 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /devtools.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | chrome.devtools.panels.create( 'PerfMeter', 'assets/icon.svg', 'panel.html', initialize ); 4 | 5 | var port = chrome.runtime.connect( null, { name: `devtools` } ); 6 | var tabId = chrome.devtools.inspectedWindow.tabId; 7 | 8 | function log( ...args ) { 9 | 10 | var strArgs = [ 11 | '"%c PerfMeter | DevTools "', 12 | '"background: #1E9433; color: #ffffff; text-shadow: 0 -1px #000; padding: 4px 0 4px 0; line-height: 0;"', 13 | ...args.map( v => JSON.stringify( v ) ) 14 | ]; 15 | 16 | chrome.devtools.inspectedWindow.eval( 17 | `console.log(${strArgs});`, 18 | ( result, isException ) => console.log( result, isException ) 19 | ); 20 | 21 | } 22 | 23 | function post( msg ) { 24 | 25 | msg.tabId = tabId; 26 | port.postMessage( msg ); 27 | 28 | } 29 | 30 | post( { action: 'start' } ); 31 | post( { action: 'getScript' } ); 32 | 33 | port.onDisconnect.addListener( _ => { 34 | log( 'Disconnect' ); 35 | } ); 36 | 37 | var script = ''; 38 | var settings = {}; 39 | var panelWindow = null; 40 | var scriptStatus = 0; 41 | 42 | var recordBuffer = []; 43 | var pollingInterval = null; 44 | var recording = false; 45 | 46 | var updateRecording = throttle( _ => panelWindow.updatePanelStatus(), 500, panelWindow ); 47 | 48 | function processMessageFromScript( msg ) { 49 | 50 | if( msg.method === 'frame' ) { 51 | recordBuffer.push( msg.data ); 52 | updateRecording(); 53 | } else { 54 | if( panelWindow ) { 55 | panelWindow.onScriptMessage( msg ); 56 | } 57 | } 58 | 59 | } 60 | 61 | function poll() { 62 | 63 | chrome.devtools.inspectedWindow.eval( 64 | 'window.__PerfMeterQueryMessageQueue()', 65 | ( result, isException ) => { 66 | if( result ) { 67 | result.forEach( msg => processMessageFromScript( msg ) ); 68 | } 69 | } 70 | ); 71 | 72 | } 73 | 74 | function hasContentScript() { 75 | 76 | return new Promise( ( resolve, reject ) => { 77 | 78 | chrome.devtools.inspectedWindow.eval( 79 | 'window.__PerfMeterHasContentScript()', 80 | ( result, isException ) => { 81 | if( result === true ) resolve(); 82 | else reject(); 83 | } 84 | ); 85 | 86 | } ); 87 | 88 | } 89 | 90 | port.onMessage.addListener( msg => { 91 | 92 | switch( msg.action ) { 93 | case 'settings': 94 | settings = msg.settings; 95 | if( panelWindow && panelWindow.setSettings ) { 96 | panelWindow.setSettings( settings ); 97 | var code = `(function() { 98 | var e = new CustomEvent( 'perfmeter-settings', { 99 | detail: ${JSON.stringify(settings)} 100 | } ); 101 | window.dispatchEvent( e ); 102 | })();` 103 | chrome.devtools.inspectedWindow.eval( 104 | code, 105 | ( result, isException ) => { 106 | if( isException ) log( result, isException ) 107 | } 108 | ); 109 | } 110 | break; 111 | case 'script': 112 | script = msg.source; 113 | break; 114 | case 'inject': 115 | scriptStatus = 2; 116 | chrome.devtools.inspectedWindow.eval( 117 | `(function(){var settings=${JSON.stringify( settings )}; ${script};})();`, 118 | ( result, isException ) => { if( isException ) log( result, isException ) } 119 | ); 120 | break; 121 | case 'fromScript': 122 | processMessageFromScript( msg.data ); 123 | break; 124 | } 125 | 126 | } ); 127 | 128 | function startRecording() { 129 | 130 | chrome.devtools.inspectedWindow.eval( 131 | `window.__PerfMeterStartRecording();`, 132 | ( result, isException ) => log( result, isException ) 133 | ); 134 | 135 | } 136 | 137 | function initialize( panel ) { 138 | 139 | panel.onShown.addListener( wnd => { 140 | 141 | if( !panelWindow ) { 142 | 143 | panelWindow = wnd; 144 | 145 | panelWindow.getScriptStatus = function() { 146 | return scriptStatus; 147 | }; 148 | 149 | panelWindow.getSettings = function() { 150 | return settings; 151 | }; 152 | 153 | panelWindow.getRecordingStatus = function() { 154 | return { status: recording, bufferSize: recordBuffer.length }; 155 | }; 156 | 157 | panelWindow.inject = function() { 158 | scriptStatus = 2; 159 | chrome.devtools.inspectedWindow.eval( 160 | `(function(){var settings=${JSON.stringify( settings )}; ${script};})();`, 161 | ( result, isException ) => log( result, isException ) 162 | ); 163 | }; 164 | 165 | panelWindow.reload = function() { 166 | scriptStatus = 1; 167 | post( { action: 'reload' } ); 168 | chrome.devtools.inspectedWindow.reload( { 169 | injectedScript: script //`(function(){var settings=${JSON.stringify( settings )}; ${script};})();` 170 | } ); 171 | }; 172 | 173 | panelWindow.startRecordingData = function() { 174 | log( 'Start Recording...' ); 175 | recording = true; 176 | recordBuffer = []; 177 | hasContentScript().then( _ => { 178 | startRecording(); 179 | } ).catch( _ => { 180 | pollingInterval = setInterval( poll, 100 ); 181 | startRecording(); 182 | } ); 183 | panelWindow.updatePanelStatus(); 184 | }; 185 | 186 | panelWindow.stopRecordingData = function() { 187 | recording = false; 188 | pollingInterval = clearInterval( pollingInterval ); 189 | log( `Recording stopped, ${recordBuffer.length} samples taken` );//, recordBuffer ); 190 | chrome.devtools.inspectedWindow.eval( 191 | `window.__PerfMeterStopRecording();`, 192 | ( result, isException ) => log( result, isException ) 193 | ); 194 | panelWindow.updatePanelStatus(); 195 | setTimeout( _ => panelWindow.plotRecording( recordBuffer ), 1 ); 196 | }; 197 | 198 | panelWindow.updateSettings = function() { 199 | 200 | post( { 201 | action: 'setSettings', 202 | settings: settings 203 | } ); 204 | 205 | }; 206 | 207 | } 208 | 209 | log( 'Show' ); 210 | 211 | panelWindow.setSettings( settings ); 212 | panelWindow.updatePanelStatus(); 213 | 214 | //post( { action: 'onShown' } ); 215 | 216 | } ); 217 | 218 | panel.onHidden.addListener( wnd => { 219 | post( { action: 'onHidden' } ); 220 | } ); 221 | 222 | } 223 | -------------------------------------------------------------------------------- /graph/graph.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | "use strict"; 4 | 5 | function throttle(fn, threshold, scope) { 6 | threshold || (threshold = 250); 7 | var last, 8 | deferTimer; 9 | return function () { 10 | var context = scope || this; 11 | 12 | var now = +new Date, 13 | args = arguments; 14 | if (last && now < last + threshold) { 15 | clearTimeout(deferTimer); 16 | deferTimer = setTimeout(function () { 17 | last = now; 18 | fn.apply(context, args); 19 | }, threshold); 20 | } else { 21 | last = now; 22 | fn.apply(context, args); 23 | } 24 | }; 25 | } 26 | 27 | function debounce(fn, delay) { 28 | var timer = null; 29 | return function () { 30 | var context = this, args = arguments; 31 | clearTimeout(timer); 32 | timer = setTimeout(function () { 33 | fn.apply(context, args); 34 | }, delay); 35 | }; 36 | } 37 | 38 | function Graph( properties ) { 39 | 40 | this.properties = properties; 41 | 42 | this.properties.baselines = this.properties.baselines || []; 43 | this.decorator = this.properties.decorator || ( v => v ); 44 | 45 | this.data = []; 46 | this.start = 0; 47 | this.end = 0; 48 | this.paddingTop = 2; 49 | this.pivot = 0; 50 | this.lastPoint = 0; 51 | 52 | this.max = 0; 53 | this.min = Number.MAX_VALUE; 54 | 55 | this.canvas = document.createElement( 'canvas' ); 56 | this.canvas.style.width = '100%'; 57 | this.canvas.style.height = '100%'; 58 | this.canvas.style.position = 'absolute'; 59 | this.canvas.style.left = 0; 60 | this.canvas.style.top = 0; 61 | 62 | this.ctx = this.canvas.getContext( '2d' ); 63 | this.dpr = window.devicePixelRatio; 64 | this.properties.target.appendChild( this.canvas ); 65 | 66 | this.title = document.createElement( 'h1' ); 67 | this.title.textContent = this.properties.title; 68 | this.properties.target.appendChild( this.title ); 69 | 70 | this.label = document.createElement( 'div' ); 71 | this.label.className = 'label hidden'; 72 | this.properties.target.appendChild( this.label ); 73 | 74 | this.overlayCanvas = document.createElement( 'canvas' ); 75 | this.overlayCanvas.style.width = '100%'; 76 | this.overlayCanvas.style.height = '100%'; 77 | this.overlayCanvas.style.position = 'absolute'; 78 | this.overlayCanvas.style.left = 0; 79 | this.overlayCanvas.style.top = 0; 80 | this.overlayCanvas.style.pointerEvents = 'none'; 81 | 82 | this.overlayCtx = this.overlayCanvas.getContext( '2d' ); 83 | this.properties.target.appendChild( this.overlayCanvas ); 84 | 85 | this.linkIn = this.showLabel; 86 | this.linkOut = this.showLabel; 87 | this.linkOver = this.updatePoint; 88 | this.linkZoom = this.updateZoom; 89 | 90 | this.resize(); 91 | 92 | this.zoom = 1; 93 | 94 | var debouncedResize = debounce( this.resize.bind( this ), 100 ); 95 | window.addEventListener( 'resize', function( e ){ 96 | debouncedResize(); 97 | }.bind( this ) ); 98 | 99 | this.canvas.addEventListener( 'mouseover', e => { 100 | 101 | this.linkIn( e.pageX ); 102 | this.linkOver( e.pageX ); 103 | 104 | } ); 105 | 106 | this.canvas.addEventListener( 'mouseout', e => { 107 | 108 | this.linkOut(); 109 | 110 | } ); 111 | 112 | this.canvas.addEventListener( 'mousemove', e => { 113 | 114 | this.linkOver( e.pageX ); 115 | 116 | }) 117 | 118 | var debouncedLinkZoom = throttle( z => this.linkZoom( z ), 20 ); 119 | 120 | this.canvas.addEventListener( 'wheel', e => { 121 | 122 | debouncedLinkZoom( this.zoom + ( .005 * e.deltaY ) ); 123 | e.preventDefault(); 124 | 125 | } ); 126 | 127 | } 128 | 129 | Graph.link = function( graphs ) { 130 | 131 | graphs.forEach( g => { 132 | g.linkIn = x => { graphs.forEach( g => g.showLabel( true ) ); }; 133 | g.linkOut = x => { graphs.forEach( g => g.showLabel( false ) ); }; 134 | g.linkOver = x => { graphs.forEach( g => g.updatePoint( x ) ); }; 135 | g.linkZoom = z => { graphs.forEach( g => g.updateZoom( z ) ); }; 136 | } ) 137 | 138 | } 139 | 140 | Graph.prototype.updateZoom = function( zoom ) { 141 | 142 | this.zoom = zoom; 143 | if( this.zoom > 1 ) this.zoom = 1; 144 | this.update(); 145 | 146 | } 147 | 148 | Graph.prototype.showLabel = function( show ) { 149 | 150 | if( show ) { 151 | this.label.classList.remove( 'hidden' ); 152 | } else { 153 | this.label.classList.add( 'hidden' ); 154 | this.overlayCtx.clearRect( 0, 0, this.overlayCanvas.width, this.overlayCanvas.height ); 155 | } 156 | 157 | } 158 | 159 | Graph.prototype.updatePoint = function( x ) { 160 | 161 | if( x === undefined ) x = this.lastPoint; 162 | this.lastPoint = x; 163 | 164 | if( this.data.length === 0 ) return; 165 | 166 | var res = this.updateLabelPosition( x ); 167 | var pos = res.x * ( this.end - this.start ) / res.width + this.start; 168 | this.pivot = res.x / res.width; 169 | var y = ( this.data.find( v => v.x >= pos ) ).y; 170 | this.label.textContent = this.decorator( y ); 171 | 172 | var x = res.x * this.dpr; 173 | var y = this.paddingTop + this.adjustY( y ); 174 | 175 | this.overlayCtx.clearRect( 0, 0, this.overlayCanvas.width, this.overlayCanvas.height ); 176 | this.overlayCtx.globalCompositeOperation = 'color-burn'; 177 | this.overlayCtx.strokeStyle = '#000000' 178 | this.overlayCtx.globalAlpha = .5; 179 | this.overlayCtx.lineWidth = 2; 180 | 181 | this.overlayCtx.setLineDash( [] ) 182 | this.overlayCtx.beginPath(); 183 | this.overlayCtx.arc( x, y, 4, 0, 2 * Math.PI ); 184 | this.overlayCtx.stroke(); 185 | 186 | this.overlayCtx.setLineDash( [ 2, 4 ] ) 187 | this.overlayCtx.beginPath(); 188 | this.overlayCtx.moveTo( x, this.overlayCanvas.height ); 189 | this.overlayCtx.lineTo( x, y + 3 ); 190 | this.overlayCtx.stroke(); 191 | 192 | } 193 | 194 | Graph.prototype.updateLabelPosition = function( x ) { 195 | 196 | var divRect = this.canvas.getBoundingClientRect(); 197 | var canvasRect = this.canvas.getBoundingClientRect(); 198 | var x = x - canvasRect.left; 199 | if( x < .5 * this.canvas.clientWidth ) { 200 | this.label.classList.remove( 'flip' ); 201 | this.label.style.transform = `translate3d(${x}px,0,0)`; 202 | this.title.classList.add( 'flip' ); 203 | } else { 204 | this.label.classList.add( 'flip' ); 205 | this.label.style.transform = `translate3d(${-(this.canvas.clientWidth-x)}px,0,0)`; 206 | this.title.classList.remove( 'flip' ); 207 | } 208 | return { x: x, width: canvasRect.width }; 209 | 210 | } 211 | 212 | Graph.prototype.resize = function() { 213 | 214 | this.canvas.width = this.properties.target.clientWidth * this.dpr; 215 | this.canvas.height = this.properties.target.clientHeight * this.dpr; 216 | 217 | this.overlayCanvas.width = this.canvas.width; 218 | this.overlayCanvas.height = this.canvas.height; 219 | 220 | this.update(); 221 | 222 | } 223 | 224 | Graph.prototype.set = function( data ) { 225 | 226 | this.data = data; 227 | this.update(); 228 | 229 | } 230 | 231 | Graph.prototype.update = function() { 232 | 233 | if( this.data.length === 0 ) return; 234 | 235 | if( this.zoom > 1 ) this.zoom = 1; 236 | if( this.zoom < .1 ) this.zoom = .1; 237 | 238 | var first = this.data[ 0 ].x; 239 | var last = this.data[ this.data.length - 1 ].x; 240 | var w = last - first; 241 | this.start = first + this.pivot * w - this.pivot * this.zoom * w; 242 | this.end = first + ( this.pivot + ( 1 - this.pivot ) * this.zoom ) * w; 243 | console.log( this.zoom,this.start, this.end ); 244 | 245 | this.max = 0; 246 | this.min = Number.MAX_VALUE; 247 | 248 | this.data.forEach( ( v, i ) => { 249 | if( v.x >= this.start && v.x <= this.end ) { 250 | if( v.y < this.min ) this.min = v.y; 251 | if( v.y > this.max ) this.max = v.y; 252 | } 253 | } ); 254 | 255 | if( this.properties.baselines.length && 256 | this.properties.baselines[ 0 ] > this.max ) { 257 | this.max = this.properties.baselines[ 0 ]; 258 | } 259 | 260 | this.refresh(); 261 | this.updatePoint(); 262 | 263 | } 264 | 265 | function createAdjustFunction( min, max, size ) { 266 | 267 | return function adjust( v ) { 268 | 269 | return ( v - min ) * size / ( max - min ); 270 | 271 | } 272 | 273 | } 274 | 275 | Graph.prototype.refresh = function() { 276 | 277 | if( !this.data.length ) return; 278 | 279 | this.adjustX = createAdjustFunction( this.start, this.end, this.canvas.width ); 280 | this.adjustY = createAdjustFunction( this.max, 0, this.canvas.height - this.paddingTop ); 281 | 282 | this.ctx.fillStyle = '#efefef' 283 | this.ctx.fillRect( 0, 0, this.canvas.width, this.canvas.height ); 284 | 285 | var path = new Path2D(); 286 | 287 | var ovx = 0; 288 | var ovy = 0; 289 | var acc = 0; 290 | var samples = 0; 291 | 292 | this.data.forEach( ( v, i ) => { 293 | 294 | var vx = ~~( this.adjustX( v.x ) ); 295 | var vy = this.adjustY( v.y ); 296 | 297 | if( i === 0 ) { 298 | path.moveTo( vx, vy ); 299 | ovx = vx; 300 | ovy = vy; 301 | } else { 302 | acc += vy; 303 | samples++; 304 | //if( vx > ovx ) { 305 | vy = this.paddingTop + acc / samples; 306 | acc = 0; 307 | samples = 0; 308 | var cpx = ovx + ( vx - ovx ) * .5; 309 | var cpy1 = ( vy < ovy ) ? vy : ovy; 310 | var cpy2 = ( vy < ovy ) ? ovy : vy; 311 | //path.lineTo( this.adjustX( ~~v.x ), this.adjustY( v.y ) ); 312 | path.bezierCurveTo( cpx, cpy1, cpx, cpy2, vx, vy ); 313 | ovx = vx; 314 | ovy = vy; 315 | //} 316 | } 317 | 318 | } ); 319 | 320 | var path2 = new Path2D( path ); 321 | path2.lineTo( this.canvas.width, this.canvas.height ); 322 | path2.lineTo( 0, this.canvas.height ); 323 | 324 | this.ctx.fillStyle = this.properties.color; 325 | this.ctx.fill( path2 ); 326 | 327 | this.ctx.translate( 0, 2 ); 328 | this.ctx.lineWidth = 1.5; 329 | this.ctx.globalCompositeOperation = 'color-burn'; 330 | this.ctx.strokeStyle = '#000000' 331 | this.ctx.globalAlpha = .1; 332 | this.ctx.stroke( path ); 333 | 334 | this.ctx.translate( 0, -2 ); 335 | 336 | if( this.properties.baselines.length ) { 337 | 338 | this.ctx.beginPath(); 339 | this.ctx.lineWidth = 1; 340 | this.ctx.globalAlpha = .25; 341 | this.properties.baselines.forEach( baseline => { 342 | var y = this.paddingTop + this.adjustY( baseline ); 343 | this.ctx.moveTo( 0, y ); 344 | this.ctx.lineTo( this.canvas.width, y ); 345 | } ); 346 | this.ctx.stroke(); 347 | 348 | } 349 | 350 | if( this.properties.baseline_range ) { 351 | 352 | this.ctx.beginPath(); 353 | this.ctx.lineWidth = 1; 354 | this.ctx.globalAlpha = .25; 355 | var steps = ~~( this.max / this.properties.baseline_range ); 356 | for( var j = 0; j < steps; j++ ) { 357 | var y = this.paddingTop + this.adjustY( j * this.properties.baseline_range ); 358 | this.ctx.moveTo( 0, y ); 359 | this.ctx.lineTo( this.canvas.width, y ); 360 | } 361 | this.ctx.stroke(); 362 | 363 | } 364 | 365 | this.ctx.globalAlpha = 1; 366 | this.ctx.globalCompositeOperation = 'source-over'; 367 | 368 | } 369 | 370 | window.Graph = Graph; 371 | 372 | })(); 373 | -------------------------------------------------------------------------------- /graph/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Graph 6 | 31 | 32 | 33 |
34 |
35 |
36 |
37 |
38 |
39 | 40 | 41 | 111 | 112 | -------------------------------------------------------------------------------- /libs/graph.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | "use strict"; 4 | 5 | function throttle(fn, threshold, scope) { 6 | threshold || (threshold = 250); 7 | var last, 8 | deferTimer; 9 | return function () { 10 | var context = scope || this; 11 | 12 | var now = +new Date, 13 | args = arguments; 14 | if (last && now < last + threshold) { 15 | clearTimeout(deferTimer); 16 | deferTimer = setTimeout(function () { 17 | last = now; 18 | fn.apply(context, args); 19 | }, threshold); 20 | } else { 21 | last = now; 22 | fn.apply(context, args); 23 | } 24 | }; 25 | } 26 | 27 | function debounce(fn, delay) { 28 | var timer = null; 29 | return function () { 30 | var context = this, args = arguments; 31 | clearTimeout(timer); 32 | timer = setTimeout(function () { 33 | fn.apply(context, args); 34 | }, delay); 35 | }; 36 | } 37 | 38 | function Graph( properties ) { 39 | 40 | this.properties = properties; 41 | 42 | this.properties.baselines = this.properties.baselines || []; 43 | this.decorator = this.properties.decorator || ( v => v ); 44 | 45 | this.data = []; 46 | this.start = 0; 47 | this.end = 0; 48 | this.paddingTop = 2; 49 | this.pivot = 0; 50 | this.lastPoint = 0; 51 | 52 | this.max = 0; 53 | this.min = Number.MAX_VALUE; 54 | 55 | this.canvas = document.createElement( 'canvas' ); 56 | this.canvas.style.width = '100%'; 57 | this.canvas.style.height = '100%'; 58 | this.canvas.style.position = 'absolute'; 59 | this.canvas.style.left = 0; 60 | this.canvas.style.top = 0; 61 | 62 | this.ctx = this.canvas.getContext( '2d' ); 63 | this.dpr = window.devicePixelRatio; 64 | this.properties.target.appendChild( this.canvas ); 65 | 66 | this.title = document.createElement( 'h1' ); 67 | this.title.textContent = this.properties.title; 68 | this.properties.target.appendChild( this.title ); 69 | 70 | this.label = document.createElement( 'div' ); 71 | this.label.className = 'label hidden'; 72 | this.properties.target.appendChild( this.label ); 73 | 74 | this.overlayCanvas = document.createElement( 'canvas' ); 75 | this.overlayCanvas.style.width = '100%'; 76 | this.overlayCanvas.style.height = '100%'; 77 | this.overlayCanvas.style.position = 'absolute'; 78 | this.overlayCanvas.style.left = 0; 79 | this.overlayCanvas.style.top = 0; 80 | this.overlayCanvas.style.pointerEvents = 'none'; 81 | 82 | this.overlayCtx = this.overlayCanvas.getContext( '2d' ); 83 | this.properties.target.appendChild( this.overlayCanvas ); 84 | 85 | this.linkIn = this.showLabel; 86 | this.linkOut = this.showLabel; 87 | this.linkOver = this.updatePoint; 88 | this.linkZoom = this.updateZoom; 89 | 90 | this.resize(); 91 | 92 | this.zoom = 1; 93 | 94 | var debouncedResize = debounce( this.resize.bind( this ), 100 ); 95 | window.addEventListener( 'resize', function( e ){ 96 | debouncedResize(); 97 | }.bind( this ) ); 98 | 99 | this.canvas.addEventListener( 'mouseover', e => { 100 | 101 | this.linkIn( e.pageX ); 102 | this.linkOver( e.pageX ); 103 | 104 | } ); 105 | 106 | this.canvas.addEventListener( 'mouseout', e => { 107 | 108 | this.linkOut(); 109 | 110 | } ); 111 | 112 | this.canvas.addEventListener( 'mousemove', e => { 113 | 114 | this.linkOver( e.pageX ); 115 | 116 | }) 117 | 118 | var debouncedLinkZoom = throttle( z => this.linkZoom( z ), 20 ); 119 | 120 | this.canvas.addEventListener( 'wheel', e => { 121 | 122 | debouncedLinkZoom( this.zoom + ( .005 * e.deltaY ) ); 123 | e.preventDefault(); 124 | 125 | } ); 126 | 127 | } 128 | 129 | Graph.link = function( graphs ) { 130 | 131 | graphs.forEach( g => { 132 | g.linkIn = x => { graphs.forEach( g => g.showLabel( true ) ); }; 133 | g.linkOut = x => { graphs.forEach( g => g.showLabel( false ) ); }; 134 | g.linkOver = x => { graphs.forEach( g => g.updatePoint( x ) ); }; 135 | g.linkZoom = z => { graphs.forEach( g => g.updateZoom( z ) ); }; 136 | } ) 137 | 138 | } 139 | 140 | Graph.prototype.updateZoom = function( zoom ) { 141 | 142 | this.zoom = zoom; 143 | if( this.zoom > 1 ) this.zoom = 1; 144 | this.update(); 145 | 146 | } 147 | 148 | Graph.prototype.showLabel = function( show ) { 149 | 150 | if( show ) { 151 | this.label.classList.remove( 'hidden' ); 152 | } else { 153 | this.label.classList.add( 'hidden' ); 154 | this.overlayCtx.clearRect( 0, 0, this.overlayCanvas.width, this.overlayCanvas.height ); 155 | } 156 | 157 | } 158 | 159 | Graph.prototype.updatePoint = function( x ) { 160 | 161 | if( x === undefined ) x = this.lastPoint; 162 | this.lastPoint = x; 163 | 164 | if( this.data.length === 0 ) return; 165 | 166 | var res = this.updateLabelPosition( x ); 167 | var pos = res.x * ( this.end - this.start ) / res.width + this.start; 168 | this.pivot = res.x / res.width; 169 | var y = ( this.data.find( v => v.x >= pos ) ).y; 170 | this.label.textContent = this.decorator( y ); 171 | 172 | var x = res.x * this.dpr; 173 | var y = this.paddingTop + this.adjustY( y ); 174 | 175 | this.overlayCtx.clearRect( 0, 0, this.overlayCanvas.width, this.overlayCanvas.height ); 176 | this.overlayCtx.globalCompositeOperation = 'color-burn'; 177 | this.overlayCtx.strokeStyle = '#000000' 178 | this.overlayCtx.globalAlpha = .5; 179 | this.overlayCtx.lineWidth = 2; 180 | 181 | this.overlayCtx.setLineDash( [] ) 182 | this.overlayCtx.beginPath(); 183 | this.overlayCtx.arc( x, y, 4, 0, 2 * Math.PI ); 184 | this.overlayCtx.stroke(); 185 | 186 | this.overlayCtx.setLineDash( [ 2, 4 ] ) 187 | this.overlayCtx.beginPath(); 188 | this.overlayCtx.moveTo( x, this.overlayCanvas.height ); 189 | this.overlayCtx.lineTo( x, y + 3 ); 190 | this.overlayCtx.stroke(); 191 | 192 | } 193 | 194 | Graph.prototype.updateLabelPosition = function( x ) { 195 | 196 | var divRect = this.canvas.getBoundingClientRect(); 197 | var canvasRect = this.canvas.getBoundingClientRect(); 198 | var x = x - canvasRect.left; 199 | if( x < .5 * this.canvas.clientWidth ) { 200 | this.label.classList.remove( 'flip' ); 201 | this.label.style.transform = `translate3d(${x}px,0,0)`; 202 | this.title.classList.add( 'flip' ); 203 | } else { 204 | this.label.classList.add( 'flip' ); 205 | this.label.style.transform = `translate3d(${-(this.canvas.clientWidth-x)}px,0,0)`; 206 | this.title.classList.remove( 'flip' ); 207 | } 208 | return { x: x, width: canvasRect.width }; 209 | 210 | } 211 | 212 | Graph.prototype.resize = function() { 213 | 214 | this.canvas.width = this.properties.target.clientWidth * this.dpr; 215 | this.canvas.height = this.properties.target.clientHeight * this.dpr; 216 | 217 | this.overlayCanvas.width = this.canvas.width; 218 | this.overlayCanvas.height = this.canvas.height; 219 | 220 | this.update(); 221 | 222 | } 223 | 224 | Graph.prototype.set = function( data ) { 225 | 226 | this.data = data; 227 | this.update(); 228 | 229 | } 230 | 231 | Graph.prototype.update = function() { 232 | 233 | if( this.data.length === 0 ) return; 234 | 235 | if( this.zoom > 1 ) this.zoom = 1; 236 | if( this.zoom < .1 ) this.zoom = .1; 237 | 238 | var first = this.data[ 0 ].x; 239 | var last = this.data[ this.data.length - 1 ].x; 240 | var w = last - first; 241 | this.start = first + this.pivot * w - this.pivot * this.zoom * w; 242 | this.end = first + ( this.pivot + ( 1 - this.pivot ) * this.zoom ) * w; 243 | console.log( this.zoom,this.start, this.end ); 244 | 245 | this.max = 0; 246 | this.min = Number.MAX_VALUE; 247 | 248 | this.data.forEach( ( v, i ) => { 249 | if( v.x >= this.start && v.x <= this.end ) { 250 | if( v.y < this.min ) this.min = v.y; 251 | if( v.y > this.max ) this.max = v.y; 252 | } 253 | } ); 254 | 255 | if( this.properties.baselines.length && 256 | this.properties.baselines[ 0 ] > this.max ) { 257 | this.max = this.properties.baselines[ 0 ]; 258 | } 259 | 260 | this.refresh(); 261 | this.updatePoint(); 262 | 263 | } 264 | 265 | function createAdjustFunction( min, max, size ) { 266 | 267 | return function adjust( v ) { 268 | 269 | return ( v - min ) * size / ( max - min ); 270 | 271 | } 272 | 273 | } 274 | 275 | Graph.prototype.refresh = function() { 276 | 277 | if( !this.data.length ) return; 278 | 279 | this.adjustX = createAdjustFunction( this.start, this.end, this.canvas.width ); 280 | this.adjustY = createAdjustFunction( this.max, 0, this.canvas.height - this.paddingTop ); 281 | 282 | this.ctx.fillStyle = '#efefef' 283 | this.ctx.fillRect( 0, 0, this.canvas.width, this.canvas.height ); 284 | 285 | var path = new Path2D(); 286 | 287 | var ovx = 0; 288 | var ovy = 0; 289 | var acc = 0; 290 | var samples = 0; 291 | 292 | this.data.forEach( ( v, i ) => { 293 | 294 | var vx = ~~( this.adjustX( v.x ) ); 295 | var vy = this.adjustY( v.y ); 296 | 297 | if( i === 0 ) { 298 | path.moveTo( vx, vy ); 299 | ovx = vx; 300 | ovy = vy; 301 | } else { 302 | acc += vy; 303 | samples++; 304 | //if( vx > ovx ) { 305 | vy = this.paddingTop + acc / samples; 306 | acc = 0; 307 | samples = 0; 308 | var cpx = ovx + ( vx - ovx ) * .5; 309 | var cpy1 = ( vy < ovy ) ? vy : ovy; 310 | var cpy2 = ( vy < ovy ) ? ovy : vy; 311 | //path.lineTo( this.adjustX( ~~v.x ), this.adjustY( v.y ) ); 312 | path.bezierCurveTo( cpx, cpy1, cpx, cpy2, vx, vy ); 313 | ovx = vx; 314 | ovy = vy; 315 | //} 316 | } 317 | 318 | } ); 319 | 320 | var path2 = new Path2D( path ); 321 | path2.lineTo( this.canvas.width, this.canvas.height ); 322 | path2.lineTo( 0, this.canvas.height ); 323 | 324 | this.ctx.fillStyle = this.properties.color; 325 | this.ctx.fill( path2 ); 326 | 327 | this.ctx.translate( 0, 2 ); 328 | this.ctx.lineWidth = 1.5; 329 | this.ctx.globalCompositeOperation = 'color-burn'; 330 | this.ctx.strokeStyle = '#000000' 331 | this.ctx.globalAlpha = .1; 332 | this.ctx.stroke( path ); 333 | 334 | this.ctx.translate( 0, -2 ); 335 | 336 | if( this.properties.baselines.length ) { 337 | 338 | this.ctx.beginPath(); 339 | this.ctx.lineWidth = 1; 340 | this.ctx.globalAlpha = .25; 341 | this.properties.baselines.forEach( baseline => { 342 | var y = this.paddingTop + this.adjustY( baseline ); 343 | this.ctx.moveTo( 0, y ); 344 | this.ctx.lineTo( this.canvas.width, y ); 345 | } ); 346 | this.ctx.stroke(); 347 | 348 | } 349 | 350 | if( this.properties.baseline_range ) { 351 | 352 | this.ctx.beginPath(); 353 | this.ctx.lineWidth = 1; 354 | this.ctx.globalAlpha = .25; 355 | var steps = ~~( this.max / this.properties.baseline_range ); 356 | for( var j = 0; j < steps; j++ ) { 357 | var y = this.paddingTop + this.adjustY( j * this.properties.baseline_range ); 358 | this.ctx.moveTo( 0, y ); 359 | this.ctx.lineTo( this.canvas.width, y ); 360 | } 361 | this.ctx.stroke(); 362 | 363 | } 364 | 365 | this.ctx.globalAlpha = 1; 366 | this.ctx.globalCompositeOperation = 'source-over'; 367 | 368 | } 369 | 370 | window.Graph = Graph; 371 | 372 | })(); 373 | -------------------------------------------------------------------------------- /libs/metricsgraphics.css: -------------------------------------------------------------------------------- 1 | .mg-active-datapoint { 2 | fill: black; 3 | font-size: 0.9rem; 4 | font-weight: 400; 5 | opacity: 0.8; 6 | } 7 | 8 | .mg-area1-color { 9 | fill: #0000ff; 10 | } 11 | 12 | .mg-area2-color { 13 | fill: #05b378; 14 | } 15 | 16 | .mg-area3-color { 17 | fill: #db4437; 18 | } 19 | 20 | .mg-area4-color { 21 | fill: #f8b128; 22 | } 23 | 24 | .mg-area5-color { 25 | fill: #5c5c5c; 26 | } 27 | 28 | text.mg-barplot-group-label { 29 | font-weight:900; 30 | } 31 | 32 | .mg-barplot rect.mg-bar { 33 | shape-rendering: auto; 34 | } 35 | 36 | .mg-barplot rect.mg-bar.default-bar { 37 | fill: #b6b6fc; 38 | } 39 | 40 | .mg-barplot rect.mg-bar.default-active { 41 | fill: #9e9efc; 42 | } 43 | 44 | .mg-barplot .mg-bar-prediction { 45 | fill: #5b5b5b; 46 | } 47 | 48 | .mg-barplot .mg-bar-baseline { 49 | stroke: #5b5b5b; 50 | stroke-width: 2; 51 | } 52 | 53 | .mg-bar-target-element { 54 | font-size:11px; 55 | padding-left:5px; 56 | padding-right:5px; 57 | font-weight:300; 58 | } 59 | 60 | .mg-baselines line { 61 | opacity: 1; 62 | shape-rendering: auto; 63 | stroke: #b3b2b2; 64 | stroke-width: 1px; 65 | } 66 | 67 | .mg-baselines text { 68 | fill: black; 69 | font-size: 0.9rem; 70 | opacity: 0.6; 71 | stroke: none; 72 | } 73 | 74 | .mg-baselines-small text { 75 | font-size: 0.6rem; 76 | } 77 | 78 | .mg-category-guides line { 79 | stroke: #b3b2b2; 80 | } 81 | 82 | .mg-header { 83 | cursor: default; 84 | font-size: 1.2rem; 85 | } 86 | 87 | .mg-header .mg-chart-description { 88 | fill: #ccc; 89 | font-family: FontAwesome; 90 | font-size: 1.2rem; 91 | } 92 | 93 | .mg-header .mg-warning { 94 | fill: #ccc; 95 | font-family: FontAwesome; 96 | font-size: 1.2rem; 97 | } 98 | 99 | .mg-points circle { 100 | opacity: 0.65; 101 | } 102 | 103 | .mg-popover { 104 | font-size: 0.95rem; 105 | } 106 | 107 | .mg-popover-content { 108 | cursor: auto; 109 | line-height: 17px; 110 | } 111 | 112 | .mg-data-table { 113 | margin-top: 30px; 114 | } 115 | 116 | .mg-data-table thead tr th { 117 | border-bottom: 1px solid darkgray; 118 | cursor: default; 119 | font-size: 1.1rem; 120 | font-weight: normal; 121 | padding: 5px 5px 8px 5px; 122 | text-align: right; 123 | } 124 | 125 | .mg-data-table thead tr th .fa { 126 | color: #ccc; 127 | padding-left: 4px; 128 | } 129 | 130 | .mg-data-table thead tr th .popover { 131 | font-size: 1rem; 132 | font-weight: normal; 133 | } 134 | 135 | .mg-data-table .secondary-title { 136 | color: darkgray; 137 | } 138 | 139 | .mg-data-table tbody tr td { 140 | margin: 2px; 141 | padding: 5px; 142 | vertical-align: top; 143 | } 144 | 145 | .mg-data-table tbody tr td.table-text { 146 | opacity: 0.8; 147 | padding-left: 30px; 148 | } 149 | 150 | .mg-y-axis line.mg-extended-yax-ticks { 151 | opacity: 0.4; 152 | } 153 | 154 | .mg-x-axis line.mg-extended-xax-ticks { 155 | opacity: 0.4; 156 | } 157 | 158 | .mg-histogram .axis path, 159 | .mg-histogram .axis line { 160 | fill: none; 161 | opacity: 0.7; 162 | shape-rendering: auto; 163 | stroke: #ccc; 164 | } 165 | 166 | tspan.hist-symbol { 167 | fill: #9e9efc; 168 | } 169 | 170 | .mg-histogram .mg-bar rect { 171 | fill: #b6b6fc; 172 | shape-rendering: auto; 173 | } 174 | 175 | .mg-histogram .mg-bar rect.active { 176 | fill: #9e9efc; 177 | } 178 | 179 | .mg-least-squares-line { 180 | stroke: red; 181 | stroke-width: 1px; 182 | } 183 | 184 | .mg-lowess-line { 185 | fill: none; 186 | stroke: red; 187 | } 188 | 189 | .mg-line1-color { 190 | stroke: #4040e8; 191 | } 192 | 193 | .mg-hover-line1-color { 194 | fill: #4040e8; 195 | } 196 | 197 | .mg-line2-color { 198 | stroke: #05b378; 199 | } 200 | 201 | .mg-hover-line2-color { 202 | fill: #05b378; 203 | } 204 | 205 | .mg-line3-color { 206 | stroke: #db4437; 207 | } 208 | 209 | .mg-hover-line3-color { 210 | fill: #db4437; 211 | } 212 | 213 | .mg-line4-color { 214 | stroke: #f8b128; 215 | } 216 | 217 | .mg-hover-line4-color { 218 | fill: #f8b128; 219 | } 220 | 221 | .mg-line5-color { 222 | stroke: #5c5c5c; 223 | } 224 | 225 | .mg-hover-line5-color { 226 | fill: #5c5c5c; 227 | } 228 | 229 | .mg-line-legend text { 230 | font-size: 0.9rem; 231 | font-weight: 300; 232 | stroke: none; 233 | } 234 | 235 | .mg-line1-legend-color { 236 | color: #4040e8; 237 | fill: #4040e8; 238 | } 239 | 240 | .mg-line2-legend-color { 241 | color: #05b378; 242 | fill: #05b378; 243 | } 244 | 245 | .mg-line3-legend-color { 246 | color: #db4437; 247 | fill: #db4437; 248 | } 249 | 250 | .mg-line4-legend-color { 251 | color: #f8b128; 252 | fill: #f8b128; 253 | } 254 | 255 | .mg-line5-legend-color { 256 | color: #5c5c5c; 257 | fill: #5c5c5c; 258 | } 259 | 260 | .mg-main-area-solid svg .mg-main-area { 261 | fill: #ccccff; 262 | opacity: 1; 263 | } 264 | 265 | .mg-markers line { 266 | opacity: 1; 267 | shape-rendering: auto; 268 | stroke: #b3b2b2; 269 | stroke-width: 1px; 270 | } 271 | 272 | .mg-markers text { 273 | fill: black; 274 | font-size: 0.8rem; 275 | opacity: 0.6; 276 | } 277 | 278 | .mg-missing-text { 279 | opacity: 0.9; 280 | } 281 | 282 | .mg-missing-background { 283 | stroke: blue; 284 | fill: none; 285 | stroke-dasharray: 10,5; 286 | stroke-opacity: 0.05; 287 | stroke-width: 2; 288 | } 289 | 290 | .mg-missing .mg-main-line { 291 | opacity: 0.1; 292 | } 293 | 294 | .mg-missing .mg-main-area { 295 | opacity: 0.03; 296 | } 297 | 298 | path.mg-main-area { 299 | opacity: 0.2; 300 | stroke: none; 301 | } 302 | 303 | path.mg-confidence-band { 304 | fill: #ccc; 305 | opacity: 0.4; 306 | stroke: none; 307 | } 308 | 309 | path.mg-main-line { 310 | fill: none; 311 | opacity: 0.8; 312 | stroke-width: 1.1px; 313 | } 314 | 315 | .mg-points circle { 316 | fill-opacity: 0.4; 317 | stroke-opacity: 1; 318 | } 319 | 320 | circle.mg-points-mono { 321 | fill: #0000ff; 322 | stroke: #0000ff; 323 | } 324 | 325 | tspan.mg-points-mono { 326 | fill: #0000ff; 327 | stroke: #0000ff; 328 | } 329 | 330 | /* a selected point in a scatterplot */ 331 | .mg-points circle.selected { 332 | fill-opacity: 1; 333 | stroke-opacity: 1; 334 | } 335 | 336 | .mg-voronoi path { 337 | fill: none; 338 | pointer-events: all; 339 | stroke: none; 340 | stroke-opacity: 0.1; 341 | } 342 | 343 | .mg-x-rug-mono, 344 | .mg-y-rug-mono { 345 | stroke: black; 346 | } 347 | 348 | .mg-x-axis line, 349 | .mg-y-axis line { 350 | opacity: 1; 351 | shape-rendering: auto; 352 | stroke: #b3b2b2; 353 | stroke-width: 1px; 354 | } 355 | 356 | .mg-x-axis text, 357 | .mg-y-axis text, 358 | .mg-histogram .axis text { 359 | fill: black; 360 | font-size: 0.9rem; 361 | opacity: 0.6; 362 | } 363 | 364 | .mg-x-axis .label, 365 | .mg-y-axis .label, 366 | .mg-axis .label { 367 | font-size: 0.8rem; 368 | text-transform: uppercase; 369 | font-weight: 400; 370 | } 371 | 372 | .mg-x-axis-small text, 373 | .mg-y-axis-small text, 374 | .mg-active-datapoint-small { 375 | font-size: 0.6rem; 376 | } 377 | 378 | .mg-x-axis-small .label, 379 | .mg-y-axis-small .label { 380 | font-size: 0.65rem; 381 | } 382 | 383 | .mg-european-hours { 384 | } 385 | 386 | .mg-year-marker text { 387 | fill: black; 388 | font-size: 0.7rem; 389 | opacity: 0.6; 390 | } 391 | 392 | .mg-year-marker line { 393 | opacity: 1; 394 | shape-rendering: auto; 395 | stroke: #b3b2b2; 396 | stroke-width: 1px; 397 | } 398 | 399 | .mg-year-marker-small text { 400 | font-size: 0.6rem; 401 | } 402 | -------------------------------------------------------------------------------- /libs/mg_line_brushing.css: -------------------------------------------------------------------------------- 1 | .mg-brush-container { 2 | cursor: crosshair; } 3 | 4 | .mg-brush-container.mg-brushing { 5 | cursor: ew-resize; } 6 | 7 | .mg-brushed, .mg-brushed * { 8 | cursor: zoom-out !important; } 9 | 10 | .mg-brush rect.mg-extent { 11 | fill: rgba(0, 0, 0, 0.3); } 12 | 13 | .mg-brushing-in-progress { 14 | -webkit-touch-callout: none; 15 | -webkit-user-select: none; 16 | -khtml-user-select: none; 17 | -moz-user-select: none; 18 | -ms-user-select: none; 19 | user-select: none; } 20 | -------------------------------------------------------------------------------- /libs/mg_line_brushing.js: -------------------------------------------------------------------------------- 1 | (function(root, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | define(['d3', 'jquery', 'MG'], factory); 4 | } else if (typeof exports === 'object') { 5 | module.exports = factory(require('d3'), require('jquery'), require('MG')); 6 | } else { 7 | root.Mg_line_brushing = factory(root.d3, root.jQuery, root.MG); 8 | } 9 | }(this, function(d3, $, MG) { 10 | /** 11 | 2. accessors 12 | */ 13 | 14 | MG.line_brushing = { 15 | set_brush_as_base: function(target) { 16 | var svg = d3.select(target).select('svg'), 17 | current, 18 | history = brushHistory[target]; 19 | 20 | svg.classed('mg-brushed', false); 21 | 22 | if (history) { 23 | history.brushed = false; 24 | 25 | current = history.current; 26 | history.original = current; 27 | 28 | args.min_x = current.min_x; 29 | args.max_x = current.max_x; 30 | args.min_y = current.min_y; 31 | args.max_y = current.max_y; 32 | 33 | history.steps = []; 34 | } 35 | }, 36 | 37 | zoom_in: function(target, options) { 38 | 39 | }, 40 | 41 | zoom_out: function(target, options) { 42 | 43 | } 44 | }; 45 | 46 | /* helpers */ 47 | function get_brush_interval(args) { 48 | var resolution = args.brushing_interval, 49 | interval; 50 | 51 | if (!resolution) { 52 | if (args.time_series) { 53 | resolution = d3.timeDay; 54 | } else { 55 | resolution = 1; 56 | } 57 | } 58 | 59 | // work with N as integer 60 | if (typeof resolution === 'number') { 61 | interval = { 62 | round: function(val) { 63 | return resolution * Math.round(val / resolution); 64 | }, 65 | offset: function(val, count) { 66 | return val + (resolution * count); 67 | } 68 | }; 69 | } 70 | // work with d3.time.[interval] 71 | else if (typeof resolution.round === 'function' 72 | && typeof resolution.offset === 'function' ) { 73 | interval = resolution; 74 | } 75 | else { 76 | console.warn('The `brushing_interval` provided is invalid. It must be either a number or expose both `round` and `offset` methods'); 77 | } 78 | 79 | return interval; 80 | } 81 | 82 | function is_within_bounds(datum, args) { 83 | var x = +datum[args.x_accessor], 84 | y = +datum[args.y_accessor]; 85 | 86 | return x >= (+args.processed.min_x || x) 87 | && x <= (+args.processed.max_x || x) 88 | && y >= (+args.processed.min_y || y) 89 | && y <= (+args.processed.max_y || y); 90 | } 91 | 92 | 93 | /** 94 | Brushing for line charts 95 | 96 | 1. hooks 97 | */ 98 | 99 | var brushHistory = {}, 100 | args; 101 | 102 | MG.add_hook('global.defaults', function(args) { 103 | // enable brushing unless it's explicitly disabled 104 | args.brushing = args.brushing !== false; 105 | if (args.brushing) { 106 | args.brushing_history = args.brushing_history !== false; 107 | args.aggregate_rollover = true; 108 | } 109 | }); 110 | 111 | function brushing() { 112 | var chartContext = this; 113 | 114 | args = this.args; 115 | 116 | if (args.brushing === false) { 117 | return this; 118 | } 119 | 120 | if (!brushHistory[args.target] || !brushHistory[args.target].brushed) { 121 | brushHistory[args.target] = { 122 | brushed: false, 123 | steps: [], 124 | original: { 125 | min_x: +args.processed.min_x, 126 | max_x: +args.processed.max_x, 127 | min_y: +args.processed.min_y, 128 | max_y: +args.processed.max_y 129 | } 130 | }; 131 | } 132 | 133 | var isDragging = false, 134 | mouseDown = false, 135 | originX, 136 | svg = d3.select(args.target).select('svg'), 137 | body = d3.select('body'), 138 | rollover = svg.select('.mg-rollover-rect, .mg-voronoi'), 139 | brushingGroup, 140 | extentRect; 141 | 142 | rollover.classed('mg-brush-container', true); 143 | 144 | brushingGroup = rollover.insert('g', '*') 145 | .classed('mg-brush', true); 146 | 147 | extentRect = brushingGroup.append('rect') 148 | .attr('opacity', 0) 149 | .attr('y', args.top) 150 | .attr('height', args.height - args.bottom - args.top - args.buffer) 151 | .classed('mg-extent', true); 152 | 153 | // mousedown, start area selection 154 | svg.on('mousedown', function() { 155 | mouseDown = true; 156 | isDragging = false; 157 | originX = d3.mouse(this)[0]; 158 | svg.classed('mg-brushed', false); 159 | svg.classed('mg-brushing-in-progress', true); 160 | extentRect.attr({ 161 | x: d3.mouse(this)[0], 162 | opacity: 0, 163 | width: 0 164 | }); 165 | }); 166 | 167 | // mousemove / drag, expand area selection 168 | svg.on('mousemove', function() { 169 | if (mouseDown) { 170 | isDragging = true; 171 | rollover.classed('mg-brushing', true); 172 | 173 | var mouseX = d3.mouse(this)[0], 174 | newX = Math.min(originX, mouseX), 175 | width = Math.max(originX, mouseX) - newX; 176 | 177 | extentRect 178 | .attr('x', newX) 179 | .attr('width', width) 180 | .attr('opacity', 1); 181 | } 182 | }); 183 | 184 | // mouseup, finish area selection 185 | svg.on('mouseup', function() { 186 | mouseDown = false; 187 | svg.classed('mg-brushing-in-progress', false); 188 | 189 | var xScale = args.scales.X, 190 | yScale = args.scales.Y, 191 | flatData = [].concat.apply([], args.data), 192 | boundedData, 193 | yBounds, 194 | xBounds, 195 | extentX0 = +extentRect.attr('x'), 196 | extentX1 = extentX0 + (+extentRect.attr('width')), 197 | interval = get_brush_interval(args), 198 | offset = 0, 199 | mapDtoX = function(d) { return +d[args.x_accessor]; }, 200 | mapDtoY = function(d) { return +d[args.y_accessor]; }; 201 | 202 | // if we're zooming in: calculate the domain for x and y axes based on the selected rect 203 | if (isDragging) { 204 | isDragging = false; 205 | 206 | if (brushHistory[args.target].brushed) { 207 | brushHistory[args.target].steps.push({ 208 | max_x: args.brushed_max_x || args.max_x, 209 | min_x: args.brushed_min_x || args.min_x, 210 | max_y: args.brushed_max_y || args.max_y, 211 | min_y: args.brushed_min_y || args.min_y 212 | }); 213 | } 214 | 215 | brushHistory[args.target].brushed = true; 216 | 217 | boundedData = []; 218 | // is there at least one data point in the chosen selection? if not, increase the range until there is. 219 | var iterations = 0; 220 | while (boundedData.length === 0 && iterations <= flatData.length) { 221 | 222 | var xValX0 = xScale.invert(extentX0); 223 | var xValX1 = xScale.invert(extentX1); 224 | xValX0 = xValX0 instanceof Date ? xValX0 : interval.round(xValX0); 225 | xValX1 = xValX1 instanceof Date ? xValX1 : interval.round(xValX1); 226 | 227 | args.brushed_min_x = xValX0; 228 | args.brushed_max_x = Math.max(interval.offset(args.min_x, 1), xValX1); 229 | 230 | boundedData = flatData.filter(function(d) { 231 | var val = d[args.x_accessor]; 232 | return val >= args.brushed_min_x && val <= args.brushed_max_x; 233 | }); 234 | 235 | iterations++; 236 | } 237 | 238 | xBounds = d3.extent(boundedData, mapDtoX); 239 | args.brushed_min_x = +xBounds[0]; 240 | args.brushed_max_x = +xBounds[1]; 241 | xScale.domain(xBounds); 242 | 243 | yBounds = d3.extent(boundedData, mapDtoY); 244 | // add 10% padding on the y axis for better display 245 | // @TODO: make this an option 246 | args.brushed_min_y = yBounds[0] * 0.9; 247 | args.brushed_max_y = yBounds[1] * 1.1; 248 | yScale.domain(yBounds); 249 | } 250 | // zooming out on click, maintaining the step history 251 | else if (args.brushing_history) { 252 | if (brushHistory[args.target].brushed) { 253 | var previousBrush = brushHistory[args.target].steps.pop(); 254 | if (previousBrush) { 255 | args.brushed_max_x = previousBrush.max_x; 256 | args.brushed_min_x = previousBrush.min_x; 257 | args.brushed_max_y = previousBrush.max_y; 258 | args.brushed_min_y = previousBrush.min_y; 259 | 260 | xBounds = [args.brushed_min_x, args.brushed_max_x]; 261 | yBounds = [args.brushed_min_y, args.brushed_max_y]; 262 | xScale.domain(xBounds); 263 | yScale.domain(yBounds); 264 | } else { 265 | brushHistory[args.target].brushed = false; 266 | 267 | delete args.brushed_max_x; 268 | delete args.brushed_min_x; 269 | delete args.brushed_max_y; 270 | delete args.brushed_min_y; 271 | 272 | xBounds = [ 273 | brushHistory[args.target].original.min_x, 274 | brushHistory[args.target].original.max_x 275 | ]; 276 | 277 | yBounds = [ 278 | brushHistory[args.target].original.min_y, 279 | brushHistory[args.target].original.max_y 280 | ]; 281 | } 282 | } 283 | } 284 | 285 | // has anything changed? 286 | if (xBounds && yBounds) { 287 | if (xBounds[0] < xBounds[1]) { 288 | // trigger the brushing callback 289 | 290 | var step = { 291 | min_x: xBounds[0], 292 | max_x: xBounds[1], 293 | min_y: yBounds[0], 294 | max_y: yBounds[1] 295 | }; 296 | 297 | brushHistory[args.target].current = step; 298 | 299 | if (args.after_brushing) { 300 | args.after_brushing.apply(this, [step]); 301 | } 302 | } 303 | 304 | // redraw the chart 305 | if (!args.brushing_manual_redraw) { 306 | MG.data_graphic(args); 307 | } 308 | } 309 | }); 310 | 311 | return this; 312 | } 313 | 314 | MG.add_hook('line.after_init', function(lineChart) { 315 | brushing.apply(lineChart); 316 | }); 317 | 318 | function processXAxis(args, min_x, max_x) { 319 | if (args.brushing) { 320 | args.processed.min_x = args.brushed_min_x ? Math.max(args.brushed_min_x, min_x) : min_x; 321 | args.processed.max_x = args.brushed_max_x ? Math.min(args.brushed_max_x, max_x) : max_x; 322 | } 323 | } 324 | 325 | MG.add_hook('x_axis.process_min_max', processXAxis); 326 | 327 | function processYAxis(args) { 328 | if (args.brushing && (args.brushed_min_y || args.brushed_max_y)) { 329 | args.processed.min_y = args.brushed_min_y; 330 | args.processed.max_y = args.brushed_max_y; 331 | } 332 | } 333 | 334 | MG.add_hook('y_axis.process_min_max', processYAxis); 335 | 336 | function afterRollover(args) { 337 | if (args.brushing_history && brushHistory[args.target] && brushHistory[args.target].brushed) { 338 | var svg = d3.select(args.target).select('svg'); 339 | svg.classed('mg-brushed', true); 340 | } 341 | } 342 | 343 | MG.add_hook('line.after_rollover', afterRollover); 344 | 345 | return ; 346 | })); 347 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PerfMeter", 3 | "description": "PerfMeter", 4 | "version": "0.1", 5 | "background": { 6 | "scripts": [ "common.js", "background.js"], 7 | "persistent": true 8 | }, 9 | "icons": { 10 | "16": "assets/icon.svg", 11 | "48": "assets/icon.svg", 12 | "128": "assets/icon.svg" 13 | }, 14 | "content_scripts": [{ 15 | "matches": [""], 16 | "js": ["content-script.js"], 17 | "run_at": "document_start", 18 | "all_frames": true 19 | } ], 20 | "permissions": [ 21 | "", 22 | "webNavigation", 23 | "tabs", 24 | "storage", 25 | "activeTab", 26 | "system.cpu", 27 | "system.display", 28 | "system.memory", 29 | "webRequest" 30 | ], 31 | "web_accessible_resources": [ 32 | "canvas-instrument.js", 33 | "css/styles.css", 34 | "css/Roboto_Mono/RobotoMono-Regular.ttf", 35 | "css/Roboto_Mono/RobotoMono-Bold.ttf" 36 | ], 37 | "devtools_page": "devtools.html", 38 | "manifest_version": 2, 39 | 40 | "content_security_policy": "script-src 'self' 'unsafe-eval' https://maps.google.com https://maps.googleapis.com https://www.gstatic.com; object-src 'self'" 41 | 42 | } 43 | -------------------------------------------------------------------------------- /panel.css: -------------------------------------------------------------------------------- 1 | *{ margin: 0; padding: 0; box-sizing: border-box; } 2 | body{ padding: 10px; line-height: 2em;} 3 | h1{ font-size: 14px; } 4 | .instrument-status{ display: none; } 5 | #scriptStatus{ background-color: #FFDA9E; padding: 10px; margin: 10px 0; } 6 | button{ padding: 4px; cursor: pointer; vertical-align: top; padding-bottom: 5px;} 7 | button object{ width: 10px; height: 10px; pointer-events: none; margin-right: 4px; line-height: 0; vertical-align: text-bottom; } 8 | input[type=checkbox]:disabled+label { opacity: .5; } 9 | .horizontal-bar{ 10 | display: flex; 11 | flex-wrap: wrap; 12 | justify-content: flex-start; 13 | list-style: none; 14 | } 15 | .horizontal-bar li{ 16 | display: flex; 17 | align-items: flex-start; 18 | } 19 | form.disabled{ 20 | opacity: .5; 21 | pointer-events: none; 22 | } 23 | input[type=radio]{ margin-right: .5em;} 24 | .graphs h2{ margin: 0 0 4px 0; font-size: 11px; line-height: 1em;} 25 | .graphs div{ height: 30px; position: relative; margin: 0 0 12px 0; background-color: #efefef;} 26 | .graphs div canvas{ cursor: crosshair; } 27 | .graphs .label{ 28 | position: absolute; 29 | top: 0; 30 | pointer-events: none; 31 | background-color: transparent; 32 | font-family: inherit; 33 | font-size: 10px; 34 | background-color: rgba( 255, 255, 255, .5 ); 35 | padding: 5px 10px 0 10px; 36 | bottom: 0; 37 | margin: 0; 38 | opacity: 1; 39 | transition: opacity 150ms ease-out; 40 | text-shadow: 0 1px #fff; 41 | } 42 | .graphs .flip{ right: 0; } 43 | .graphs .hidden{ opacity: 0; transition: opacity 350ms ease-out; } 44 | .graphs div h1{ position: absolute; top: 0; padding: 5px; font-size: 11px; line-height: 1em; opacity: .5; pointer-events: none; text-shadow: 0 1px #fff;} 45 | -------------------------------------------------------------------------------- /panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerfMeter Panel 6 | 7 | 8 | 9 |

PerfMeter

10 |
11 |

Tab and frames instrumented.

12 |

Tab instrumented. Frames might not work.

13 |

Tab not instrumented.

14 | 15 |
16 |

Time Series

17 |
    18 |
  • 19 |
  • 20 |
  • 21 |
22 |

23 |
24 |
25 |
26 |
27 |
28 |
29 |

Settings

30 |
31 |

Widget

32 |

33 | 34 | 35 | 36 | 37 |

38 |

Monitoring

39 |

40 |

41 |

42 |

Reporting

43 |

44 |

45 |

46 |

47 |

Extension

48 |

49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /panel.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | ge( 'reload-button' ).addEventListener( 'click', e => { 4 | 5 | reload(); 6 | e.preventDefault(); 7 | 8 | } ); 9 | 10 | ge( 'start-record-data-button' ).addEventListener( 'click', e => { 11 | 12 | startRecordingData(); 13 | e.preventDefault(); 14 | 15 | } ); 16 | 17 | ge( 'stop-record-data-button' ).addEventListener( 'click', e => { 18 | 19 | stopRecordingData(); 20 | e.preventDefault(); 21 | 22 | } ); 23 | 24 | ge( 'autoinstrument' ).addEventListener( 'change', e => { 25 | 26 | window.settings.autoinstrument = e.target.checked; 27 | updateSettings(); 28 | e.preventDefault(); 29 | 30 | } ); 31 | 32 | ge( 'log-calls' ).addEventListener( 'change', e => { 33 | 34 | window.settings.log = e.target.checked; 35 | updateSettings(); 36 | e.preventDefault(); 37 | 38 | } ); 39 | 40 | ge( 'show-gpuinfo' ).addEventListener( 'change', e => { 41 | 42 | window.settings.showGPUInfo = e.target.checked; 43 | updateSettings(); 44 | e.preventDefault(); 45 | 46 | } ); 47 | 48 | ge( 'profile-shaders' ).addEventListener( 'change', e => { 49 | 50 | window.settings.profileShaders = e.target.checked; 51 | updateSettings(); 52 | e.preventDefault(); 53 | 54 | } ); 55 | 56 | function setSettings( settings ) { 57 | 58 | window.settings = settings; 59 | 60 | ge( 'autoinstrument' ).checked = settings.autoinstrument; 61 | ge( 'show-gpuinfo' ).checked = settings.showGPUInfo; 62 | ge( 'log-calls' ).checked = settings.log; 63 | ge( 'profile-shaders' ).checked = settings.profileShaders; 64 | 65 | } 66 | 67 | function updatePanelStatus() { 68 | 69 | updateScriptStatus(); 70 | updateRecordingStatus(); 71 | 72 | } 73 | 74 | function updateRecordingStatus() { 75 | 76 | var recordingStatus = getRecordingStatus(); 77 | if( recordingStatus.status ){ 78 | ge( 'recording-progress' ).textContent = `Recording. ${recordingStatus.bufferSize} samples...`; 79 | ge( 'start-record-data-button' ).style.display = 'none'; 80 | ge( 'stop-record-data-button' ).style.display = 'block'; 81 | ge( 'download-data-button' ).disabled = true; 82 | } else { 83 | ge( 'recording-progress' ).textContent = 'Standing by'; 84 | ge( 'start-record-data-button' ).style.display = 'block'; 85 | ge( 'stop-record-data-button' ).style.display = 'none'; 86 | ge( 'download-data-button' ).disabled = false; 87 | } 88 | 89 | } 90 | 91 | function updateScriptStatus() { 92 | 93 | ge( 'not-instrumented' ).style.display = 'block'; 94 | ge( 'reload-button' ).style.display = 'block'; 95 | 96 | [].forEach.call( document.querySelectorAll( '.instrument-status' ), el => el.style.display = 'none' ); 97 | switch( getScriptStatus() ) { 98 | case 0: ge( 'not-instrumented' ).style.display = 'block'; ge( 'reload-button' ).style.display = 'block'; break; 99 | case 1: ge( 'injected-instrumented' ).style.display = 'block'; ge( 'reload-button' ).style.display = 'none'; break; 100 | case 2: ge( 'executed-instrumented' ).style.display = 'block'; ge( 'reload-button' ).style.display = 'block'; break; 101 | } 102 | 103 | } 104 | 105 | function onScriptMessage( msg ) { 106 | 107 | switch( msg.method ) { 108 | case 'ready': 109 | updateScriptStatus(); 110 | break; 111 | } 112 | 113 | } 114 | 115 | ge( 'download-data-button' ).addEventListener( 'click', e => { 116 | 117 | var blob = new Blob( [ JSON.stringify( recordedData ) ],{ type: 'application/json' } ); 118 | var url = window.URL.createObjectURL( blob ); 119 | var anchor = document.createElement( 'a' ); 120 | anchor.href = url; 121 | anchor.setAttribute( 'download', 'data.json' ); 122 | anchor.className = "download-js-link"; 123 | anchor.innerHTML = "downloading..."; 124 | anchor.style.display = "none"; 125 | document.body.appendChild(anchor); 126 | setTimeout(function() { 127 | anchor.click(); 128 | document.body.removeChild(anchor); 129 | }, 1 ); 130 | 131 | } ); 132 | 133 | var recordedData = null; 134 | 135 | function formatNumber( value, sizes, decimals ) { 136 | if(value == 0) return '0'; 137 | var k = 1000; // or 1024 for binary 138 | var dm = decimals || 2; 139 | var i = Math.floor(Math.log(value) / Math.log(k)); 140 | return parseFloat((value / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; 141 | } 142 | 143 | var timeSizes = ['ns', 'µs', 'ms', 's' ]; 144 | var callSizes = [ '', 'K', 'M', 'G' ]; 145 | // baseline_range: n 146 | // baselines: [ a, b, c... ] 147 | 148 | var g1 = new Graph( { 149 | title: 'Framerate', 150 | target: document.getElementById( 'framerate-div' ), 151 | color: '#d7f0d1', 152 | baselines: [ 30, 60, 90 ], 153 | decorator: v => `${v.toFixed( 2 )} FPS` 154 | } ); 155 | 156 | var g2 = new Graph( { 157 | title: 'GPU time', 158 | target: document.getElementById( 'gpu-div' ), 159 | color: '#f0c457', 160 | baselines: [ 16666666 ], 161 | decorator: v => `${formatNumber(v,timeSizes,2)}` 162 | } ); 163 | 164 | var g3 = new Graph( { 165 | title: 'JavaScript time', 166 | target: document.getElementById( 'js-div' ), 167 | color: '#9b7fe6', 168 | baselines: [ 16 ], 169 | decorator: v => `${formatNumber(v*1000*1000,timeSizes,2)}` 170 | } ); 171 | 172 | var g4 = new Graph( { 173 | title: 'Draw calls', 174 | target: document.getElementById( 'drawcalls-div' ), 175 | color: '#9dc0ed', 176 | baseline_range: 200, 177 | decorator: v => `${formatNumber(v,callSizes,3)}` 178 | } ); 179 | 180 | Graph.link( [ g1, g2, g3, g4 ] ); 181 | 182 | function plotRecording( recordBuffer ) { 183 | 184 | recordedData = recordBuffer; 185 | 186 | if( recordBuffer.length === 0 ) return; 187 | 188 | var points = recordBuffer.map( v => { return{ x: v.timestamp, y: v.framerate } } ); 189 | g1.set( points ); 190 | 191 | var points2 = recordBuffer.map( v => { return{ x: v.timestamp, y: v.disjointTime } } ); 192 | g2.set( points2 ); 193 | 194 | var points3 = recordBuffer.map( v => { return{ x: v.timestamp, y: v.JavaScriptTime } } ); 195 | g3.set( points3 ); 196 | 197 | var points4 = recordBuffer.map( v => { return{ x: v.timestamp, y: v.drawCount } } ); 198 | g4.set( points4 ); 199 | 200 | 201 | } 202 | -------------------------------------------------------------------------------- /script-common.js: -------------------------------------------------------------------------------- 1 | var log = function() { 2 | 3 | window.console.log.apply( 4 | window.console, [ 5 | `%c PerfMeter | ${performance.now().toFixed(2)} `, 6 | 'background: #1E9433; color: #ffffff; text-shadow: 0 -1px #000; padding: 4px 0 4px 0; line-height: 0', 7 | ...arguments 8 | ] 9 | ); 10 | 11 | }; 12 | 13 | var postWithContentScript = function( msg ) { 14 | 15 | var e = new CustomEvent( 'perfmeter-message', { detail: msg } ); 16 | window.dispatchEvent( e ); 17 | 18 | }; 19 | 20 | var messageQueue = []; 21 | var postWithoutContentScript = function( msg ) { 22 | 23 | msg.source = 'perfmeter-script'; 24 | messageQueue.push( msg ); 25 | 26 | }; 27 | 28 | var queryMessageQueue = function() { 29 | 30 | var res = messageQueue.slice(); 31 | messageQueue = []; 32 | return res; 33 | 34 | }; 35 | 36 | var checkCount = 0; 37 | 38 | function checkContentScript() { 39 | 40 | log( 'Checking for content script' ); 41 | 42 | var e = new Event( 'perfmeter-content-script-available' ); 43 | window.dispatchEvent( e ); 44 | 45 | checkCount++; 46 | if( checkCount > 10 ) checkInterval = clearInterval( checkInterval ); 47 | 48 | } 49 | 50 | var checkInterval = setInterval( checkContentScript, 500 ); 51 | 52 | window.addEventListener( 'perfmeter-content-script-available', e => { 53 | 54 | log( 'Content Script Available' ); 55 | checkInterval = clearInterval( checkInterval ); 56 | 57 | } ); 58 | 59 | window.__PerfMeterQueryMessageQueue = queryMessageQueue; 60 | 61 | window.__PerfMeterHasContentScript = function() { 62 | 63 | return window.__PerfMeterContentScript === undefined ? false : true; 64 | 65 | }; 66 | 67 | var post = function( msg ) { 68 | 69 | ( window.__PerfMeterContentScript ? postWithContentScript : postWithoutContentScript )( msg ); 70 | 71 | }; 72 | -------------------------------------------------------------------------------- /src/CanvasRenderingContext2DWrapper.js: -------------------------------------------------------------------------------- 1 | import{ ContextWrapper } from "./ContextWrapper"; 2 | 3 | function CanvasRenderingContext2DWrapper( context ){ 4 | 5 | ContextWrapper.call( this, context ); 6 | 7 | this.frameId = null; 8 | 9 | } 10 | 11 | CanvasRenderingContext2DWrapper.prototype = Object.create( ContextWrapper.prototype ); 12 | 13 | CanvasRenderingContext2DWrapper.prototype.setFrameId = function( frameId ) { 14 | 15 | this.frameId = frameId; 16 | 17 | } 18 | 19 | CanvasRenderingContext2DWrapper.prototype.resetFrame = function(){ 20 | 21 | ContextWrapper.prototype.resetFrame.call( this ); 22 | 23 | } 24 | 25 | Object.keys( CanvasRenderingContext2D.prototype ).forEach( key => { 26 | 27 | if( key !== 'canvas' ){ 28 | 29 | try{ 30 | if( typeof CanvasRenderingContext2D.prototype[ key ] === 'function' ){ 31 | CanvasRenderingContext2DWrapper.prototype[ key ] = function(){ 32 | var args = new Array(arguments.length); 33 | for (var i = 0, l = arguments.length; i < l; i++){ 34 | args[i] = arguments[i]; 35 | } 36 | return this.run( key, args, _ => { 37 | return CanvasRenderingContext2D.prototype[ key ].apply( this.context, args ); 38 | }); 39 | } 40 | } else { 41 | CanvasRenderingContext2DWrapper.prototype[ key ] = CanvasRenderingContext2D.prototype[ key ]; 42 | } 43 | } catch( e ){ 44 | Object.defineProperty( CanvasRenderingContext2DWrapper.prototype, key, { 45 | get: function (){ return this.context[ key ]; }, 46 | set: function ( v ){ this.context[ key ] = v; } 47 | }); 48 | } 49 | 50 | } 51 | 52 | }); 53 | 54 | export { CanvasRenderingContext2DWrapper }; 55 | -------------------------------------------------------------------------------- /src/ContextWrapper.js: -------------------------------------------------------------------------------- 1 | import{ Wrapper } from "./Wrapper"; 2 | 3 | function ContextWrapper( context ) { 4 | 5 | Wrapper.call( this ); 6 | this.context = context; 7 | 8 | this.count = 0; 9 | this.JavaScriptTime = 0; 10 | 11 | this.log = []; 12 | 13 | } 14 | 15 | ContextWrapper.prototype = Object.create( Wrapper.prototype ); 16 | 17 | ContextWrapper.prototype.run = function(fName, fArgs, fn) { 18 | 19 | this.incrementCount(); 20 | this.beginProfile( fName, fArgs ); 21 | const res = fn(); 22 | this.endProfile(); 23 | return res; 24 | 25 | } 26 | 27 | ContextWrapper.prototype.resetFrame = function() { 28 | 29 | this.resetCount(); 30 | this.resetJavaScriptTime(); 31 | this.resetLog(); 32 | 33 | } 34 | 35 | ContextWrapper.prototype.resetCount = function() { 36 | 37 | this.count = 0; 38 | 39 | } 40 | 41 | ContextWrapper.prototype.incrementCount = function() { 42 | 43 | this.count++; 44 | 45 | } 46 | 47 | ContextWrapper.prototype.resetLog = function() { 48 | 49 | this.log.length = 0; 50 | 51 | } 52 | 53 | ContextWrapper.prototype.resetJavaScriptTime = function() { 54 | 55 | this.JavaScriptTime = 0; 56 | 57 | } 58 | 59 | ContextWrapper.prototype.incrementJavaScriptTime = function(time) { 60 | 61 | this.JavaScriptTime += time; 62 | 63 | } 64 | 65 | ContextWrapper.prototype.beginProfile = function(fn, args) { 66 | 67 | const t = performance.now(); 68 | this.log.push( { function: fn, arguments: args, start: t, end: 0 } ); 69 | this.startTime = t; 70 | 71 | } 72 | 73 | ContextWrapper.prototype.endProfile = function() { 74 | 75 | const t = performance.now(); 76 | this.log[ this.log.length - 1 ].end = t; 77 | this.incrementJavaScriptTime( t - this.startTime ); 78 | 79 | } 80 | 81 | 82 | export { ContextWrapper } 83 | -------------------------------------------------------------------------------- /src/WebGLRenderingContextWrapper.js: -------------------------------------------------------------------------------- 1 | import{ Wrapper } from "./Wrapper"; 2 | import{ ContextWrapper } from "./ContextWrapper"; 3 | 4 | import{ EXTDisjointTimerQueryExtensionWrapper } from "./extensions/EXTDisjointTimerQueryExtensionWrapper"; 5 | import{ WebGLDebugShadersExtensionWrapper } from "./extensions/WebGLDebugShadersExtensionWrapper"; 6 | import{ ANGLEInstancedArraysExtensionWrapper } from "./extensions/ANGLEInstancedArraysExtensionWrapper"; 7 | 8 | function WebGLRenderingContextWrapper( context ){ 9 | 10 | ContextWrapper.call( this, context ); 11 | 12 | this.queryStack = []; 13 | this.activeQuery = null; 14 | this.queryExt = null; 15 | 16 | this.drawQueries = []; 17 | 18 | this.programCount = 0; 19 | this.textureCount = 0; 20 | this.framebufferCount = 0; 21 | 22 | this.useProgramCount = 0; 23 | this.bindTextureCount = 0; 24 | this.bindFramebufferCount = 0; 25 | 26 | this.drawArraysCalls = 0; 27 | this.drawElementsCalls = 0; 28 | 29 | this.instancedDrawArraysCalls = 0; 30 | this.instancedDrawElementsCalls = 0; 31 | 32 | this.pointsCount = 0; 33 | this.linesCount = 0; 34 | this.trianglesCount = 0; 35 | 36 | this.instancedPointsCount = 0; 37 | this.instancedLinesCount = 0; 38 | this.instancedTrianglesCount = 0; 39 | 40 | this.frameId = null; 41 | this.currentProgram = null; 42 | this.boundTexture2D = null; 43 | this.boundTextureCube = null; 44 | 45 | this.textures = new Map(); 46 | 47 | this.boundBuffer = null; 48 | 49 | this.buffers = new Map(); 50 | 51 | } 52 | 53 | WebGLRenderingContextWrapper.prototype = Object.create( ContextWrapper.prototype ); 54 | 55 | WebGLRenderingContextWrapper.prototype.cloned = false; 56 | 57 | cloneWebGLRenderingContextPrototype(); 58 | 59 | WebGLRenderingContextWrapper.prototype.setFrameId = function( frameId ) { 60 | 61 | this.frameId = frameId; 62 | 63 | } 64 | 65 | WebGLRenderingContextWrapper.prototype.resetFrame = function(){ 66 | 67 | ContextWrapper.prototype.resetFrame.call( this ); 68 | 69 | this.useProgramCount = 0; 70 | this.bindTextureCount = 0; 71 | this.bindFramebufferCount = 0; 72 | 73 | this.drawArraysCalls = 0; 74 | this.drawElementsCalls = 0; 75 | 76 | this.instancedDrawArraysCalls = 0; 77 | this.instancedDrawElementsCalls = 0; 78 | 79 | this.pointsCount = 0; 80 | this.linesCount = 0; 81 | this.trianglesCount = 0; 82 | 83 | this.instancedPointsCount = 0; 84 | this.instancedLinesCount = 0; 85 | this.instancedTrianglesCount = 0; 86 | 87 | } 88 | 89 | function cloneWebGLRenderingContextPrototype(){ 90 | 91 | // some sites (e.g. http://codeflow.org/webgl/deferred-irradiance-volumes/www/) 92 | // modify the prototype, and they do it after the initial check for support 93 | 94 | // if( WebGLRenderingContextWrapper.prototype.cloned ) return; 95 | // WebGLRenderingContextWrapper.prototype.cloned = true; 96 | 97 | Object.keys( WebGLRenderingContext.prototype ).forEach( key => { 98 | 99 | // .canvas is weird, so it's directly assigned when creating the wrapper 100 | 101 | if( key !== 'canvas' ){ 102 | 103 | try{ 104 | if( typeof WebGLRenderingContext.prototype[ key ] === 'function' ){ 105 | WebGLRenderingContextWrapper.prototype[ key ] = function(){ 106 | var args = new Array(arguments.length); 107 | for (var i = 0, l = arguments.length; i < l; i++){ 108 | args[i] = arguments[i]; 109 | } 110 | return this.run( key, args, _ => { 111 | return WebGLRenderingContext.prototype[ key ].apply( this.context, args ); 112 | }); 113 | } 114 | } else { 115 | WebGLRenderingContextWrapper.prototype[ key ] = WebGLRenderingContext.prototype[ key ]; 116 | } 117 | } catch( e ){ 118 | Object.defineProperty( WebGLRenderingContext.prototype, key, { 119 | get: function (){ return this.context[ key ]; }, 120 | set: function ( v ){ this.context[ key ] = v; } 121 | }); 122 | } 123 | 124 | } 125 | 126 | }); 127 | 128 | instrumentWebGLRenderingContext(); 129 | 130 | } 131 | 132 | WebGLRenderingContextWrapper.prototype.getTextureMemory = function() { 133 | 134 | var memory = 0; 135 | 136 | this.textures.forEach( t => { 137 | 138 | memory += t.size; 139 | 140 | }); 141 | 142 | return memory; 143 | 144 | } 145 | 146 | WebGLRenderingContextWrapper.prototype.getBufferMemory = function() { 147 | 148 | var memory = 0; 149 | 150 | this.buffers.forEach( b => { 151 | 152 | memory += b.size; 153 | 154 | }); 155 | 156 | return memory; 157 | 158 | } 159 | 160 | const extensionWrappers = { 161 | WEBGL_debug_shaders: WebGLDebugShadersExtensionWrapper, 162 | EXT_disjoint_timer_query: EXTDisjointTimerQueryExtensionWrapper, 163 | ANGLE_instanced_arrays: ANGLEInstancedArraysExtensionWrapper 164 | }; 165 | 166 | WebGLRenderingContextWrapper.prototype.getExtension = function(){ 167 | 168 | var extensionName = arguments[ 0 ]; 169 | 170 | return this.run( 'getExtension', arguments, _ => { 171 | 172 | var wrapper = extensionWrappers[ extensionName ]; 173 | if( wrapper ) { 174 | return new wrapper( this ); 175 | } 176 | 177 | return this.context.getExtension( extensionName ); 178 | 179 | }); 180 | 181 | } 182 | 183 | WebGLRenderingContextWrapper.prototype.updateDrawCount = function( mode, count ){ 184 | 185 | var gl = this.context; 186 | 187 | switch( mode ){ 188 | case gl.POINTS: 189 | this.pointsCount += count; 190 | break; 191 | case gl.LINE_STRIP: 192 | this.linesCount += count - 1; 193 | break; 194 | case gl.LINE_LOOP: 195 | this.linesCount += count; 196 | break; 197 | case gl.LINES: 198 | this.linesCount += count / 2; 199 | break; 200 | case gl.TRIANGLE_STRIP: 201 | case gl.TRIANGLE_FAN: 202 | this.trianglesCount += count - 2; 203 | break; 204 | case gl.TRIANGLES: 205 | this.trianglesCount += count / 3; 206 | break; 207 | } 208 | 209 | }; 210 | 211 | WebGLRenderingContextWrapper.prototype.updateInstancedDrawCount = function( mode, count ){ 212 | 213 | var gl = this.context; 214 | 215 | switch( mode ){ 216 | case gl.POINTS: 217 | this.instancedPointsCount += count; 218 | break; 219 | case gl.LINE_STRIP: 220 | this.instancedLinesCount += count - 1; 221 | break; 222 | case gl.LINE_LOOP: 223 | this.instancedLinesCount += count; 224 | break; 225 | case gl.LINES: 226 | this.instancedLinesCount += count / 2; 227 | break; 228 | case gl.TRIANGLE_STRIP: 229 | case gl.TRIANGLE_FAN: 230 | this.instancedTrianglesCount += count - 2; 231 | break; 232 | case gl.TRIANGLES: 233 | this.instancedTrianglesCount += count / 3; 234 | break; 235 | } 236 | 237 | }; 238 | 239 | WebGLRenderingContextWrapper.prototype.drawElements = function(){ 240 | 241 | this.drawElementsCalls++; 242 | this.updateDrawCount( arguments[ 0 ], arguments[ 1 ] ); 243 | 244 | return this.run( 'drawElements', arguments, _ => { 245 | 246 | var program = this.context.getParameter( this.context.CURRENT_PROGRAM ); 247 | if( program !== this.currentProgram.program ) { 248 | debugger; 249 | } 250 | 251 | if( settings.profileShaders ) { 252 | var ext = this.queryExt; 253 | var query = ext.createQueryEXT(); 254 | ext.beginQueryEXT( ext.TIME_ELAPSED_EXT, query ); 255 | this.drawQueries.push( { 256 | query, 257 | program: this.currentProgram, 258 | frameId: this.frameId 259 | } ); 260 | } 261 | 262 | var res = WebGLRenderingContext.prototype.drawElements.apply( this.context, arguments ); 263 | 264 | if( settings.profileShaders ) { 265 | ext.endQueryEXT( ext.TIME_ELAPSED_EXT ); 266 | } 267 | 268 | return res; 269 | 270 | }); 271 | 272 | } 273 | 274 | WebGLRenderingContextWrapper.prototype.drawArrays = function(){ 275 | 276 | this.drawArraysCalls++; 277 | this.updateDrawCount( arguments[ 0 ], arguments[ 2 ] ); 278 | 279 | return this.run( 'drawArrays', arguments, _ => { 280 | 281 | var program = this.context.getParameter( this.context.CURRENT_PROGRAM ); 282 | if( program !== this.currentProgram.program ) { 283 | debugger; 284 | } 285 | 286 | if( settings.profileShaders ) { 287 | var ext = this.queryExt; 288 | var query = ext.createQueryEXT(); 289 | ext.beginQueryEXT( ext.TIME_ELAPSED_EXT, query ); 290 | this.drawQueries.push( { 291 | query, 292 | program: this.currentProgram, 293 | frameId: this.frameId 294 | } ); 295 | } 296 | 297 | var res = WebGLRenderingContext.prototype.drawArrays.apply( this.context, arguments ); 298 | 299 | if( settings.profileShaders ) { 300 | ext.endQueryEXT( ext.TIME_ELAPSED_EXT ); 301 | } 302 | 303 | return res; 304 | 305 | }); 306 | 307 | } 308 | 309 | const formats = {} 310 | formats[ WebGLRenderingContext.prototype.ALPHA ] = 1; 311 | formats[ WebGLRenderingContext.prototype.RGB ] = 3; 312 | formats[ WebGLRenderingContext.prototype.RGBA ] = 4; 313 | formats[ WebGLRenderingContext.prototype.RGBA ] = 4; 314 | formats[ WebGLRenderingContext.prototype.LUMINANCE ] = 1; 315 | formats[ WebGLRenderingContext.prototype.LUMINANCE_ALPHA ] = 1; 316 | formats[ WebGLRenderingContext.prototype.DEPTH_COMPONENT ] = 1; 317 | 318 | const types = {} 319 | types[ WebGLRenderingContext.prototype.UNSIGNED_BYTE ] = 1; 320 | types[ WebGLRenderingContext.prototype.FLOAT ] = 4; 321 | types[ 36193 ] = 2; // OESTextureHalfFloat.HALF_FLOAT_OES 322 | types[ WebGLRenderingContext.prototype.UNSIGNED_INT ] = 4; 323 | 324 | function WebGLShaderWrapper( contextWrapper, type ){ 325 | 326 | Wrapper.call( this ); 327 | 328 | this.contextWrapper = contextWrapper; 329 | this.shader = WebGLRenderingContext.prototype.createShader.apply( this.contextWrapper.context, [ type ] ); 330 | this.version = 1; 331 | this.source = null; 332 | this.type = type; 333 | 334 | } 335 | 336 | WebGLShaderWrapper.prototype = Object.create( Wrapper.prototype ); 337 | 338 | WebGLShaderWrapper.prototype.shaderSource = function( source ){ 339 | 340 | this.source = source; 341 | return WebGLRenderingContext.prototype.shaderSource.apply( this.contextWrapper.context, [ this.shader, source ] ); 342 | 343 | } 344 | 345 | function WebGLUniformLocationWrapper( contextWrapper, program, name ){ 346 | 347 | Wrapper.call( this ); 348 | 349 | this.contextWrapper = contextWrapper; 350 | this.program = program; 351 | this.name = name; 352 | this.getUniformLocation(); 353 | 354 | this.program.uniformLocations[ this.name ] = this; 355 | 356 | //log( 'Location for uniform', name, 'on program', this.program.uuid ); 357 | 358 | } 359 | 360 | WebGLUniformLocationWrapper.prototype = Object.create( Wrapper.prototype ); 361 | 362 | WebGLUniformLocationWrapper.prototype.getUniformLocation = function(){ 363 | 364 | this.uniformLocation = WebGLRenderingContext.prototype.getUniformLocation.apply( this.contextWrapper, [ this.program.program, this.name ] ); 365 | 366 | } 367 | 368 | function WebGLProgramWrapper( contextWrapper ){ 369 | 370 | Wrapper.call( this ); 371 | 372 | this.contextWrapper = contextWrapper; 373 | this.program = WebGLRenderingContext.prototype.createProgram.apply( this.contextWrapper.context ); 374 | this.version = 1; 375 | this.vertexShaderWrapper = null; 376 | this.fragmentShaderWrapper = null; 377 | 378 | this.uniformLocations = {}; 379 | 380 | } 381 | 382 | WebGLProgramWrapper.prototype = Object.create( Wrapper.prototype ); 383 | 384 | WebGLProgramWrapper.prototype.attachShader = function(){ 385 | 386 | var shaderWrapper = arguments[ 0 ]; 387 | 388 | if( shaderWrapper.type == this.contextWrapper.context.VERTEX_SHADER ) this.vertexShaderWrapper = shaderWrapper; 389 | if( shaderWrapper.type == this.contextWrapper.context.FRAGMENT_SHADER ) this.fragmentShaderWrapper = shaderWrapper; 390 | 391 | return this.contextWrapper.run( 'attachShader', arguments, _ => { 392 | return WebGLRenderingContext.prototype.attachShader.apply( this.contextWrapper.context, [ this.program, shaderWrapper.shader ] ); 393 | }); 394 | 395 | } 396 | 397 | WebGLProgramWrapper.prototype.detachShader = function(){ 398 | 399 | var shaderWrapper = arguments[ 0 ]; 400 | 401 | return this.contextWrapper.run( 'detachShader', arguments, _ => { 402 | return WebGLRenderingContext.prototype.detachShader.apply( this.contextWrapper.context, [ this.program, shaderWrapper.shader ] ); 403 | }); 404 | 405 | } 406 | 407 | WebGLProgramWrapper.prototype.highlight = function(){ 408 | 409 | this.contextWrapper.context.detachShader.apply( this.contextWrapper.context, [ this.program, this.fragmentShaderWrapper.shader ] ); 410 | 411 | var fs = this.fragmentShaderWrapper.source; 412 | fs = fs.replace( /\s+main\s*\(/, ' ShaderEditorInternalMain(' ); 413 | fs += '\r\n' + 'void main(){ ShaderEditorInternalMain(); gl_FragColor.rgb *= vec3(1.,0.,1.); }'; 414 | 415 | var highlightShaderWrapper = new WebGLShaderWrapper( this.contextWrapper, this.contextWrapper.context.FRAGMENT_SHADER ); 416 | highlightShaderWrapper.shaderSource( fs ); 417 | WebGLRenderingContext.prototype.compileShader.apply( this.contextWrapper.context, [ highlightShaderWrapper.shader ] ); 418 | WebGLRenderingContext.prototype.attachShader.apply( this.contextWrapper.context, [ this.program, highlightShaderWrapper.shader ] ); 419 | WebGLRenderingContext.prototype.linkProgram.apply( this.contextWrapper.context, [ this.program ] ); 420 | 421 | Object.keys( this.uniformLocations ).forEach( name => { 422 | this.uniformLocations[ name ].getUniformLocation(); 423 | }); 424 | 425 | } 426 | 427 | function WebGLTextureWrapper( contextWrapper ) { 428 | 429 | Wrapper.call( this ); 430 | 431 | log( 'createTexture', this.uuid ); 432 | 433 | this.contextWrapper = contextWrapper; 434 | this.texture = this.contextWrapper.context.createTexture(); 435 | 436 | this.contextWrapper.textures.set( this, this ); 437 | 438 | this.size = 0; 439 | 440 | } 441 | 442 | WebGLTextureWrapper.prototype = Object.create( Wrapper.prototype ); 443 | 444 | WebGLTextureWrapper.prototype.computeTextureMemoryUsage = function() { 445 | 446 | log( 'texImaged2D', arguments ); 447 | 448 | if( arguments.length === 6 ) { 449 | 450 | // texImage2D(target, level, internalformat, format, type, ImageData? pixels); 451 | // texImage2D(target, level, internalformat, format, type, HTMLImageElement? pixels); 452 | // texImage2D(target, level, internalformat, format, type, HTMLCanvasElement? pixels); 453 | // texImage2D(target, level, internalformat, format, type, HTMLVideoElement? pixels); 454 | 455 | var size = formats[ arguments[ 2 ] ] * types[ arguments[ 4 ] ]; 456 | if( isNaN( size ) ) debugger; 457 | var width = 0; 458 | var height = 0; 459 | 460 | if( arguments[ 5 ] instanceof HTMLImageElement ) { 461 | width = arguments[ 5 ].naturalWidth; 462 | height = arguments[ 5 ].naturalHeight; 463 | } 464 | 465 | if( arguments[ 5 ] instanceof HTMLCanvasElement || arguments[ 5 ] instanceof ImageData ) { 466 | width = arguments[ 5 ].width; 467 | height = arguments[ 5 ].height; 468 | } 469 | 470 | if( arguments[ 5 ] instanceof HTMLVideoElement ) { 471 | width = arguments[ 5 ].videoWidth; 472 | height = arguments[ 5 ].videoHeight; 473 | } 474 | 475 | var memory = width * height * size; 476 | this.size = memory; 477 | 478 | log( 'computeTextureMemoryUsage', width, height, size, memory, 'bytes' ); 479 | 480 | } else if( arguments.length === 9 ) { 481 | 482 | // texImage2D(target, level, internalformat, width, height, border, format, type, ArrayBufferView? pixels); 483 | 484 | var size = formats[ arguments[ 2 ] ] * types[ arguments[ 7 ] ]; 485 | if( isNaN( size ) ) debugger; 486 | var width = arguments[ 3 ] 487 | var height = arguments[ 4 ]; 488 | 489 | var memory = width * height * size; 490 | this.size = memory; 491 | 492 | log( 'computeTextureMemoryUsage', width, height, size, memory, 'bytes' ); 493 | 494 | } else { 495 | 496 | log( 'ARGUMENTS LENGTH NOT RECOGNISED' ); 497 | 498 | } 499 | 500 | } 501 | 502 | function WebGLBufferWrapper( contextWrapper ) { 503 | 504 | Wrapper.call( this ); 505 | 506 | this.contextWrapper = contextWrapper; 507 | this.buffer = this.contextWrapper.context.createBuffer(); 508 | 509 | this.contextWrapper.buffers.set( this, this ); 510 | 511 | this.size = 0; 512 | 513 | } 514 | 515 | WebGLBufferWrapper.prototype = Object.create( Wrapper.prototype ); 516 | 517 | function instrumentWebGLRenderingContext(){ 518 | 519 | WebGLRenderingContextWrapper.prototype.createShader = function(){ 520 | 521 | //log( 'create shader' ); 522 | return this.run( 'createShader', arguments, _ => { 523 | return new WebGLShaderWrapper( this, arguments[ 0 ] ); 524 | }); 525 | 526 | } 527 | 528 | WebGLRenderingContextWrapper.prototype.shaderSource = function(){ 529 | 530 | return this.run( 'shaderSource', arguments, _ => { 531 | return arguments[ 0 ].shaderSource( arguments[ 1 ] ); 532 | }); 533 | 534 | } 535 | 536 | WebGLRenderingContextWrapper.prototype.compileShader = function(){ 537 | 538 | return this.run( 'compileShader', arguments, _ => { 539 | return WebGLRenderingContext.prototype.compileShader.apply( this.context, [ arguments[ 0 ].shader ] ); 540 | }); 541 | 542 | } 543 | 544 | WebGLRenderingContextWrapper.prototype.getShaderParameter = function(){ 545 | 546 | return this.run( 'getShaderParameter', arguments, _ => { 547 | return WebGLRenderingContext.prototype.getShaderParameter.apply( this.context, [ arguments[ 0 ].shader, arguments[ 1 ] ] ); 548 | }); 549 | 550 | } 551 | 552 | WebGLRenderingContextWrapper.prototype.getShaderInfoLog = function(){ 553 | 554 | return this.run( 'getShaderInfoLog', arguments, _ => { 555 | return WebGLRenderingContext.prototype.getShaderInfoLog.apply( this.context, [ arguments[ 0 ].shader ] ); 556 | }); 557 | 558 | } 559 | 560 | WebGLRenderingContextWrapper.prototype.deleteShader = function(){ 561 | 562 | return this.run( 'deleteShader', arguments, _ => { 563 | return WebGLRenderingContext.prototype.deleteShader.apply( this.context, [ arguments[ 0 ].shader ] ); 564 | }); 565 | 566 | } 567 | 568 | WebGLRenderingContextWrapper.prototype.createProgram = function(){ 569 | 570 | log( 'create program' ); 571 | this.programCount++; 572 | return this.run( 'createProgram', arguments, _ => { 573 | return new WebGLProgramWrapper( this ); 574 | }); 575 | 576 | } 577 | 578 | WebGLRenderingContextWrapper.prototype.deleteProgram = function( programWrapper ){ 579 | 580 | this.incrementCount(); 581 | this.programCount--; 582 | return this.run( 'deleteProgram', arguments, _ => { 583 | return WebGLRenderingContext.prototype.deleteProgram.apply( this.context, [ programWrapper.program ] ); 584 | }); 585 | 586 | } 587 | 588 | WebGLRenderingContextWrapper.prototype.attachShader = function(){ 589 | 590 | return arguments[ 0 ].attachShader( arguments[ 1 ] ); 591 | 592 | } 593 | 594 | WebGLRenderingContextWrapper.prototype.detachShader = function(){ 595 | 596 | return arguments[ 0 ].detachShader( arguments[ 1 ] ); 597 | 598 | } 599 | 600 | WebGLRenderingContextWrapper.prototype.linkProgram = function(){ 601 | 602 | return this.run( 'linkProgram', arguments, _ => { 603 | return WebGLRenderingContext.prototype.linkProgram.apply( this.context, [ arguments[ 0 ].program ] ); 604 | }); 605 | 606 | } 607 | 608 | WebGLRenderingContextWrapper.prototype.validateProgram = function(){ 609 | 610 | return this.run( 'validateProgram', arguments, _ => { 611 | return WebGLRenderingContext.prototype.validateProgram.apply( this.context, [ arguments[ 0 ].program ] ); 612 | }); 613 | 614 | } 615 | 616 | WebGLRenderingContextWrapper.prototype.getProgramParameter = function(){ 617 | 618 | return this.run( 'getProgramParameter', arguments, _ => { 619 | return WebGLRenderingContext.prototype.getProgramParameter.apply( this.context, [ arguments[ 0 ].program, arguments[ 1 ] ] ); 620 | }); 621 | 622 | } 623 | 624 | WebGLRenderingContextWrapper.prototype.getProgramInfoLog = function(){ 625 | 626 | return this.run( 'getProgramInfoLog', arguments, _ => { 627 | return WebGLRenderingContext.prototype.getProgramInfoLog.apply( this.context, [ arguments[ 0 ].program ] ); 628 | }); 629 | 630 | } 631 | 632 | WebGLRenderingContextWrapper.prototype.getActiveAttrib = function(){ 633 | 634 | return this.run( 'getActiveAttrib', arguments, _ => { 635 | return WebGLRenderingContext.prototype.getActiveAttrib.apply( this.context, [ arguments[ 0 ].program, arguments[ 1 ] ] ); 636 | }); 637 | 638 | } 639 | 640 | WebGLRenderingContextWrapper.prototype.getAttribLocation = function(){ 641 | 642 | return this.run( 'getAttribLocation', arguments, _ => { 643 | return WebGLRenderingContext.prototype.getAttribLocation.apply( this.context, [ arguments[ 0 ].program, arguments[ 1 ] ] ); 644 | }); 645 | 646 | } 647 | 648 | WebGLRenderingContextWrapper.prototype.bindAttribLocation = function(){ 649 | 650 | return this.run( 'bindAttribLocation', arguments, _ => { 651 | return WebGLRenderingContext.prototype.bindAttribLocation.apply( this.context, [ arguments[ 0 ].program, arguments[ 1 ], arguments[ 2 ] ] ); 652 | }); 653 | 654 | } 655 | 656 | WebGLRenderingContextWrapper.prototype.getActiveUniform = function(){ 657 | 658 | return this.run( 'getActiveUniform', arguments, _ => { 659 | return WebGLRenderingContext.prototype.getActiveUniform.apply( this.context, [ arguments[ 0 ].program, arguments[ 1 ] ] ); 660 | }); 661 | 662 | } 663 | 664 | WebGLRenderingContextWrapper.prototype.getUniformLocation = function(){ 665 | 666 | return this.run( 'getUniformLocation', arguments, _ => { 667 | return new WebGLUniformLocationWrapper( this.context, arguments[ 0 ], arguments[ 1 ] ); 668 | }); 669 | 670 | } 671 | 672 | WebGLRenderingContextWrapper.prototype.useProgram = function(){ 673 | 674 | this.useProgramCount++; 675 | this.currentProgram = arguments[ 0 ]; 676 | return this.run( 'useProgram', arguments, _ => { 677 | return WebGLRenderingContext.prototype.useProgram.apply( this.context, [ arguments[ 0 ] ? arguments[ 0 ].program : null ] ); 678 | }); 679 | 680 | } 681 | 682 | WebGLRenderingContextWrapper.prototype.createTexture = function(){ 683 | 684 | this.textureCount++; 685 | return this.run( 'createTexture', arguments, _ => { 686 | return new WebGLTextureWrapper( this ); 687 | }); 688 | 689 | } 690 | 691 | WebGLRenderingContextWrapper.prototype.deleteTexture = function(){ 692 | 693 | this.textures.delete( arguments[ 0 ] ); 694 | this.textureCount--; 695 | 696 | return this.run( 'deleteTexture', arguments, _ => { 697 | return WebGLRenderingContext.prototype.deleteTexture.apply( this.context, [ arguments[ 0 ].texture ] ); 698 | }); 699 | 700 | } 701 | 702 | WebGLRenderingContextWrapper.prototype.isTexture = function(){ 703 | 704 | return this.run( 'isTexture', arguments, _ => { 705 | return WebGLRenderingContext.prototype.isTexture.apply( this.context, [ arguments[ 0 ].texture ] ); 706 | }); 707 | 708 | } 709 | 710 | WebGLRenderingContextWrapper.prototype.bindTexture = function(){ 711 | 712 | log( 'bindTexture', arguments[ 1 ] ); 713 | 714 | this.bindTextureCount++; 715 | if( arguments[ 0 ] === WebGLRenderingContext.prototype.TEXTURE_2D ) { 716 | this.boundTexture2D = arguments[ 1 ]; 717 | } 718 | if( arguments[ 0 ] === WebGLRenderingContext.prototype.TEXTURE_CUBE_MAP ) { 719 | this.boundTextureCube = arguments[ 1 ]; 720 | } 721 | 722 | return this.run( 'bindTexture', arguments, _ => { 723 | return WebGLRenderingContext.prototype.bindTexture.apply( 724 | this.context, 725 | [ 726 | arguments[ 0 ], 727 | arguments[ 1 ] ? arguments[ 1 ].texture : null 728 | ] 729 | ); 730 | }); 731 | 732 | } 733 | 734 | var cubeMapConsts = [ 735 | WebGLRenderingContext.prototype.TEXTURE_CUBE_MAP_POSITIVE_X, 736 | WebGLRenderingContext.prototype.TEXTURE_CUBE_MAP_NEGATIVE_X, 737 | WebGLRenderingContext.prototype.TEXTURE_CUBE_MAP_POSITIVE_Y, 738 | WebGLRenderingContext.prototype.TEXTURE_CUBE_MAP_NEGATIVE_Y, 739 | WebGLRenderingContext.prototype.TEXTURE_CUBE_MAP_POSITIVE_Z, 740 | WebGLRenderingContext.prototype.TEXTURE_CUBE_MAP_NEGATIVE_Z 741 | ]; 742 | 743 | WebGLRenderingContextWrapper.prototype.texImage2D = function(){ 744 | 745 | if( arguments[ 0 ] === WebGLRenderingContext.prototype.TEXTURE_2D ) { 746 | if( this.boundTexture2D ) { 747 | this.boundTexture2D.computeTextureMemoryUsage.apply( this.boundTexture2D, arguments ); 748 | } 749 | } 750 | 751 | if( cubeMapConsts.some( v => v === arguments[ 0 ] ) ) { 752 | this.boundTextureCube.computeTextureMemoryUsage.apply( this.boundTextureCube, arguments ); 753 | } 754 | 755 | return this.run( 'texImage2D', arguments, _ => { 756 | return WebGLRenderingContext.prototype.texImage2D.apply( 757 | this.context, 758 | arguments 759 | ); 760 | }); 761 | 762 | } 763 | 764 | WebGLRenderingContextWrapper.prototype.framebufferTexture2D = function(){ 765 | 766 | return this.run( 'framebufferTexture2D', arguments, _ => { 767 | return WebGLRenderingContext.prototype.framebufferTexture2D.apply( 768 | this.context, 769 | [ 770 | arguments[ 0 ], 771 | arguments[ 1 ], 772 | arguments[ 2 ], 773 | arguments[ 3 ].texture, 774 | arguments[ 4 ] 775 | ] ); 776 | }); 777 | 778 | } 779 | 780 | WebGLRenderingContextWrapper.prototype.createFramebuffer = function() { 781 | 782 | this.framebufferCount++; 783 | return this.run( 'createFramebuffer', arguments, _ => { 784 | return WebGLRenderingContext.prototype.createFramebuffer.apply( this.context, arguments ); 785 | }); 786 | 787 | } 788 | 789 | WebGLRenderingContextWrapper.prototype.deleteFramebuffer = function() { 790 | 791 | this.framebufferCount--; 792 | return this.run( 'deleteFramebuffer', arguments, _ => { 793 | return WebGLRenderingContext.prototype.deleteFramebuffer.apply( this.context, arguments ); 794 | }); 795 | 796 | } 797 | 798 | WebGLRenderingContextWrapper.prototype.bindFramebuffer = function() { 799 | 800 | this.bindFramebufferCount++; 801 | return this.run( 'bindFramebuffer', arguments, _ => { 802 | return WebGLRenderingContext.prototype.bindFramebuffer.apply( this.context, arguments ); 803 | }); 804 | 805 | } 806 | 807 | var methods = [ 808 | 'uniform1f', 'uniform1fv', 'uniform1i', 'uniform1iv', 809 | 'uniform2f', 'uniform2fv', 'uniform2i', 'uniform2iv', 810 | 'uniform3f', 'uniform3fv', 'uniform3i', 'uniform3iv', 811 | 'uniform4f', 'uniform4fv', 'uniform4i', 'uniform4iv', 812 | 'uniformMatrix2fv', 'uniformMatrix3fv', 'uniformMatrix4fv' 813 | ]; 814 | 815 | var originalMethods = {}; 816 | 817 | methods.forEach( method => { 818 | 819 | var original = WebGLRenderingContext.prototype[ method ]; 820 | originalMethods[ method ] = original; 821 | 822 | WebGLRenderingContextWrapper.prototype[ method ] = function(){ 823 | 824 | var args = new Array(arguments.length); 825 | for (var i = 0, l = arguments.length; i < l; i++){ 826 | args[i] = arguments[i]; 827 | } 828 | if( !args[ 0 ] ) return; 829 | args[ 0 ] = args[ 0 ].uniformLocation; 830 | return this.run( method, args, _ => { 831 | return original.apply( this.context, args ); 832 | }); 833 | 834 | } 835 | 836 | }); 837 | 838 | WebGLRenderingContextWrapper.prototype.createBuffer = function() { 839 | 840 | return this.run( 'createBuffer', arguments, _ => { 841 | return new WebGLBufferWrapper( this ); 842 | }); 843 | 844 | } 845 | 846 | 847 | WebGLRenderingContextWrapper.prototype.bufferData = function() { 848 | 849 | this.boundBuffer.size = arguments[ 1 ].length; 850 | 851 | return this.run( 'bufferData', arguments, _ => { 852 | return WebGLRenderingContext.prototype.bufferData.apply( this.context, arguments ); 853 | }); 854 | 855 | } 856 | 857 | WebGLRenderingContextWrapper.prototype.bindBuffer = function() { 858 | 859 | this.boundBuffer = arguments[ 1 ]; 860 | 861 | return this.run( 'bindBuffer', arguments, _ => { 862 | return WebGLRenderingContext.prototype.bindBuffer.apply( 863 | this.context, 864 | [ 865 | arguments[ 0 ], 866 | arguments[ 1 ] ? arguments[ 1 ].buffer : null 867 | ] 868 | ); 869 | }); 870 | 871 | } 872 | 873 | } 874 | 875 | export { WebGLRenderingContextWrapper }; 876 | -------------------------------------------------------------------------------- /src/Wrapper.js: -------------------------------------------------------------------------------- 1 | import{ createUUID } from "./utils"; 2 | 3 | class Wrapper { 4 | 5 | constructor() { 6 | 7 | this.uuid = createUUID(); 8 | 9 | } 10 | 11 | } 12 | 13 | export { Wrapper } 14 | -------------------------------------------------------------------------------- /src/canvas-instrument.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | "use strict"; 4 | 5 | function createUUID(){ 6 | 7 | function s4(){ 8 | return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); 9 | } 10 | 11 | return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`; 12 | 13 | } 14 | 15 | function FrameData( id ){ 16 | 17 | this.frameId = id; 18 | 19 | this.framerate = 0; 20 | this.frameTime = 0; 21 | this.JavaScriptTime = 0; 22 | 23 | this.contexts = new Map(); 24 | 25 | } 26 | 27 | function ContextFrameData( type ){ 28 | 29 | this.type = type; 30 | 31 | this.JavaScriptTime = 0; 32 | this.GPUTime = 0; 33 | this.log = []; 34 | 35 | this.createProgram = 0; 36 | this.createTexture = 0; 37 | 38 | this.useProgram = 0; 39 | this.bindTexture = 0; 40 | 41 | this.triangles = 0; 42 | this.lines = 0; 43 | this.points = 0; 44 | 45 | this.startTime = 0; 46 | 47 | } 48 | 49 | function ContextData( contextWrapper ){ 50 | 51 | this.id = createUUID(); 52 | this.queryExt = null; 53 | this.contextWrapper = contextWrapper; 54 | this.extQueries = []; 55 | 56 | } 57 | 58 | function Wrapper( context ){ 59 | 60 | this.id = createUUID(); 61 | this.context = context; 62 | 63 | this.count = 0; 64 | this.JavaScriptTime = 0; 65 | 66 | this.log = []; 67 | 68 | } 69 | 70 | Wrapper.prototype.run = function( fName, fArgs, fn ){ 71 | 72 | this.incrementCount(); 73 | this.beginProfile( fName, fArgs ); 74 | var res = fn(); 75 | this.endProfile(); 76 | return res; 77 | 78 | } 79 | 80 | Wrapper.prototype.resetFrame = function(){ 81 | 82 | this.resetCount(); 83 | this.resetJavaScriptTime(); 84 | this.resetLog(); 85 | 86 | } 87 | 88 | Wrapper.prototype.resetCount = function(){ 89 | 90 | this.count = 0; 91 | 92 | } 93 | 94 | Wrapper.prototype.incrementCount = function(){ 95 | 96 | this.count++; 97 | 98 | } 99 | 100 | Wrapper.prototype.resetLog = function(){ 101 | 102 | this.log.length = 0; 103 | 104 | } 105 | 106 | Wrapper.prototype.resetJavaScriptTime = function(){ 107 | 108 | this.JavaScriptTime = 0; 109 | 110 | } 111 | 112 | Wrapper.prototype.incrementJavaScriptTime = function( time ){ 113 | 114 | this.JavaScriptTime += time; 115 | 116 | } 117 | 118 | Wrapper.prototype.beginProfile = function( fn, args ){ 119 | 120 | var t = performance.now(); 121 | this.log.push( { function: fn, arguments: args, start: t, end: 0 } ); 122 | this.startTime = t; 123 | 124 | } 125 | 126 | Wrapper.prototype.endProfile = function(){ 127 | 128 | var t = performance.now(); 129 | this.log[ this.log.length - 1 ].end = t; 130 | this.incrementJavaScriptTime( t - this.startTime ); 131 | 132 | } 133 | 134 | function CanvasRenderingContext2DWrapper( context ){ 135 | 136 | Wrapper.call( this, context ); 137 | 138 | } 139 | 140 | CanvasRenderingContext2DWrapper.prototype = Object.create( Wrapper.prototype ); 141 | 142 | CanvasRenderingContext2DWrapper.prototype.resetFrame = function(){ 143 | 144 | Wrapper.prototype.resetFrame.call( this ); 145 | 146 | } 147 | 148 | Object.keys( CanvasRenderingContext2D.prototype ).forEach( key => { 149 | 150 | if( key !== 'canvas' ){ 151 | 152 | try{ 153 | if( typeof CanvasRenderingContext2D.prototype[ key ] === 'function' ){ 154 | CanvasRenderingContext2DWrapper.prototype[ key ] = function(){ 155 | var args = new Array(arguments.length); 156 | for (var i = 0, l = arguments.length; i < l; i++){ 157 | args[i] = arguments[i]; 158 | } 159 | return this.run( key, args, _ => { 160 | return CanvasRenderingContext2D.prototype[ key ].apply( this.context, args ); 161 | }); 162 | } 163 | } else { 164 | CanvasRenderingContext2DWrapper.prototype[ key ] = CanvasRenderingContext2D.prototype[ key ]; 165 | } 166 | } catch( e ){ 167 | Object.defineProperty( CanvasRenderingContext2DWrapper.prototype, key, { 168 | get: function (){ return this.context[ key ]; }, 169 | set: function ( v ){ this.context[ key ] = v; } 170 | }); 171 | } 172 | 173 | } 174 | 175 | }); 176 | 177 | function WebGLRenderingContextWrapper( context ){ 178 | 179 | Wrapper.call( this, context ); 180 | 181 | this.queryStack = []; 182 | this.activeQuery = null; 183 | this.queryExt = null; 184 | 185 | this.drawQueries = []; 186 | 187 | this.programCount = 0; 188 | this.textureCount = 0; 189 | 190 | this.useProgramCount = 0; 191 | this.bindTextureCount = 0; 192 | 193 | this.drawArrayCalls = 0; 194 | this.drawElementsCalls = 0; 195 | 196 | this.pointsCount = 0; 197 | this.linesCount = 0; 198 | this.trianglesCount = 0; 199 | 200 | } 201 | 202 | WebGLRenderingContextWrapper.prototype = Object.create( Wrapper.prototype ); 203 | 204 | WebGLRenderingContextWrapper.prototype.cloned = false; 205 | 206 | cloneWebGLRenderingContextPrototype(); 207 | 208 | WebGLRenderingContextWrapper.prototype.resetFrame = function(){ 209 | 210 | Wrapper.prototype.resetFrame.call( this ); 211 | 212 | this.useProgramCount = 0; 213 | this.bindTextureCount = 0; 214 | 215 | this.drawArrayCalls = 0; 216 | this.drawElementsCalls = 0; 217 | 218 | this.pointsCount = 0; 219 | this.linesCount = 0; 220 | this.trianglesCount = 0; 221 | 222 | } 223 | 224 | function cloneWebGLRenderingContextPrototype(){ 225 | 226 | // some sites (e.g. http://codeflow.org/webgl/deferred-irradiance-volumes/www/) 227 | // modify the prototype, and they do it after the initial check for support 228 | 229 | // if( WebGLRenderingContextWrapper.prototype.cloned ) return; 230 | // WebGLRenderingContextWrapper.prototype.cloned = true; 231 | 232 | Object.keys( WebGLRenderingContext.prototype ).forEach( key => { 233 | 234 | // .canvas is weird, so it's directly assigned when creating the wrapper 235 | 236 | if( key !== 'canvas' ){ 237 | 238 | try{ 239 | if( typeof WebGLRenderingContext.prototype[ key ] === 'function' ){ 240 | WebGLRenderingContextWrapper.prototype[ key ] = function(){ 241 | var args = new Array(arguments.length); 242 | for (var i = 0, l = arguments.length; i < l; i++){ 243 | args[i] = arguments[i]; 244 | } 245 | return this.run( key, args, _ => { 246 | return WebGLRenderingContext.prototype[ key ].apply( this.context, args ); 247 | }); 248 | } 249 | } else { 250 | WebGLRenderingContextWrapper.prototype[ key ] = WebGLRenderingContext.prototype[ key ]; 251 | } 252 | } catch( e ){ 253 | Object.defineProperty( WebGLRenderingContext.prototype, key, { 254 | get: function (){ return this.context[ key ]; }, 255 | set: function ( v ){ this.context[ key ] = v; } 256 | }); 257 | } 258 | 259 | } 260 | 261 | }); 262 | 263 | instrumentWebGLRenderingContext(); 264 | 265 | } 266 | 267 | function WebGLDebugShadersExtensionWrapper( contextWrapper ){ 268 | 269 | this.id = createUUID(); 270 | this.contextWrapper = contextWrapper; 271 | this.extension = WebGLRenderingContext.prototype.getExtension.apply( this.contextWrapper.context, [ 'WEBGL_debug_shaders' ] ); 272 | 273 | } 274 | 275 | WebGLDebugShadersExtensionWrapper.prototype.getTranslatedShaderSource = function( shaderWrapper ){ 276 | 277 | return this.extension.getTranslatedShaderSource( shaderWrapper.shader ); 278 | 279 | } 280 | 281 | WebGLRenderingContextWrapper.prototype.getExtension = function(){ 282 | 283 | this.incrementCount(); 284 | 285 | var extensionName = arguments[ 0 ]; 286 | 287 | switch( extensionName ){ 288 | 289 | case 'WEBGL_debug_shaders': 290 | return new WebGLDebugShadersExtensionWrapper( this ); 291 | break; 292 | 293 | case 'EXT_disjoint_timer_query': 294 | return new EXTDisjointTimerQueryExtensionWrapper( this ); 295 | break; 296 | 297 | } 298 | 299 | return this.context.getExtension( extensionName ); 300 | 301 | } 302 | 303 | WebGLRenderingContextWrapper.prototype.updateDrawCount = function( mode, count ){ 304 | 305 | var gl = this.context; 306 | 307 | switch( mode ){ 308 | case gl.POINTS: 309 | this.pointsCount += count; 310 | break; 311 | case gl.LINE_STRIP: 312 | this.linesCount += count - 1; 313 | break; 314 | case gl.LINE_LOOP: 315 | this.linesCount += count; 316 | break; 317 | case gl.LINES: 318 | this.linesCount += count / 2; 319 | break; 320 | case gl.TRIANGLE_STRIP: 321 | case gl.TRIANGLE_FAN: 322 | this.trianglesCount += count - 2; 323 | break; 324 | case gl.TRIANGLES: 325 | this.trianglesCount += count / 3; 326 | break; 327 | } 328 | 329 | }; 330 | 331 | WebGLRenderingContextWrapper.prototype.drawElements = function(){ 332 | 333 | this.drawElementsCalls++; 334 | this.updateDrawCount( arguments[ 0 ], arguments[ 1 ] ); 335 | 336 | return this.run( 'drawElements', arguments, _ => { 337 | 338 | /*var ext = this.queryExt; 339 | var query = ext.createQueryEXT(); 340 | ext.beginQueryEXT( ext.TIME_ELAPSED_EXT, query ); 341 | this.drawQueries.push( query );*/ 342 | 343 | var res = WebGLRenderingContext.prototype.drawElements.apply( this.context, arguments ); 344 | 345 | //ext.endQueryEXT( ext.TIME_ELAPSED_EXT ); 346 | 347 | return res; 348 | 349 | }); 350 | 351 | } 352 | 353 | WebGLRenderingContextWrapper.prototype.drawArrays = function(){ 354 | 355 | this.drawArrayCalls++; 356 | this.updateDrawCount( arguments[ 0 ], arguments[ 2 ] ); 357 | 358 | return this.run( 'drawArrays', arguments, _ => { 359 | 360 | /*var ext = this.queryExt; 361 | var query = ext.createQueryEXT(); 362 | ext.beginQueryEXT( ext.TIME_ELAPSED_EXT, query ); 363 | this.drawQueries.push( query );*/ 364 | 365 | var res = WebGLRenderingContext.prototype.drawArrays.apply( this.context, arguments ); 366 | 367 | //ext.endQueryEXT( ext.TIME_ELAPSED_EXT ); 368 | 369 | return res; 370 | 371 | }); 372 | 373 | } 374 | 375 | var contexts = []; 376 | var canvasContexts = new WeakMap(); 377 | 378 | var getContext = HTMLCanvasElement.prototype.getContext; 379 | 380 | HTMLCanvasElement.prototype.getContext = function(){ 381 | 382 | setupUI(); 383 | 384 | var c = canvasContexts.get( this ); 385 | if( c ){ 386 | log( arguments, '(CACHED)' ); 387 | return c; 388 | } else { 389 | log( arguments ); 390 | } 391 | 392 | var context = getContext.apply( this, arguments ); 393 | 394 | if( arguments[ 0 ] === 'webgl' || arguments[ 0 ] === 'experimental-webgl' ){ 395 | 396 | var wrapper = new WebGLRenderingContextWrapper( context ); 397 | wrapper.canvas = this; 398 | var cData = new ContextData( wrapper ); 399 | cData.queryExt = wrapper.getExtension( 'EXT_disjoint_timer_query' ); 400 | wrapper.queryExt = cData.queryExt; 401 | contexts.push( cData ); 402 | canvasContexts.set( this, wrapper ); 403 | return wrapper; 404 | 405 | } 406 | 407 | if( arguments[ 0 ] === '2d' ){ 408 | 409 | var wrapper = new CanvasRenderingContext2DWrapper( context ); 410 | wrapper.canvas = this; 411 | var cData = new ContextData( wrapper ); 412 | contexts.push( cData ); 413 | canvasContexts.set( this, wrapper ); 414 | return wrapper; 415 | 416 | } 417 | 418 | canvasContexts.set( this, context ); 419 | return context; 420 | 421 | } 422 | 423 | function WebGLShaderWrapper( contextWrapper, type ){ 424 | 425 | this.id = createUUID(); 426 | this.contextWrapper = contextWrapper; 427 | this.shader = WebGLRenderingContext.prototype.createShader.apply( this.contextWrapper.context, [ type ] ); 428 | this.version = 1; 429 | this.source = null; 430 | this.type = type; 431 | 432 | } 433 | 434 | WebGLShaderWrapper.prototype.shaderSource = function( source ){ 435 | 436 | this.source = source; 437 | return WebGLRenderingContext.prototype.shaderSource.apply( this.contextWrapper.context, [ this.shader, source ] ); 438 | 439 | } 440 | 441 | function WebGLUniformLocationWrapper( contextWrapper, program, name ){ 442 | 443 | this.id = createUUID(); 444 | this.contextWrapper = contextWrapper; 445 | this.program = program; 446 | this.name = name; 447 | this.getUniformLocation(); 448 | 449 | this.program.uniformLocations[ this.name ] = this; 450 | 451 | log( 'Location for uniform', name, 'on program', this.program.id ); 452 | 453 | } 454 | 455 | WebGLUniformLocationWrapper.prototype.getUniformLocation = function(){ 456 | 457 | this.uniformLocation = WebGLRenderingContext.prototype.getUniformLocation.apply( this.contextWrapper, [ this.program.program, this.name ] ); 458 | 459 | } 460 | 461 | function WebGLProgramWrapper( contextWrapper ){ 462 | 463 | this.id = createUUID(); 464 | this.contextWrapper = contextWrapper; 465 | this.program = WebGLRenderingContext.prototype.createProgram.apply( this.contextWrapper.context ); 466 | this.version = 1; 467 | this.vertexShaderWrapper = null; 468 | this.fragmentShaderWrapper = null; 469 | 470 | this.uniformLocations = {}; 471 | 472 | } 473 | 474 | WebGLProgramWrapper.prototype.attachShader = function(){ 475 | 476 | var shaderWrapper = arguments[ 0 ]; 477 | 478 | if( shaderWrapper.type == this.contextWrapper.context.VERTEX_SHADER ) this.vertexShaderWrapper = shaderWrapper; 479 | if( shaderWrapper.type == this.contextWrapper.context.FRAGMENT_SHADER ) this.fragmentShaderWrapper = shaderWrapper; 480 | 481 | return this.contextWrapper.run( 'attachShader', arguments, _ => { 482 | return WebGLRenderingContext.prototype.attachShader.apply( this.contextWrapper.context, [ this.program, shaderWrapper.shader ] ); 483 | }); 484 | 485 | } 486 | 487 | WebGLProgramWrapper.prototype.highlight = function(){ 488 | 489 | detachShader.apply( this.contextWrapper.context, [ this.program, this.fragmentShaderWrapper.shader ] ); 490 | 491 | var fs = this.fragmentShaderWrapper.source; 492 | fs = fs.replace( /\s+main\s*\(/, ' ShaderEditorInternalMain(' ); 493 | fs += '\r\n' + 'void main(){ ShaderEditorInternalMain(); gl_FragColor.rgb *= vec3(1.,0.,1.); }'; 494 | 495 | var highlightShaderWrapper = new WebGLShaderWrapper( this.contextWrapper, this.contextWrapper.context.FRAGMENT_SHADER ); 496 | highlightShaderWrapper.shaderSource( fs ); 497 | WebGLRenderingContext.prototype.compileShader.apply( this.contextWrapper.context, [ highlightShaderWrapper.shader ] ); 498 | WebGLRenderingContext.prototype.attachShader.apply( this.contextWrapper.context, [ this.program, highlightShaderWrapper.shader ] ); 499 | WebGLRenderingContext.prototype.linkProgram.apply( this.contextWrapper.context, [ this.program ] ); 500 | 501 | Object.keys( this.uniformLocations ).forEach( name => { 502 | this.uniformLocations[ name ].getUniformLocation(); 503 | }); 504 | 505 | } 506 | 507 | function instrumentWebGLRenderingContext(){ 508 | 509 | WebGLRenderingContextWrapper.prototype.createShader = function(){ 510 | 511 | log( 'create shader' ); 512 | return this.run( 'createShader', arguments, _ => { 513 | return new WebGLShaderWrapper( this, arguments[ 0 ] ); 514 | }); 515 | 516 | } 517 | 518 | WebGLRenderingContextWrapper.prototype.shaderSource = function(){ 519 | 520 | return this.run( 'shaderSource', arguments, _ => { 521 | return arguments[ 0 ].shaderSource( arguments[ 1 ] ); 522 | }); 523 | 524 | } 525 | 526 | WebGLRenderingContextWrapper.prototype.compileShader = function(){ 527 | 528 | return this.run( 'compileShader', arguments, _ => { 529 | return WebGLRenderingContext.prototype.compileShader.apply( this.context, [ arguments[ 0 ].shader ] ); 530 | }); 531 | 532 | } 533 | 534 | WebGLRenderingContextWrapper.prototype.getShaderParameter = function(){ 535 | 536 | return this.run( 'getShaderParameter', arguments, _ => { 537 | return WebGLRenderingContext.prototype.getShaderParameter.apply( this.context, [ arguments[ 0 ].shader, arguments[ 1 ] ] ); 538 | }); 539 | 540 | } 541 | 542 | WebGLRenderingContextWrapper.prototype.getShaderInfoLog = function(){ 543 | 544 | return this.run( 'getShaderInfoLog', arguments, _ => { 545 | return WebGLRenderingContext.prototype.getShaderInfoLog.apply( this.context, [ arguments[ 0 ].shader ] ); 546 | }); 547 | 548 | } 549 | 550 | WebGLRenderingContextWrapper.prototype.deleteShader = function(){ 551 | 552 | return this.run( 'deleteShader', arguments, _ => { 553 | return WebGLRenderingContext.prototype.deleteShader.apply( this.context, [ arguments[ 0 ].shader ] ); 554 | }); 555 | 556 | } 557 | 558 | WebGLRenderingContextWrapper.prototype.createProgram = function(){ 559 | 560 | log( 'create program' ); 561 | this.programCount++; 562 | return this.run( 'createProgram', arguments, _ => { 563 | return new WebGLProgramWrapper( this ); 564 | }); 565 | 566 | } 567 | 568 | WebGLRenderingContextWrapper.prototype.deleteProgram = function( programWrapper ){ 569 | 570 | this.incrementCount(); 571 | this.programCount--; 572 | return this.run( 'deleteProgram', arguments, _ => { 573 | return WebGLRenderingContext.prototype.deleteProgram.apply( this.context, [ programWrapper.program ] ); 574 | }); 575 | 576 | } 577 | 578 | WebGLRenderingContextWrapper.prototype.attachShader = function(){ 579 | 580 | return arguments[ 0 ].attachShader( arguments[ 1 ] ); 581 | 582 | } 583 | 584 | WebGLRenderingContextWrapper.prototype.linkProgram = function(){ 585 | 586 | return this.run( 'linkProgram', arguments, _ => { 587 | return WebGLRenderingContext.prototype.linkProgram.apply( this.context, [ arguments[ 0 ].program ] ); 588 | }); 589 | } 590 | 591 | WebGLRenderingContextWrapper.prototype.getProgramParameter = function(){ 592 | 593 | return this.run( 'getProgramParameter', arguments, _ => { 594 | return WebGLRenderingContext.prototype.getProgramParameter.apply( this.context, [ arguments[ 0 ].program, arguments[ 1 ] ] ); 595 | }); 596 | 597 | } 598 | 599 | WebGLRenderingContextWrapper.prototype.getProgramInfoLog = function(){ 600 | 601 | return this.run( 'getProgramInfoLog', arguments, _ => { 602 | return WebGLRenderingContext.prototype.getProgramInfoLog.apply( this.context, [ arguments[ 0 ].program ] ); 603 | }); 604 | 605 | } 606 | 607 | WebGLRenderingContextWrapper.prototype.getActiveAttrib = function(){ 608 | 609 | return this.run( 'getActiveAttrib', arguments, _ => { 610 | return WebGLRenderingContext.prototype.getActiveAttrib.apply( this.context, [ arguments[ 0 ].program, arguments[ 1 ] ] ); 611 | }); 612 | 613 | } 614 | 615 | WebGLRenderingContextWrapper.prototype.getAttribLocation = function(){ 616 | 617 | return this.run( 'getAttribLocation', arguments, _ => { 618 | return WebGLRenderingContext.prototype.getAttribLocation.apply( this.context, [ arguments[ 0 ].program, arguments[ 1 ] ] ); 619 | }); 620 | 621 | } 622 | 623 | WebGLRenderingContextWrapper.prototype.bindAttribLocation = function(){ 624 | 625 | return this.run( 'bindAttribLocation', arguments, _ => { 626 | return WebGLRenderingContext.prototype.bindAttribLocation.apply( this.context, [ arguments[ 0 ].program, arguments[ 1 ], arguments[ 2 ] ] ); 627 | }); 628 | 629 | } 630 | 631 | WebGLRenderingContextWrapper.prototype.getActiveUniform = function(){ 632 | 633 | return this.run( 'getActiveUniform', arguments, _ => { 634 | return WebGLRenderingContext.prototype.getActiveUniform.apply( this.context, [ arguments[ 0 ].program, arguments[ 1 ] ] ); 635 | }); 636 | 637 | } 638 | 639 | WebGLRenderingContextWrapper.prototype.getUniformLocation = function(){ 640 | 641 | return this.run( 'getUniformLocation', arguments, _ => { 642 | return new WebGLUniformLocationWrapper( this.context, arguments[ 0 ], arguments[ 1 ] ); 643 | }); 644 | 645 | } 646 | 647 | WebGLRenderingContextWrapper.prototype.useProgram = function(){ 648 | 649 | this.useProgramCount++; 650 | return this.run( 'useProgram', arguments, _ => { 651 | return WebGLRenderingContext.prototype.useProgram.apply( this.context, [ arguments[ 0 ] ? arguments[ 0 ].program : null ] ); 652 | }); 653 | 654 | } 655 | 656 | var methods = [ 657 | 'uniform1f', 'uniform1fv', 'uniform1i', 'uniform1iv', 658 | 'uniform2f', 'uniform2fv', 'uniform2i', 'uniform2iv', 659 | 'uniform3f', 'uniform3fv', 'uniform3i', 'uniform3iv', 660 | 'uniform4f', 'uniform4fv', 'uniform4i', 'uniform4iv', 661 | 'uniformMatrix2fv', 'uniformMatrix3fv', 'uniformMatrix4fv' 662 | ]; 663 | 664 | var originalMethods = {}; 665 | 666 | methods.forEach( method => { 667 | 668 | var original = WebGLRenderingContext.prototype[ method ]; 669 | originalMethods[ method ] = original; 670 | 671 | WebGLRenderingContextWrapper.prototype[ method ] = function(){ 672 | 673 | var args = new Array(arguments.length); 674 | for (var i = 0, l = arguments.length; i < l; i++){ 675 | args[i] = arguments[i]; 676 | } 677 | if( !args[ 0 ] ) return; 678 | args[ 0 ] = args[ 0 ].uniformLocation; 679 | return this.run( method, args, _ => { 680 | return original.apply( this.context, args ); 681 | }); 682 | 683 | } 684 | 685 | }); 686 | 687 | } 688 | 689 | function WebGLTimerQueryEXTWrapper( contextWrapper, extension ){ 690 | 691 | this.contextWrapper = contextWrapper; 692 | this.extension = extension; 693 | this.query = this.extension.createQueryEXT(); 694 | this.time = 0; 695 | this.available = false; 696 | this.nested = []; 697 | 698 | } 699 | 700 | WebGLTimerQueryEXTWrapper.prototype.getTimes = function(){ 701 | 702 | var time = this.getTime(); 703 | this.nested.forEach( q => { 704 | time += q.getTimes(); 705 | }); 706 | 707 | return time; 708 | 709 | } 710 | 711 | WebGLTimerQueryEXTWrapper.prototype.getTime = function(){ 712 | 713 | this.time = this.extension.getQueryObjectEXT( this.query, this.extension.QUERY_RESULT_EXT ); 714 | 715 | return this.time; 716 | 717 | } 718 | 719 | WebGLTimerQueryEXTWrapper.prototype.getResultsAvailable = function(){ 720 | 721 | var res = true; 722 | this.nested.forEach( q => { 723 | res = res && q.getResultsAvailable(); 724 | }); 725 | 726 | return res; 727 | 728 | } 729 | 730 | WebGLTimerQueryEXTWrapper.prototype.getResultsAvailable = function(){ 731 | 732 | this.available = this.extension.getQueryObjectEXT( this.query, this.extension.QUERY_RESULT_AVAILABLE_EXT ); 733 | return this.available; 734 | 735 | } 736 | 737 | function EXTDisjointTimerQueryExtensionWrapper( contextWrapper ){ 738 | 739 | this.contextWrapper = contextWrapper; 740 | this.extension = WebGLRenderingContext.prototype.getExtension.apply( this.contextWrapper.context, [ 'EXT_disjoint_timer_query' ] ); 741 | 742 | this.QUERY_COUNTER_BITS_EXT = this.extension.QUERY_COUNTER_BITS_EXT; 743 | this.CURRENT_QUERY_EXT = this.extension.CURRENT_QUERY_EXT; 744 | this.QUERY_RESULT_AVAILABLE_EXT = this.extension.QUERY_RESULT_AVAILABLE_EXT; 745 | this.GPU_DISJOINT_EXT = this.extension.GPU_DISJOINT_EXT; 746 | this.QUERY_RESULT_EXT = this.extension.QUERY_RESULT_EXT; 747 | this.TIME_ELAPSED_EXT = this.extension.TIME_ELAPSED_EXT; 748 | this.TIMESTAMP_EXT = this.extension.TIMESTAMP_EXT; 749 | 750 | } 751 | 752 | EXTDisjointTimerQueryExtensionWrapper.prototype.createQueryEXT = function(){ 753 | 754 | return new WebGLTimerQueryEXTWrapper( this.contextWrapper, this.extension ); 755 | 756 | } 757 | 758 | EXTDisjointTimerQueryExtensionWrapper.prototype.beginQueryEXT = function( type, query ){ 759 | 760 | if( this.contextWrapper.activeQuery ){ 761 | this.extension.endQueryEXT( type ); 762 | this.contextWrapper.activeQuery.nested.push( query ); 763 | this.contextWrapper.queryStack.push( this.contextWrapper.activeQuery ); 764 | } 765 | 766 | this.contextWrapper.activeQuery = query; 767 | 768 | return this.extension.beginQueryEXT( type, query.query ); 769 | 770 | } 771 | 772 | EXTDisjointTimerQueryExtensionWrapper.prototype.endQueryEXT = function( type ){ 773 | 774 | this.contextWrapper.activeQuery = this.contextWrapper.queryStack.pop(); 775 | var res = this.extension.endQueryEXT( type ); 776 | if( this.contextWrapper.activeQuery ){ 777 | var newQuery = new WebGLTimerQueryEXTWrapper( this.contextWrapper, this.extension ); 778 | this.contextWrapper.activeQuery.nested.push( newQuery ); 779 | this.extension.beginQueryEXT( type, newQuery.query ); 780 | } 781 | return res; 782 | 783 | } 784 | 785 | EXTDisjointTimerQueryExtensionWrapper.prototype.getQueryObjectEXT = function( query, pname ){ 786 | 787 | if( pname === this.extension.QUERY_RESULT_AVAILABLE_EXT ){ 788 | return query.getResultsAvailable(); 789 | } 790 | 791 | if( pname === this.extension.QUERY_RESULT_EXT ){ 792 | return query.getTimes(); 793 | } 794 | 795 | return this.extension.getQueryObjectEXT( query.query, pname ); 796 | 797 | } 798 | 799 | EXTDisjointTimerQueryExtensionWrapper.prototype.getQueryEXT = function( target, pname ){ 800 | 801 | return this.extension.getQueryEXT( target, pname ); 802 | 803 | } 804 | 805 | // 806 | // This is the UI 807 | // 808 | 809 | var text; 810 | var uiIsSetup = false; 811 | 812 | function setupUI() { 813 | 814 | if( uiIsSetup ) return; 815 | uiIsSetup = true; 816 | 817 | text = document.createElement( 'div' ); 818 | text.setAttribute( 'id', 'perfmeter-panel' ); 819 | 820 | var fileref = document.createElement("link"); 821 | fileref.rel = "stylesheet"; 822 | fileref.type = "text/css"; 823 | fileref.href = settings.cssPath; 824 | 825 | window.document.getElementsByTagName("head")[0].appendChild(fileref); 826 | 827 | if( !window.document.body ) { 828 | window.addEventListener( 'load', function() { 829 | window.document.body.appendChild( text ); 830 | } ); 831 | } else { 832 | window.document.body.appendChild( text ); 833 | } 834 | 835 | } 836 | 837 | // 838 | // This is the rAF queue processing 839 | // 840 | 841 | var originalRequestAnimationFrame = window.requestAnimationFrame; 842 | var rAFQueue = []; 843 | var frameCount = 0; 844 | var frameId = 0; 845 | var framerate = 0; 846 | var lastTime = 0; 847 | 848 | window.requestAnimationFrame = function( c ){ 849 | 850 | rAFQueue.push( c ); 851 | 852 | } 853 | 854 | function processRequestAnimationFrames( timestamp ){ 855 | 856 | contexts.forEach( ctx => { 857 | 858 | ctx.contextWrapper.resetFrame(); 859 | 860 | var ext = ctx.queryExt; 861 | 862 | if( ext ){ 863 | 864 | var query = ext.createQueryEXT(); 865 | ext.beginQueryEXT( ext.TIME_ELAPSED_EXT, query ); 866 | ctx.extQueries.push( query ); 867 | 868 | } 869 | 870 | }); 871 | 872 | var startTime = performance.now(); 873 | 874 | var queue = rAFQueue.slice( 0 ); 875 | rAFQueue.length = 0; 876 | queue.forEach( rAF => { 877 | rAF( timestamp ); 878 | }); 879 | 880 | var endTime = performance.now(); 881 | var frameTime = endTime - startTime; 882 | 883 | frameCount++; 884 | if( endTime > lastTime + 1000 ) { 885 | framerate = frameCount * 1000 / ( endTime - lastTime ); 886 | frameCount = 0; 887 | lastTime = endTime; 888 | } 889 | 890 | frameId++; 891 | 892 | var logs = []; 893 | 894 | contexts.forEach( ctx => { 895 | 896 | var ext = ctx.queryExt; 897 | 898 | if( ext ){ 899 | 900 | ext.endQueryEXT( ext.TIME_ELAPSED_EXT ); 901 | 902 | ctx.extQueries.forEach( ( query, i ) => { 903 | 904 | var available = ext.getQueryObjectEXT( query, ext.QUERY_RESULT_AVAILABLE_EXT ); 905 | var disjoint = ctx.contextWrapper.context.getParameter( ext.GPU_DISJOINT_EXT ); 906 | 907 | if (available && !disjoint){ 908 | 909 | var queryTime = ext.getQueryObjectEXT( query, ext.QUERY_RESULT_EXT ); 910 | var time = queryTime; 911 | if (ctx.contextWrapper.count ){ 912 | logs.push( { 913 | id: ctx.contextWrapper.id, 914 | count: ctx.contextWrapper.count, 915 | time: ( time / 1000000 ).toFixed( 2 ), 916 | jstime: ctx.contextWrapper.JavaScriptTime.toFixed(2), 917 | drawArrays: ctx.contextWrapper.drawArrayCalls, 918 | drawElements: ctx.contextWrapper.drawElementsCalls, 919 | points: ctx.contextWrapper.pointsCount, 920 | lines: ctx.contextWrapper.linesCount, 921 | triangles: ctx.contextWrapper.trianglesCount, 922 | programs: ctx.contextWrapper.programCount, 923 | usePrograms: ctx.contextWrapper.useProgramCount 924 | } ); 925 | } 926 | ctx.extQueries.splice( i, 1 ); 927 | 928 | } 929 | 930 | }); 931 | 932 | /*ctx.contextWrapper.drawQueries.forEach( ( query, i ) => { 933 | 934 | var available = ext.getQueryObjectEXT( query, ext.QUERY_RESULT_AVAILABLE_EXT ); 935 | var disjoint = ctx.contextWrapper.context.getParameter( ext.GPU_DISJOINT_EXT ); 936 | 937 | if (available && !disjoint){ 938 | 939 | var queryTime = ext.getQueryObjectEXT( query, ext.QUERY_RESULT_EXT ); 940 | var time = queryTime; 941 | if (ctx.contextWrapper.count ){ 942 | log( 'Draw ', time ); 943 | } 944 | ctx.contextWrapper.drawQueries.splice( i, 1 ); 945 | 946 | } 947 | 948 | });*/ 949 | 950 | } 951 | 952 | }); 953 | 954 | var str = `Framerate: ${framerate.toFixed(2)} FPS 955 | Frame JS time: ${frameTime.toFixed(2)} ms 956 | 957 | `; 958 | logs.forEach( l => { 959 | str += `Canvas 960 | ID: ${l.id} 961 | Count: ${l.count} 962 | Canvas time: ${l.jstime} ms 963 | WebGL 964 | GPU time: ${l.time} ms 965 | Programs: ${l.programs} 966 | usePrograms: ${l.usePrograms} 967 | dArrays: ${l.drawArrays} 968 | dElems: ${l.drawElements} 969 | Points: ${l.points} 970 | Lines: ${l.lines} 971 | Triangles: ${l.triangles} 972 | 973 | `; 974 | }); 975 | if( text ) text.innerHTML = str; 976 | 977 | originalRequestAnimationFrame( processRequestAnimationFrames ); 978 | 979 | } 980 | 981 | processRequestAnimationFrames(); 982 | 983 | })(); 984 | -------------------------------------------------------------------------------- /src/extensions/ANGLEInstancedArraysExtensionWrapper.js: -------------------------------------------------------------------------------- 1 | import{ Wrapper } from "../Wrapper"; 2 | 3 | function ANGLEInstancedArraysExtensionWrapper( contextWrapper ) { 4 | 5 | Wrapper.call( this ); 6 | 7 | this.contextWrapper = contextWrapper; 8 | this.extension = WebGLRenderingContext.prototype.getExtension.apply( this.contextWrapper.context, [ 'ANGLE_instanced_arrays' ] ); 9 | 10 | } 11 | 12 | ANGLEInstancedArraysExtensionWrapper.prototype = Object.create( Wrapper.prototype ); 13 | 14 | ANGLEInstancedArraysExtensionWrapper.prototype.drawArraysInstancedANGLE = function() { 15 | 16 | this.contextWrapper.instancedDrawArraysCalls++; 17 | this.contextWrapper.updateInstancedDrawCount( arguments[ 0 ], arguments[ 2 ] * arguments[ 3 ] ); 18 | return this.contextWrapper.run( 'drawArraysInstancedANGLE', arguments, _ => { 19 | return this.extension.drawArraysInstancedANGLE.apply( this.extension, arguments ); 20 | } ); 21 | 22 | } 23 | 24 | ANGLEInstancedArraysExtensionWrapper.prototype.drawElementsInstancedANGLE = function() { 25 | 26 | this.contextWrapper.instancedDrawElementsCalls++; 27 | this.contextWrapper.updateInstancedDrawCount( arguments[ 0 ], arguments[ 1 ] * arguments[ 4 ] ); 28 | return this.contextWrapper.run( 'drawElementsInstancedANGLE', arguments, _ => { 29 | return this.extension.drawElementsInstancedANGLE.apply( this.extension, arguments ); 30 | } ); 31 | 32 | } 33 | 34 | ANGLEInstancedArraysExtensionWrapper.prototype.vertexAttribDivisorANGLE = function() { 35 | 36 | return this.extension.vertexAttribDivisorANGLE.apply( this.extension, arguments ); 37 | 38 | } 39 | 40 | export { ANGLEInstancedArraysExtensionWrapper }; 41 | -------------------------------------------------------------------------------- /src/extensions/EXTDisjointTimerQueryExtensionWrapper.js: -------------------------------------------------------------------------------- 1 | import{ Wrapper } from "../Wrapper"; 2 | 3 | function WebGLTimerQueryEXTWrapper( contextWrapper, extension ){ 4 | 5 | Wrapper.call( this ); 6 | 7 | this.contextWrapper = contextWrapper; 8 | this.extension = extension; 9 | this.query = this.extension.createQueryEXT(); 10 | this.time = -1; 11 | this.nestedTime = -1; 12 | this.available = false; 13 | this.nested = []; 14 | 15 | } 16 | 17 | WebGLTimerQueryEXTWrapper.prototype = Object.create( Wrapper.prototype ); 18 | 19 | WebGLTimerQueryEXTWrapper.prototype.getTimes = function(){ 20 | 21 | var time = this.getTime(); 22 | this.nested.forEach( q => { 23 | time += q.getTimes(); 24 | }); 25 | 26 | this.nestedTime = time; 27 | 28 | return time; 29 | 30 | } 31 | 32 | WebGLTimerQueryEXTWrapper.prototype.getTime = function(){ 33 | 34 | if( this.time !== -1 ) return this.time; 35 | 36 | this.time = this.extension.getQueryObjectEXT( this.query, this.extension.QUERY_RESULT_EXT ); 37 | return this.time; 38 | 39 | } 40 | 41 | WebGLTimerQueryEXTWrapper.prototype.getResultsAvailable = function(){ 42 | 43 | var res = true; 44 | this.nested.forEach( q => { 45 | res = res && q.getResultsAvailable(); 46 | }); 47 | 48 | return res; 49 | 50 | } 51 | 52 | WebGLTimerQueryEXTWrapper.prototype.getResultsAvailable = function(){ 53 | 54 | if( this.available ) return true; 55 | 56 | this.available = this.extension.getQueryObjectEXT( this.query, this.extension.QUERY_RESULT_AVAILABLE_EXT ); 57 | return this.available; 58 | 59 | } 60 | 61 | function EXTDisjointTimerQueryExtensionWrapper( contextWrapper ){ 62 | 63 | Wrapper.call( this ); 64 | 65 | this.contextWrapper = contextWrapper; 66 | this.extension = WebGLRenderingContext.prototype.getExtension.apply( this.contextWrapper.context, [ 'EXT_disjoint_timer_query' ] ); 67 | 68 | this.QUERY_COUNTER_BITS_EXT = this.extension.QUERY_COUNTER_BITS_EXT; 69 | this.CURRENT_QUERY_EXT = this.extension.CURRENT_QUERY_EXT; 70 | this.QUERY_RESULT_AVAILABLE_EXT = this.extension.QUERY_RESULT_AVAILABLE_EXT; 71 | this.GPU_DISJOINT_EXT = this.extension.GPU_DISJOINT_EXT; 72 | this.QUERY_RESULT_EXT = this.extension.QUERY_RESULT_EXT; 73 | this.TIME_ELAPSED_EXT = this.extension.TIME_ELAPSED_EXT; 74 | this.TIMESTAMP_EXT = this.extension.TIMESTAMP_EXT; 75 | 76 | } 77 | 78 | EXTDisjointTimerQueryExtensionWrapper.prototype = Object.create( Wrapper.prototype ); 79 | 80 | EXTDisjointTimerQueryExtensionWrapper.prototype.createQueryEXT = function(){ 81 | 82 | return new WebGLTimerQueryEXTWrapper( this.contextWrapper, this.extension ); 83 | 84 | } 85 | 86 | EXTDisjointTimerQueryExtensionWrapper.prototype.beginQueryEXT = function( type, query ){ 87 | 88 | if( this.contextWrapper.activeQuery ){ 89 | this.extension.endQueryEXT( type ); 90 | this.contextWrapper.activeQuery.nested.push( query ); 91 | this.contextWrapper.queryStack.push( this.contextWrapper.activeQuery ); 92 | } 93 | 94 | this.contextWrapper.activeQuery = query; 95 | 96 | return this.extension.beginQueryEXT( type, query.query ); 97 | 98 | } 99 | 100 | EXTDisjointTimerQueryExtensionWrapper.prototype.endQueryEXT = function( type ){ 101 | 102 | this.contextWrapper.activeQuery = this.contextWrapper.queryStack.pop(); 103 | var res = this.extension.endQueryEXT( type ); 104 | if( this.contextWrapper.activeQuery ){ 105 | var newQuery = new WebGLTimerQueryEXTWrapper( this.contextWrapper, this.extension ); 106 | this.contextWrapper.activeQuery.nested.push( newQuery ); 107 | this.extension.beginQueryEXT( type, newQuery.query ); 108 | } 109 | return res; 110 | 111 | } 112 | 113 | EXTDisjointTimerQueryExtensionWrapper.prototype.getQueryObjectEXT = function( query, pname ){ 114 | 115 | if( pname === this.extension.QUERY_RESULT_AVAILABLE_EXT ){ 116 | return query.getResultsAvailable(); 117 | } 118 | 119 | if( pname === this.extension.QUERY_RESULT_EXT ){ 120 | return query.getTimes(); 121 | } 122 | 123 | return this.extension.getQueryObjectEXT( query.query, pname ); 124 | 125 | } 126 | 127 | EXTDisjointTimerQueryExtensionWrapper.prototype.getQueryEXT = function( target, pname ){ 128 | 129 | return this.extension.getQueryEXT( target, pname ); 130 | 131 | } 132 | 133 | export { EXTDisjointTimerQueryExtensionWrapper }; 134 | -------------------------------------------------------------------------------- /src/extensions/WebGLDebugShadersExtensionWrapper.js: -------------------------------------------------------------------------------- 1 | import{ Wrapper } from "../Wrapper"; 2 | 3 | function WebGLDebugShadersExtensionWrapper( contextWrapper ){ 4 | 5 | Wrapper.call( this ); 6 | 7 | this.contextWrapper = contextWrapper; 8 | this.extension = WebGLRenderingContext.prototype.getExtension.apply( this.contextWrapper.context, [ 'WEBGL_debug_shaders' ] ); 9 | 10 | } 11 | 12 | WebGLDebugShadersExtensionWrapper.prototype = Object.create( Wrapper.prototype ); 13 | 14 | WebGLDebugShadersExtensionWrapper.prototype.getTranslatedShaderSource = function( shaderWrapper ){ 15 | 16 | return this.extension.getTranslatedShaderSource( shaderWrapper.shader ); 17 | 18 | } 19 | 20 | export { WebGLDebugShadersExtensionWrapper }; 21 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import{ Wrapper } from "./Wrapper"; 2 | 3 | import { CanvasRenderingContext2DWrapper } from "./CanvasRenderingContext2DWrapper" 4 | import { WebGLRenderingContextWrapper } from "./WebGLRenderingContextWrapper" 5 | 6 | import { setInfo } from "./widget"; 7 | 8 | window.addEventListener( 'perfmeter-settings', e => { 9 | settings = e.detail; 10 | } ); 11 | 12 | var glInfo = { 13 | versions: [], 14 | WebGLAvailable: 'WebGLRenderingContext' in window, 15 | WebGL2Available: 'WebGL2RenderingContext' in window 16 | }; 17 | 18 | var getGLInfo = function( context ) { 19 | var gl = document.createElement( 'canvas' ).getContext( context ); 20 | if( !gl ) return; 21 | var debugInfo = gl.getExtension( 'WEBGL_debug_renderer_info' ); 22 | var version = { 23 | type: context, 24 | vendor: gl.getParameter( debugInfo.UNMASKED_VENDOR_WEBGL ), 25 | renderer: gl.getParameter( debugInfo.UNMASKED_RENDERER_WEBGL ), 26 | glVersion: gl.getParameter( gl.VERSION ), 27 | glslVersion: gl.getParameter( gl.SHADING_LANGUAGE_VERSION ) 28 | }; 29 | glInfo.versions.push( version ); 30 | }; 31 | 32 | getGLInfo( 'webgl' ); 33 | getGLInfo( 'webgl2' ); 34 | 35 | var webGLInfo = ''; 36 | glInfo.versions.forEach( v => { 37 | var glInfo = `GL Version: ${v.glVersion} 38 | GLSL Version: ${v.glslVersion} 39 | Vendor: ${v.vendor} 40 | Renderer: ${v.renderer} 41 | `; 42 | webGLInfo += glInfo; 43 | } ); 44 | 45 | setInfo( webGLInfo ); 46 | 47 | function FrameData( id ){ 48 | 49 | this.frameId = id; 50 | 51 | this.framerate = 0; 52 | this.frameTime = 0; 53 | this.JavaScriptTime = 0; 54 | 55 | this.contexts = new Map(); 56 | 57 | } 58 | 59 | function ContextFrameData( type ){ 60 | 61 | this.type = type; 62 | 63 | this.JavaScriptTime = 0; 64 | this.GPUTime = 0; 65 | this.log = []; 66 | 67 | this.createProgram = 0; 68 | this.createTexture = 0; 69 | 70 | this.useProgram = 0; 71 | this.bindTexture = 0; 72 | 73 | this.triangles = 0; 74 | this.lines = 0; 75 | this.points = 0; 76 | 77 | this.startTime = 0; 78 | 79 | } 80 | 81 | function ContextData( contextWrapper ){ 82 | 83 | Wrapper.call( this ); 84 | 85 | this.queryExt = null; 86 | this.contextWrapper = contextWrapper; 87 | this.extQueries = []; 88 | 89 | this.metrics = {} 90 | 91 | } 92 | 93 | ContextData.prototype = Object.create( Wrapper.prototype ); 94 | 95 | var contexts = []; 96 | var canvasContexts = new WeakMap(); 97 | 98 | var getContext = HTMLCanvasElement.prototype.getContext; 99 | 100 | HTMLCanvasElement.prototype.getContext = function(){ 101 | 102 | var c = canvasContexts.get( this ); 103 | if( c ){ 104 | log( arguments, '(CACHED)' ); 105 | return c; 106 | } else { 107 | log( arguments ); 108 | } 109 | 110 | var context = getContext.apply( this, arguments ); 111 | 112 | if( arguments[ 0 ] === 'webgl' || arguments[ 0 ] === 'experimental-webgl' ){ 113 | 114 | var wrapper = new WebGLRenderingContextWrapper( context ); 115 | wrapper.canvas = this; 116 | var cData = new ContextData( wrapper ); 117 | cData.queryExt = wrapper.getExtension( 'EXT_disjoint_timer_query' ); 118 | wrapper.queryExt = cData.queryExt; 119 | contexts.push( cData ); 120 | canvasContexts.set( this, wrapper ); 121 | return wrapper; 122 | 123 | } 124 | 125 | if( arguments[ 0 ] === '2d' ){ 126 | 127 | var wrapper = new CanvasRenderingContext2DWrapper( context ); 128 | wrapper.canvas = this; 129 | var cData = new ContextData( wrapper ); 130 | contexts.push( cData ); 131 | canvasContexts.set( this, wrapper ); 132 | return wrapper; 133 | 134 | } 135 | 136 | canvasContexts.set( this, context ); 137 | return context; 138 | 139 | } 140 | 141 | // 142 | // This is the rAF queue processing 143 | // 144 | 145 | var originalRequestAnimationFrame = window.requestAnimationFrame; 146 | var rAFQueue = []; 147 | var frameCount = 0; 148 | var frameId = 0; 149 | var framerate = 0; 150 | var lastTime = 0; 151 | 152 | window.requestAnimationFrame = function( c ){ 153 | 154 | rAFQueue.push( c ); 155 | 156 | } 157 | 158 | function processRequestAnimationFrames( timestamp ){ 159 | 160 | contexts.forEach( ctx => { 161 | 162 | ctx.contextWrapper.setFrameId( frameId ); 163 | ctx.contextWrapper.resetFrame(); 164 | 165 | var ext = ctx.queryExt; 166 | 167 | if( ext ){ 168 | 169 | var query = ext.createQueryEXT(); 170 | ext.beginQueryEXT( ext.TIME_ELAPSED_EXT, query ); 171 | ctx.extQueries.push({ 172 | query, 173 | frameId 174 | }); 175 | 176 | } 177 | 178 | }); 179 | 180 | var startTime = performance.now(); 181 | 182 | var queue = rAFQueue.slice( 0 ); 183 | rAFQueue.length = 0; 184 | queue.forEach( rAF => { 185 | rAF( timestamp ); 186 | }); 187 | 188 | var endTime = performance.now(); 189 | var frameTime = endTime - startTime; 190 | 191 | frameCount++; 192 | if( endTime > lastTime + 1000 ) { 193 | framerate = frameCount * 1000 / ( endTime - lastTime ); 194 | frameCount = 0; 195 | lastTime = endTime; 196 | } 197 | 198 | frameId++; 199 | 200 | contexts.forEach( ctx => { 201 | 202 | var ext = ctx.queryExt; 203 | 204 | if( ext ){ 205 | 206 | ext.endQueryEXT( ext.TIME_ELAPSED_EXT ); 207 | 208 | ctx.extQueries.forEach( ( query, i ) => { 209 | 210 | var available = ext.getQueryObjectEXT( query.query, ext.QUERY_RESULT_AVAILABLE_EXT ); 211 | var disjoint = ctx.contextWrapper.context.getParameter( ext.GPU_DISJOINT_EXT ); 212 | 213 | if (available && !disjoint){ 214 | 215 | var queryTime = ext.getQueryObjectEXT( query.query, ext.QUERY_RESULT_EXT ); 216 | var time = queryTime; 217 | 218 | var wrapper = ctx.contextWrapper; 219 | 220 | ctx.metrics = { 221 | uuid: wrapper.uuid, 222 | textureMemory: wrapper.getTextureMemory(), 223 | bufferMemory: wrapper.getBufferMemory(), 224 | count: wrapper.count, 225 | time: ( time / 1000000 ).toFixed( 2 ), 226 | jstime: wrapper.JavaScriptTime.toFixed(2), 227 | drawArrays: wrapper.drawArraysCalls, 228 | drawElements: wrapper.drawElementsCalls, 229 | instancedDrawArrays: wrapper.instancedDrawArraysCalls, 230 | instancedDrawElements: wrapper.instancedDrawElementsCalls, 231 | points: wrapper.pointsCount, 232 | lines: wrapper.linesCount, 233 | triangles: wrapper.trianglesCount, 234 | instancedPoints: wrapper.instancedPointsCount, 235 | instancedLines: wrapper.instancedLinesCount, 236 | instancedTriangles: wrapper.instancedTrianglesCount, 237 | programs: wrapper.programCount, 238 | usePrograms: wrapper.useProgramCount, 239 | textures: wrapper.textureCount, 240 | bindTextures: wrapper.bindTextureCount, 241 | framebuffers: wrapper.framebufferCount, 242 | bindFramebuffers: wrapper.bindFramebufferCount 243 | }; 244 | 245 | ctx.extQueries.splice( i, 1 ); 246 | 247 | } 248 | 249 | }); 250 | 251 | ctx.metrics.shaderTime = {}; 252 | 253 | ctx.contextWrapper.drawQueries.forEach( ( query, i ) => { 254 | 255 | var available = ext.getQueryObjectEXT( query.query, ext.QUERY_RESULT_AVAILABLE_EXT ); 256 | var disjoint = ctx.contextWrapper.context.getParameter( ext.GPU_DISJOINT_EXT ); 257 | 258 | if (available && !disjoint){ 259 | 260 | var queryTime = ext.getQueryObjectEXT( query.query, ext.QUERY_RESULT_EXT ); 261 | var time = queryTime; 262 | if( ctx.metrics.shaderTime[ query.program.uuid ] === undefined ) { 263 | ctx.metrics.shaderTime[ query.program.uuid ] = 0; 264 | } 265 | ctx.metrics.shaderTime[ query.program.uuid ] += time; 266 | ctx.contextWrapper.drawQueries.splice( i, 1 ); 267 | 268 | } 269 | 270 | }); 271 | 272 | } 273 | 274 | }); 275 | 276 | var logs = []; 277 | contexts.forEach( ctx => { 278 | logs.push( ctx.metrics ) 279 | } ); 280 | 281 | var e = new CustomEvent( 'perfmeter-framedata', { 282 | detail: { 283 | rAFS: queue.length, 284 | frameId, 285 | framerate, 286 | frameTime, 287 | logs 288 | } 289 | } ); 290 | window.dispatchEvent( e ); 291 | 292 | originalRequestAnimationFrame( processRequestAnimationFrames ); 293 | 294 | } 295 | 296 | processRequestAnimationFrames(); 297 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "babel-cli": "^6.16.0", 4 | "babel-preset-es2015": "^6.16.0", 5 | "babelify": "^7.3.0", 6 | "browserify": "^13.1.0", 7 | "watchify": "^3.7.0" 8 | }, 9 | "scripts": { 10 | "watch": "watchify main.js -o lib.js -t [ babelify --presets [ es2015 ] ]", 11 | "build": "browserify main.js -o lib.js -t [ babelify --presets [ es2015 ] ]" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | function createUUID(){ 2 | 3 | function s4(){ 4 | return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); 5 | } 6 | 7 | return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`; 8 | 9 | } 10 | 11 | export { createUUID }; 12 | -------------------------------------------------------------------------------- /src/widget.js: -------------------------------------------------------------------------------- 1 | const text = document.createElement( 'div' ); 2 | text.setAttribute( 'id', 'perfmeter-panel' ); 3 | 4 | function setupUI() { 5 | 6 | /*const fileref = document.createElement("link"); 7 | fileref.rel = "stylesheet"; 8 | fileref.type = "text/css"; 9 | fileref.href = settings.cssPath; 10 | 11 | window.document.getElementsByTagName("head")[0].appendChild(fileref);*/ 12 | 13 | var node = window.document.createElement('style'); 14 | node.innerHTML = settings.stylesheet; 15 | window.document.body.appendChild(node); 16 | 17 | window.document.body.appendChild( text ); 18 | 19 | window.addEventListener( 'perfmeter-framedata', updateUI ); 20 | 21 | } 22 | 23 | if( !window.document.body ) { 24 | window.addEventListener( 'load', setupUI ); 25 | } else { 26 | setupUI(); 27 | } 28 | 29 | function formatNumber( value, sizes, decimals ) { 30 | 31 | if(value == 0) return '0'; 32 | 33 | var k = 1000; // or 1024 for binary 34 | var dm = decimals || 2; 35 | var i = Math.floor(Math.log(value) / Math.log(k)); 36 | 37 | return parseFloat((value / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; 38 | 39 | } 40 | 41 | var timeSizes = ['ns', 'µs', 'ms', 's' ]; 42 | var callSizes = [ '', 'K', 'M', 'G' ]; 43 | var memorySizes = [ 'Bytes', 'KB', 'MB', 'GB', 'TB' ]; 44 | 45 | function updateUI( e ) { 46 | 47 | const d = e.detail; 48 | 49 | if( d.rAFS === 0 || d.logs.length === 0 ) { 50 | text.style.display = 'none'; 51 | } else { 52 | text.style.display = 'block'; 53 | } 54 | 55 | const blocks = []; 56 | 57 | blocks.push( `Framerate: ${d.framerate.toFixed(2)} FPS 58 | Frame JS time: ${d.frameTime.toFixed(2)} ms 59 | rAFS: ${d.rAFS}` ); 60 | 61 | d.logs.forEach( l => { 62 | 63 | if( l.count ) { 64 | 65 | var shaderTime = []; 66 | Object.keys( l.shaderTime ).forEach( key => { 67 | shaderTime.push( `${key} ${( l.shaderTime[ key ] / 1000000 ).toFixed(2)} ms` ); 68 | } ); 69 | var shaderTimeStr = shaderTime.join( "\r\n" ); 70 | 71 | blocks.push( `Canvas 72 | ID: ${l.uuid} 73 | Count: ${l.count} 74 | Canvas time: ${l.jstime} ms 75 | WebGL 76 | TexMem: ${formatNumber(l.textureMemory,memorySizes,2)} 77 | BufMem: ${formatNumber(l.bufferMemory,memorySizes,2)} 78 | Total: ${formatNumber(l.textureMemory+l.bufferMemory,memorySizes,2)} 79 | GPU time: ${l.time} ms 80 | Shader time: 81 | ${shaderTimeStr} 82 | Programs: ${l.usePrograms} / ${l.programs} 83 | Textures: ${l.bindTextures} / ${l.textures} 84 | Framebuffers: ${l.bindFramebuffers} / ${l.framebuffers} 85 | dArrays: ${l.drawArrays} 86 | dElems: ${l.drawElements} 87 | Points: ${l.points} 88 | Lines: ${l.lines} 89 | Triangles: ${l.triangles} 90 | idArrays: ${l.instancedDrawArrays} 91 | idElems: ${l.instancedDrawElements} 92 | iPoints: ${l.instancedPoints} 93 | iLines: ${l.instancedLines} 94 | iTriangles: ${l.instancedTriangles}` ); 95 | } 96 | 97 | }); 98 | 99 | blocks.push( `Browser 100 | Mem: ${(performance.memory.usedJSHeapSize/(1024*1024)).toFixed(2)}/${(performance.memory.totalJSHeapSize/(1024*1024)).toFixed(2)}` ); 101 | 102 | if( settings.showGPUInfo ) blocks.push( glInfo ); 103 | 104 | text.innerHTML = blocks.join( "\r\n\r\n" ); 105 | 106 | } 107 | 108 | var glInfo = ''; 109 | 110 | function setInfo( info ) { 111 | 112 | glInfo = info; 113 | 114 | } 115 | 116 | export { setInfo } 117 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | function throttle(fn, threshold, scope) { 2 | threshold || (threshold = 250); 3 | var last, 4 | deferTimer; 5 | return function () { 6 | var context = scope || this; 7 | 8 | var now = +new Date, 9 | args = arguments; 10 | if (last && now < last + threshold) { 11 | clearTimeout(deferTimer); 12 | deferTimer = setTimeout(function () { 13 | last = now; 14 | fn.apply(context, args); 15 | }, threshold); 16 | } else { 17 | last = now; 18 | fn.apply(context, args); 19 | } 20 | }; 21 | } 22 | 23 | function debounce(fn, delay) { 24 | var timer = null; 25 | return function () { 26 | var context = this, args = arguments; 27 | clearTimeout(timer); 28 | timer = setTimeout(function () { 29 | fn.apply(context, args); 30 | }, delay); 31 | }; 32 | } 33 | 34 | var ge = (function(){ 35 | 36 | var map = new Map(); 37 | 38 | return function( id ) { 39 | var el = map.get( id ); 40 | if( el ) { 41 | } else { 42 | el = document.getElementById( id ); 43 | map.set( id, el ); 44 | } 45 | return el; 46 | } 47 | 48 | })(); 49 | --------------------------------------------------------------------------------