Web Inspector: [WebGL] Allow collecting calls for Resource objects affecting their...
[WebKit-https.git] / Source / WebCore / inspector / InjectedScriptWebGLModuleSource.js
1 /*
2  * Copyright (C) 2012 Google Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are
6  * met:
7  *
8  *     * Redistributions of source code must retain the above copyright
9  * notice, this list of conditions and the following disclaimer.
10  *     * Redistributions in binary form must reproduce the above
11  * copyright notice, this list of conditions and the following disclaimer
12  * in the documentation and/or other materials provided with the
13  * distribution.
14  *     * Neither the name of Google Inc. nor the names of its
15  * contributors may be used to endorse or promote products derived from
16  * this software without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  */
30
31 /**
32  * @param {InjectedScriptHost} InjectedScriptHost
33  * @param {Window} inspectedWindow
34  * @param {number} injectedScriptId
35  */
36 (function (InjectedScriptHost, inspectedWindow, injectedScriptId) {
37
38 var TypeUtils = {
39     /**
40      * http://www.khronos.org/registry/typedarray/specs/latest/#7
41      * @type {Array.<Function>}
42      */
43     typedArrayClasses: (function(typeNames) {
44         var result = [];
45         for (var i = 0, n = typeNames.length; i < n; ++i) {
46             if (inspectedWindow[typeNames[i]])
47                 result.push(inspectedWindow[typeNames[i]]);
48         }
49         return result;
50     })(["Int8Array", "Uint8Array", "Uint8ClampedArray", "Int16Array", "Uint16Array", "Int32Array", "Uint32Array", "Float32Array", "Float64Array"]),
51
52     /**
53      * @param {*} array
54      * @return {Function}
55      */
56     typedArrayClass: function(array)
57     {
58         var classes = TypeUtils.typedArrayClasses;
59         for (var i = 0, n = classes.length; i < n; ++i) {
60             if (array instanceof classes[i])
61                 return classes[i];
62         }
63         return null;
64     },
65
66     /**
67      * @param {*} obj
68      * @return {*}
69      * FIXME: suppress checkTypes due to outdated builtin externs for CanvasRenderingContext2D and ImageData
70      * @suppress {checkTypes}
71      */
72     clone: function(obj)
73     {
74         if (!obj)
75             return obj;
76
77         var type = typeof obj;
78         if (type !== "object" && type !== "function")
79             return obj;
80
81         // Handle Array and ArrayBuffer instances.
82         if (typeof obj.slice === "function") {
83             console.assert(obj instanceof Array || obj instanceof ArrayBuffer);
84             return obj.slice(0);
85         }
86
87         var typedArrayClass = TypeUtils.typedArrayClass(obj);
88         if (typedArrayClass)
89             return new typedArrayClass(obj);
90
91         if (obj instanceof HTMLImageElement)
92             return obj.cloneNode(true);
93
94         if (obj instanceof HTMLCanvasElement) {
95             var result = obj.cloneNode(true);
96             var context = result.getContext("2d");
97             context.drawImage(obj, 0, 0);
98             return result;
99         }
100
101         if (obj instanceof HTMLVideoElement) {
102             var result = obj.cloneNode(true);
103             // FIXME: Copy HTMLVideoElement's current image into a 2d canvas.
104             return result;
105         }
106
107         if (obj instanceof ImageData) {
108             var context = TypeUtils._dummyCanvas2dContext();
109             var result = context.createImageData(obj);
110             for (var i = 0, n = obj.data.length; i < n; ++i)
111               result.data[i] = obj.data[i];
112             return result;
113         }
114
115         console.error("ASSERT_NOT_REACHED: failed to clone object: ", obj);
116         return obj;
117     },
118
119     /**
120      * @return {CanvasRenderingContext2D}
121      */
122     _dummyCanvas2dContext: function()
123     {
124         var context = TypeUtils._dummyCanvas2dContext;
125         if (!context) {
126             var canvas = inspectedWindow.document.createElement("canvas");
127             context = canvas.getContext("2d");
128             var contextResource = Resource.forObject(context);
129             if (contextResource)
130                 context = contextResource.wrappedObject();
131             TypeUtils._dummyCanvas2dContext = context;
132         }
133         return context;
134     }
135 }
136
137 /**
138  * @constructor
139  */
140 function Cache()
141 {
142     this.reset();
143 }
144
145 Cache.prototype = {
146     /**
147      * @return {number}
148      */
149     size: function()
150     {
151         return this._size;
152     },
153
154     reset: function()
155     {
156         this._items = Object.create(null);
157         this._size = 0;
158     },
159
160     /**
161      * @param {number} key
162      * @return {boolean}
163      */
164     has: function(key)
165     {
166         return key in this._items;
167     },
168
169     /**
170      * @param {number} key
171      * @return {Object}
172      */
173     get: function(key)
174     {
175         return this._items[key];
176     },
177
178     /**
179      * @param {number} key
180      * @param {Object} item
181      */
182     put: function(key, item)
183     {
184         if (!this.has(key))
185             ++this._size;
186         this._items[key] = item;
187     }
188 }
189
190 /**
191  * @constructor
192  * @param {Resource|Object} thisObject
193  * @param {string} functionName
194  * @param {Array|Arguments} args
195  * @param {Resource|*=} result
196  */
197 function Call(thisObject, functionName, args, result)
198 {
199     this._thisObject = thisObject;
200     this._functionName = functionName;
201     this._args = Array.prototype.slice.call(args, 0);
202     this._result = result;
203 }
204
205 Call.prototype = {
206     /**
207      * @return {Resource}
208      */
209     resource: function()
210     {
211         return Resource.forObject(this._thisObject);
212     },
213
214     /**
215      * @return {string}
216      */
217     functionName: function()
218     {
219         return this._functionName;
220     },
221
222     /**
223      * @return {Array}
224      */
225     args: function()
226     {
227         return this._args;
228     },
229
230     /**
231      * @return {*}
232      */
233     result: function()
234     {
235         return this._result;
236     },
237
238     freeze: function()
239     {
240         if (this._freezed)
241             return;
242         this._freezed = true;
243         for (var i = 0, n = this._args.length; i < n; ++i) {
244             // FIXME: freeze the Resources also!
245             if (!Resource.forObject(this._args[i]))
246                 this._args[i] = TypeUtils.clone(this._args[i]);
247         }
248     }
249 }
250
251 /**
252  * @constructor
253  * @param {ReplayableResource} thisObject
254  * @param {string} functionName
255  * @param {Array.<ReplayableResource|*>} args
256  * @param {ReplayableResource|*} result
257  */
258 function ReplayableCall(thisObject, functionName, args, result)
259 {
260     this._thisObject = thisObject;
261     this._functionName = functionName;
262     this._args = args;
263     this._result = result;
264 }
265
266 ReplayableCall.prototype = {
267     /**
268      * @return {ReplayableResource}
269      */
270     resource: function()
271     {
272         return this._thisObject;
273     },
274
275     /**
276      * @return {string}
277      */
278     functionName: function()
279     {
280         return this._functionName;
281     },
282
283     /**
284      * @return {Array.<ReplayableResource|*>}
285      */
286     args: function()
287     {
288         return this._args;
289     },
290
291     /**
292      * @return {ReplayableResource|*}
293      */
294     result: function()
295     {
296         return this._result;
297     },
298
299     /**
300      * @param {Cache} cache
301      * @return {Call}
302      */
303     replay: function(cache)
304     {
305         // FIXME: Do the replay.
306     }
307 }
308
309 /**
310  * @constructor
311  * @param {Object} wrappedObject
312  */
313 function Resource(wrappedObject)
314 {
315     this._id = ++Resource._uniqueId;
316     this._resourceManager = null;
317     this._calls = [];
318     this.setWrappedObject(wrappedObject);
319 }
320
321 /**
322  * @type {number}
323  */
324 Resource._uniqueId = 0;
325
326 /**
327  * @param {*} obj
328  * @return {Resource}
329  */
330 Resource.forObject = function(obj)
331 {
332     if (!obj)
333         return null;
334     if (obj instanceof Resource)
335         return obj;
336     if (typeof obj === "object")
337         return obj["__resourceObject"];
338     return null;
339 }
340
341 Resource.prototype = {
342     /**
343      * @return {number}
344      */
345     id: function()
346     {
347         return this._id;
348     },
349
350     /**
351      * @return {Object}
352      */
353     wrappedObject: function()
354     {
355         return this._wrappedObject;
356     },
357
358     /**
359      * @param {Object} value
360      */
361     setWrappedObject: function(value)
362     {
363         console.assert(value, "wrappedObject should not be NULL");
364         console.assert(!(value instanceof Resource), "Binding a Resource object to another Resource object?");
365         this._wrappedObject = value;
366         this._bindObjectToResource(value);
367     },
368
369     /**
370      * @return {Object}
371      */
372     proxyObject: function()
373     {
374         // No proxy wrapping by default.
375         return this.wrappedObject();
376     },
377
378     /**
379      * @return {ResourceTrackingManager}
380      */
381     manager: function()
382     {
383         return this._resourceManager;
384     },
385
386     /**
387      * @param {ResourceTrackingManager} value
388      */
389     setManager: function(value)
390     {
391         this._resourceManager = value;
392     },
393
394     /**
395      * @return {Array.<Call>}
396      */
397     calls: function()
398     {
399         return this._calls;
400     },
401
402     /**
403      * @param {Call} call
404      */
405     pushCall: function(call)
406     {
407         call.freeze();
408         this._calls.push(call);
409     },
410
411     /**
412      * @param {Object} object
413      */
414     _bindObjectToResource: function(object)
415     {
416         object["__resourceObject"] = this;
417     }
418 }
419
420 /**
421  * @constructor
422  * @param {Resource} originalResource
423  * @param {Object} data
424  */
425 function ReplayableResource(originalResource, data)
426 {
427 }
428
429 ReplayableResource.prototype = {
430     /**
431      * @param {Cache} cache
432      * @return {Resource}
433      */
434     replay: function(cache)
435     {
436         // FIXME: Do the replay.
437     }
438 }
439
440 /**
441  * @constructor
442  * @extends {Resource}
443  * @param {WebGLRenderingContext} glContext
444  */
445 function WebGLRenderingContextResource(glContext)
446 {
447     Resource.call(this, glContext);
448     this._proxyObject = null;
449 }
450
451 WebGLRenderingContextResource.prototype = {
452     /**
453      * @return {Object}
454      */
455     proxyObject: function()
456     {
457         if (!this._proxyObject)
458             this._proxyObject = this._wrapObject();
459         return this._proxyObject;
460     },
461
462     /**
463      * @return {Object}
464      */
465     _wrapObject: function()
466     {
467         var glContext = this.wrappedObject();
468         var proxy = Object.create(glContext.__proto__); // In order to emulate "instanceof".
469
470         var self = this;
471         function processProperty(property)
472         {
473             if (typeof glContext[property] === "function") {
474                 // FIXME: override GL calls affecting resources states here.
475                 proxy[property] = self._wrapFunction(self, glContext, glContext[property], property);
476             } else if (/^[A-Z0-9_]+$/.test(property)) {
477                 // Fast access to enums and constants.
478                 proxy[property] = glContext[property];
479             } else {
480                 Object.defineProperty(proxy, property, {
481                     get: function()
482                     {
483                         return glContext[property];
484                     },
485                     set: function(value)
486                     {
487                         glContext[property] = value;
488                     }
489                 });
490             }
491         }
492
493         for (var property in glContext)
494             processProperty(property);
495
496         return proxy;
497     },
498
499     /**
500      * @param {Resource} resource
501      * @param {WebGLRenderingContext} originalObject
502      * @param {Function} originalFunction
503      * @param {string} functionName
504      * @return {*}
505      */
506     _wrapFunction: function(resource, originalObject, originalFunction, functionName)
507     {
508         return function()
509         {
510             var manager = resource.manager();
511             if (!manager || !manager.capturing())
512                 return originalFunction.apply(originalObject, arguments);
513             manager.captureArguments(resource, arguments);
514             var result = originalFunction.apply(originalObject, arguments);
515             var call = new Call(resource, functionName, arguments, result);
516             manager.reportCall(call);
517             return result;
518         };
519     }
520 }
521
522 WebGLRenderingContextResource.prototype.__proto__ = Resource.prototype;
523
524 /**
525  * @constructor
526  * @param {WebGLRenderingContext} originalObject
527  * @param {Function} originalFunction
528  * @param {string} functionName
529  * @param {Array|Arguments} args
530  */
531 WebGLRenderingContextResource.WrapFunction = function(originalObject, originalFunction, functionName, args)
532 {
533     this._originalObject = originalObject;
534     this._originalFunction = originalFunction;
535     this._functionName = functionName;
536     this._args = args;
537     this._glResource = Resource.forObject(originalObject);
538 }
539
540 WebGLRenderingContextResource.WrapFunction.prototype = {
541     /**
542      * @return {*}
543      */
544     result: function()
545     {
546         if (!this._executed) {
547             this._executed = true;
548             this._result = this._originalFunction.apply(this._originalObject, this._args);
549         }
550         return this._result;
551     },
552
553     /**
554      * @return {Call}
555      */
556     call: function()
557     {
558         if (!this._call)
559             this._call = new Call(this._glResource, this._functionName, this._args, this.result());
560         return this._call;
561     }
562 }
563
564 /**
565  * @constructor
566  */
567 function TraceLog()
568 {
569     this._replayableCalls = [];
570     this._replayablesCache = new Cache();
571 }
572
573 TraceLog.prototype = {
574     /**
575      * @return {number}
576      */
577     size: function()
578     {
579         return this._replayableCalls.length;
580     },
581
582     /**
583      * @return {Array.<ReplayableCall>}
584      */
585     replayableCalls: function()
586     {
587         return this._replayableCalls;
588     },
589
590     /**
591      * @param {Resource} resource
592      */
593     captureResource: function(resource)
594     {
595         // FIXME: Capture current resource state to start the replay from.
596     },
597
598     /**
599      * @param {Call} call
600      */
601     addCall: function(call)
602     {
603         // FIXME: Convert the call to a ReplayableCall and push it.
604     }
605 }
606
607 /**
608  * @constructor
609  * @param {TraceLog} traceLog
610  */
611 function TraceLogPlayer(traceLog)
612 {
613     this._traceLog = traceLog;
614     this._nextReplayStep = 0;
615     this._replayWorldCache = new Cache();
616 }
617
618 TraceLogPlayer.prototype = {
619     /**
620      * @return {TraceLog}
621      */
622     traceLog: function()
623     {
624         return this._traceLog;
625     },
626
627     /**
628      * @return {number}
629      */
630     nextReplayStep: function()
631     {
632         return this._nextReplayStep;
633     },
634
635     reset: function()
636     {
637         // FIXME: Prevent memory leaks: detach and delete all old resources OR reuse them OR create a new replay canvas every time.
638         this._nextReplayStep = 0;
639         this._replayWorldCache.reset();
640     },
641
642     step: function()
643     {
644         this.stepTo(this._nextReplayStep);
645     },
646
647     /**
648      * @param {number} stepNum
649      */
650     stepTo: function(stepNum)
651     {
652         stepNum = Math.min(stepNum, this._traceLog.size() - 1);
653         console.assert(stepNum >= 0);
654         if (this._nextReplayStep > stepNum)
655             this.reset();
656         // FIXME: Replay all the cached resources first to warm-up.
657         var replayableCalls = this._traceLog.replayableCalls();
658         while (this._nextReplayStep <= stepNum)
659             replayableCalls[this._nextReplayStep++].replay(this._replayWorldCache);            
660     },
661
662     replay: function()
663     {
664         this.stepTo(this._traceLog.size() - 1);
665     }
666 }
667
668 /**
669  * @constructor
670  */
671 function ResourceTrackingManager()
672 {
673     this._capturing = false;
674     this._stopCapturingOnFrameEnd = false;
675     this._lastTraceLog = null;
676 }
677
678 ResourceTrackingManager.prototype = {
679     /**
680      * @return {boolean}
681      */
682     capturing: function()
683     {
684         return this._capturing;
685     },
686
687     /**
688      * @return {TraceLog}
689      */
690     lastTraceLog: function()
691     {
692         return this._lastTraceLog;
693     },
694
695     /**
696      * @param {Resource} resource
697      */
698     registerResource: function(resource)
699     {
700         resource.setManager(this);
701     },
702
703     startCapturing: function()
704     {
705         if (!this._capturing)
706             this._lastTraceLog = new TraceLog();
707         this._capturing = true;
708         this._stopCapturingOnFrameEnd = false;
709     },
710
711     /**
712      * @param {TraceLog=} traceLog
713      */
714     stopCapturing: function(traceLog)
715     {
716         if (traceLog && this._lastTraceLog !== traceLog)
717             return;
718         this._capturing = false;
719         this._stopCapturingOnFrameEnd = false;
720     },
721
722     captureFrame: function()
723     {
724         this._lastTraceLog = new TraceLog();
725         this._capturing = true;
726         this._stopCapturingOnFrameEnd = true;
727     },
728
729     /**
730      * @param {Resource} resource
731      * @param {Array|Arguments} args
732      */
733     captureArguments: function(resource, args)
734     {
735         if (!this._capturing)
736             return;
737         this._lastTraceLog.captureResource(resource);
738         for (var i = 0, n = args.length; i < n; ++i) {
739             var res = Resource.forObject(args[i]);
740             if (res)
741                 this._lastTraceLog.captureResource(res);
742         }
743     },
744
745     /**
746      * @param {Call} call
747      */
748     reportCall: function(call)
749     {
750         if (!this._capturing)
751             return;
752         this._lastTraceLog.addCall(call);
753         if (this._stopCapturingOnFrameEnd && this._lastTraceLog.size() === 1) {
754             this._stopCapturingOnFrameEnd = false;
755             this._setZeroTimeouts(this.stopCapturing.bind(this, this._lastTraceLog));
756         }
757     },
758
759     /**
760      * @param {Function} callback
761      */
762     _setZeroTimeouts: function(callback)
763     {
764         // We need a fastest async callback, whatever fires first.
765         // Usually a postMessage should be faster than a setTimeout(0).
766         var channel = new MessageChannel();
767         channel.port1.onmessage = callback;
768         channel.port2.postMessage("");
769         inspectedWindow.setTimeout(callback, 0);
770     }
771 }
772
773 /**
774  * @constructor
775  */
776 var InjectedScript = function()
777 {
778     this._manager = new ResourceTrackingManager();
779     this._lastTraceLogId = 0;
780     this._traceLogs = {};
781     this._traceLogPlayer = null;
782     this._replayContext = null;
783 }
784
785 InjectedScript.prototype = {
786     /**
787      * @param {WebGLRenderingContext} glContext
788      * @return {Object}
789      */
790     wrapWebGLContext: function(glContext)
791     {
792         var resource = Resource.forObject(glContext) || new WebGLRenderingContextResource(glContext);
793         this._manager.registerResource(resource);
794         var proxy = resource.proxyObject();
795         return proxy;
796     },
797
798     captureFrame: function()
799     {
800         var id = this._makeTraceLogId();
801         this._manager.captureFrame();
802         this._traceLogs[id] = this._manager.lastTraceLog();
803         return id;
804     },
805
806     /**
807      * @param {string} id
808      */
809     dropTraceLog: function(id)
810     {
811         if (this._traceLogPlayer && this._traceLogPlayer.traceLog() === this._traceLogs[id])
812             this._traceLogPlayer = null;
813         delete this._traceLogs[id];
814     },
815
816     /**
817      * @param {string} id
818      * @return {Object|string}
819      */
820     traceLog: function(id)
821     {
822         var traceLog = this._traceLogs[id];
823         if (!traceLog)
824             return "Error: Trace log with this ID not found.";
825         var result = {
826             id: id,
827             calls: []
828         };
829         var calls = traceLog.replayableCalls();
830         for (var i = 0, n = calls.length; i < n; ++i) {
831             var call = calls[i];
832             result.calls.push({
833                 functionName: call.functionName() + "(" + call.args().join(", ") + ") => " + call.result()
834             });
835         }
836         return result;
837     },
838
839     /**
840      * @param {string} id
841      * @param {number} stepNo
842      * @return {string}
843      */
844     replayTraceLog: function(id, stepNo)
845     {
846         var traceLog = this._traceLogs[id];
847         if (!traceLog)
848             return "";
849         if (!this._traceLogPlayer || this._traceLogPlayer.traceLog() !== traceLog)
850             this._traceLogPlayer = new TraceLogPlayer(traceLog);
851         this._traceLogPlayer.stepTo(stepNo);
852         if (!this._replayContext) {
853             console.error("ASSERT_NOT_REACHED: replayTraceLog failed to create a replay canvas?!");
854             return "";
855         }
856         // Return current screenshot.
857         return this._replayContext.canvas.toDataURL();
858     },
859
860     /**
861      * @return {string}
862      */
863     _makeTraceLogId: function()
864     {
865         return "{\"injectedScriptId\":" + injectedScriptId + ",\"traceLogId\":" + (++this._lastTraceLogId) + "}";
866     }
867 }
868
869 var injectedScript = new InjectedScript();
870 return injectedScript;
871
872 })