136e8c83abb2ab1650519f7a7dd199292b732d99
[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 /**
39  * @constructor
40  */
41 function Cache()
42 {
43     this.reset();
44 }
45
46 Cache.prototype = {
47     /**
48      * @return {number}
49      */
50     size: function()
51     {
52         return this._size;
53     },
54
55     reset: function()
56     {
57         this._items = Object.create(null);
58         this._size = 0;
59     },
60
61     /**
62      * @param {number} key
63      * @return {boolean}
64      */
65     has: function(key)
66     {
67         return key in this._items;
68     },
69
70     /**
71      * @param {number} key
72      * @return {Object}
73      */
74     get: function(key)
75     {
76         return this._items[key];
77     },
78
79     /**
80      * @param {number} key
81      * @param {Object} item
82      */
83     put: function(key, item)
84     {
85         if (!this.has(key))
86             ++this._size;
87         this._items[key] = item;
88     }
89 }
90
91 /**
92  * @constructor
93  * @param {Resource|Object} thisObject
94  * @param {string} functionName
95  * @param {Array|Arguments} args
96  * @param {Resource|*} result
97  */
98 function Call(thisObject, functionName, args, result)
99 {
100     this._thisObject = thisObject;
101     this._functionName = functionName;
102     this._args = Array.prototype.slice.call(args, 0);
103     this._result = result;
104 }
105
106 Call.prototype = {
107     /**
108      * @return {Resource}
109      */
110     resource: function()
111     {
112         return Resource.forObject(this._thisObject);
113     },
114
115     /**
116      * @return {string}
117      */
118     functionName: function()
119     {
120         return this._functionName;
121     },
122
123     /**
124      * @return {Array}
125      */
126     args: function()
127     {
128         return this._args;
129     },
130
131     /**
132      * @return {*}
133      */
134     result: function()
135     {
136         return this._result;
137     }
138 }
139
140 /**
141  * @constructor
142  * @param {ReplayableResource} thisObject
143  * @param {string} functionName
144  * @param {Array.<ReplayableResource|*>} args
145  * @param {ReplayableResource|*} result
146  */
147 function ReplayableCall(thisObject, functionName, args, result)
148 {
149     this._thisObject = thisObject;
150     this._functionName = functionName;
151     this._args = args;
152     this._result = result;
153 }
154
155 ReplayableCall.prototype = {
156     /**
157      * @return {ReplayableResource}
158      */
159     resource: function()
160     {
161         return this._thisObject;
162     },
163
164     /**
165      * @return {string}
166      */
167     functionName: function()
168     {
169         return this._functionName;
170     },
171
172     /**
173      * @return {Array.<ReplayableResource|*>}
174      */
175     args: function()
176     {
177         return this._args;
178     },
179
180     /**
181      * @return {ReplayableResource|*}
182      */
183     result: function()
184     {
185         return this._result;
186     },
187
188     /**
189      * @param {Cache} cache
190      * @return {Call}
191      */
192     replay: function(cache)
193     {
194         // FIXME: Do the replay.
195     }
196 }
197
198 /**
199  * @constructor
200  * @param {Object} wrappedObject
201  */
202 function Resource(wrappedObject)
203 {
204     this._id = ++Resource._uniqueId;
205     this._resourceManager = null;
206     this.setWrappedObject(wrappedObject);
207 }
208
209 Resource._uniqueId = 0;
210
211 /**
212  * @param {Object} obj
213  * @return {Resource}
214  */
215 Resource.forObject = function(obj)
216 {
217     if (!obj || obj instanceof Resource)
218         return obj;
219     if (typeof obj === "object")
220         return obj["__resourceObject"];
221     return null;
222 }
223
224 Resource.prototype = {
225     /**
226      * @return {number}
227      */
228     id: function()
229     {
230         return this._id;
231     },
232
233     /**
234      * @return {Object}
235      */
236     wrappedObject: function()
237     {
238         return this._wrappedObject;
239     },
240
241     /**
242      * @param {Object} value
243      */
244     setWrappedObject: function(value)
245     {
246         console.assert(value && !(value instanceof Resource), "Binding a Resource object to another Resource object?");
247         this._wrappedObject = value;
248         this._bindObjectToResource(value);
249     },
250
251     /**
252      * @return {Object}
253      */
254     proxyObject: function()
255     {
256         // No proxy wrapping by default.
257         return this.wrappedObject();
258     },
259
260     /**
261      * @return {ResourceTrackingManager}
262      */
263     manager: function()
264     {
265         return this._resourceManager;
266     },
267
268     /**
269      * @param {ResourceTrackingManager} value
270      */
271     setManager: function(value)
272     {
273         this._resourceManager = value;
274     },
275
276     /**
277      * @param {Object} object
278      */
279     _bindObjectToResource: function(object)
280     {
281         object["__resourceObject"] = this;
282     }
283 }
284
285 /**
286  * @constructor
287  * @param {Resource} originalResource
288  * @param {Object} data
289  */
290 function ReplayableResource(originalResource, data)
291 {
292 }
293
294 ReplayableResource.prototype = {
295     /**
296      * @param {Cache} cache
297      * @return {Resource}
298      */
299     replay: function(cache)
300     {
301         // FIXME: Do the replay.
302     }
303 }
304
305 /**
306  * @constructor
307  * @extends {Resource}
308  * @param {WebGLRenderingContext} glContext
309  */
310 function WebGLRenderingContextResource(glContext)
311 {
312     Resource.call(this, glContext);
313     this._proxyObject = null;
314 }
315
316 WebGLRenderingContextResource.prototype = {
317     /**
318      * @return {Object}
319      */
320     proxyObject: function()
321     {
322         if (!this._proxyObject)
323             this._proxyObject = this._wrapObject();
324         return this._proxyObject;
325     },
326
327     /**
328      * @return {Object}
329      */
330     _wrapObject: function()
331     {
332         var glContext = this.wrappedObject();
333         var proxy = Object.create(glContext.__proto__); // In order to emulate "instanceof".
334
335         var self = this;
336         function processProperty(property)
337         {
338             if (typeof glContext[property] === "function") {
339                 // FIXME: override GL calls affecting resources states here.
340                 proxy[property] = self._wrapFunction(self, glContext, glContext[property], property);
341             } else if (/^[A-Z0-9_]+$/.test(property)) {
342                 // Fast access to enums and constants.
343                 proxy[property] = glContext[property];
344             } else {
345                 Object.defineProperty(proxy, property, {
346                     get: function()
347                     {
348                         return glContext[property];
349                     },
350                     set: function(value)
351                     {
352                         glContext[property] = value;
353                     }
354                 });
355             }
356         }
357
358         for (var property in glContext)
359             processProperty(property);
360
361         return proxy;
362     },
363
364     /**
365      * @param {Resource} resource
366      * @param {WebGLRenderingContext} originalObject
367      * @param {Function} originalFunction
368      * @param {string} functionName
369      * @return {*}
370      */
371     _wrapFunction: function(resource, originalObject, originalFunction, functionName)
372     {
373         return function()
374         {
375             var manager = resource.manager();
376             if (!manager || !manager.capturing())
377                 return originalFunction.apply(originalObject, arguments);
378             manager.captureArguments(resource, arguments);
379             var result = originalFunction.apply(originalObject, arguments);
380             var call = new Call(resource, functionName, arguments, result);
381             manager.reportCall(call);
382             return result;
383         };
384     }
385 }
386
387 WebGLRenderingContextResource.prototype.__proto__ = Resource.prototype;
388
389 /**
390  * @constructor
391  * @param {WebGLRenderingContext} originalObject
392  * @param {Function} originalFunction
393  * @param {string} functionName
394  * @param {Array} args
395  */
396 WebGLRenderingContextResource.WrapFunction = function(originalObject, originalFunction, functionName, args)
397 {
398     this._originalObject = originalObject;
399     this._originalFunction = originalFunction;
400     this._functionName = functionName;
401     this._args = args;
402     this._glResource = Resource.forObject(originalObject);
403 }
404
405 WebGLRenderingContextResource.WrapFunction.prototype = {
406     /**
407      * @return {*}
408      */
409     result: function()
410     {
411         if (!this._executed) {
412             this._executed = true;
413             this._result = this._originalFunction.apply(this._originalObject, this._args);
414         }
415         return this._result;
416     },
417
418     /**
419      * @return {Call}
420      */
421     call: function()
422     {
423         if (!this._call)
424             this._call = new Call(this._glResource, this._functionName, this._args, this.result());
425         return this._call;
426     }
427 }
428
429 /**
430  * @constructor
431  */
432 function TraceLog()
433 {
434     this._replayableCalls = [];
435     this._replayablesCache = new Cache();
436 }
437
438 TraceLog.prototype = {
439     /**
440      * @return {number}
441      */
442     size: function()
443     {
444         return this._replayableCalls.length;
445     },
446
447     /**
448      * @return {Array.<ReplayableCall>}
449      */
450     replayableCalls: function()
451     {
452         return this._replayableCalls;
453     },
454
455     /**
456      * @param {Resource} resource
457      */
458     captureResource: function(resource)
459     {
460         // FIXME: Capture current resource state to start the replay from.
461     },
462
463     /**
464      * @param {Call} call
465      */
466     addCall: function(call)
467     {
468         // FIXME: Convert the call to a ReplayableCall and push it.
469     }
470 }
471
472 /**
473  * @constructor
474  * @param {TraceLog} traceLog
475  */
476 function TraceLogPlayer(traceLog)
477 {
478     this._traceLog = traceLog;
479     this._nextReplayStep = 0;
480     this._replayWorldCache = new Cache();
481 }
482
483 TraceLogPlayer.prototype = {
484     /**
485      * @return {TraceLog}
486      */
487     traceLog: function()
488     {
489         return this._traceLog;
490     },
491
492     /**
493      * @return {number}
494      */
495     nextReplayStep: function()
496     {
497         return this._nextReplayStep;
498     },
499
500     reset: function()
501     {
502         // FIXME: Prevent memory leaks: detach and delete all old resources OR reuse them OR create a new replay canvas every time.
503         this._nextReplayStep = 0;
504         this._replayWorldCache.reset();
505     },
506
507     step: function()
508     {
509         this.stepTo(this._nextReplayStep);
510     },
511
512     /**
513      * @param {number} stepNum
514      */
515     stepTo: function(stepNum)
516     {
517         stepNum = Math.min(stepNum, this._traceLog.size() - 1);
518         console.assert(stepNum >= 0);
519         if (this._nextReplayStep > stepNum)
520             this.reset();
521         // FIXME: Replay all the cached resources first to warm-up.
522         var replayableCalls = this._traceLog.replayableCalls();
523         while (this._nextReplayStep <= stepNum)
524             replayableCalls[this._nextReplayStep++].replay(this._replayWorldCache);            
525     },
526
527     replay: function()
528     {
529         this.stepTo(this._traceLog.size() - 1);
530     }
531 }
532
533 /**
534  * @constructor
535  */
536 function ResourceTrackingManager()
537 {
538     this._capturing = false;
539     this._stopCapturingOnFrameEnd = false;
540     this._lastTraceLog = null;
541 }
542
543 ResourceTrackingManager.prototype = {
544     /**
545      * @return {boolean}
546      */
547     capturing: function()
548     {
549         return this._capturing;
550     },
551
552     /**
553      * @return {TraceLog}
554      */
555     lastTraceLog: function()
556     {
557         return this._lastTraceLog;
558     },
559
560     /**
561      * @param {Resource} resource
562      */
563     registerResource: function(resource)
564     {
565         resource.setManager(this);
566     },
567
568     startCapturing: function()
569     {
570         if (!this._capturing)
571             this._lastTraceLog = new TraceLog();
572         this._capturing = true;
573         this._stopCapturingOnFrameEnd = false;
574     },
575
576     /**
577      * @param {TraceLog=} traceLog
578      */
579     stopCapturing: function(traceLog)
580     {
581         if (traceLog && this._lastTraceLog !== traceLog)
582             return;
583         this._capturing = false;
584         this._stopCapturingOnFrameEnd = false;
585     },
586
587     captureFrame: function()
588     {
589         this._lastTraceLog = new TraceLog();
590         this._capturing = true;
591         this._stopCapturingOnFrameEnd = true;
592     },
593
594     captureArguments: function(resource, args)
595     {
596         if (!this._capturing)
597             return;
598         this._lastTraceLog.captureResource(resource);
599         for (var i = 0, n = args.length; i < n; ++i) {
600             var res = Resource.forObject(args[i]);
601             if (res)
602                 this._lastTraceLog.captureResource(res);
603         }
604     },
605
606     /**
607      * @param {Call} call
608      */
609     reportCall: function(call)
610     {
611         if (!this._capturing)
612             return;
613         this._lastTraceLog.addCall(call);
614         if (this._stopCapturingOnFrameEnd && this._lastTraceLog.size() === 1) {
615             this._stopCapturingOnFrameEnd = false;
616             this._setZeroTimeouts(this.stopCapturing.bind(this, this._lastTraceLog));
617         }
618     },
619
620     /**
621      * @param {Function} callback
622      */
623     _setZeroTimeouts: function(callback)
624     {
625         // We need a fastest async callback, whatever fires first.
626         // Usually a postMessage should be faster than a setTimeout(0).
627         var channel = new MessageChannel();
628         channel.port1.onmessage = callback;
629         channel.port2.postMessage("");
630         inspectedWindow.setTimeout(callback, 0);
631     }
632 }
633
634 /**
635  * @constructor
636  */
637 var InjectedScript = function()
638 {
639     this._manager = new ResourceTrackingManager();
640     this._lastTraceLogId = 0;
641     this._traceLogs = {};
642     this._traceLogPlayer = null;
643     this._replayContext = null;
644 }
645
646 InjectedScript.prototype = {
647     /**
648      * @param {WebGLRenderingContext} glContext
649      * @return {Object}
650      */
651     wrapWebGLContext: function(glContext)
652     {
653         var resource = Resource.forObject(glContext) || new WebGLRenderingContextResource(glContext);
654         this._manager.registerResource(resource);
655         var proxy = resource.proxyObject();
656         return proxy;
657     },
658
659     captureFrame: function()
660     {
661         var id = this._makeTraceLogId();
662         this._manager.captureFrame();
663         this._traceLogs[id] = this._manager.lastTraceLog();
664         return id;
665     },
666
667     /**
668      * @param {string} id
669      */
670     dropTraceLog: function(id)
671     {
672         if (this._traceLogPlayer && this._traceLogPlayer.traceLog() === this._traceLogs[id])
673             this._traceLogPlayer = null;
674         delete this._traceLogs[id];
675     },
676
677     /**
678      * @param {string} id
679      * @return {Object|string}
680      */
681     traceLog: function(id)
682     {
683         var traceLog = this._traceLogs[id];
684         if (!traceLog)
685             return "Error: Trace log with this ID not found.";
686         var result = {
687             id: id,
688             calls: []
689         };
690         var calls = traceLog.replayableCalls();
691         for (var i = 0, n = calls.length; i < n; ++i) {
692             var call = calls[i];
693             result.calls.push({
694                 functionName: call.functionName() + "(" + call.args().join(", ") + ") => " + call.result()
695             });
696         }
697         return result;
698     },
699
700     /**
701      * @param {string} id
702      * @param {number} stepNo
703      * @return {string}
704      */
705     replayTraceLog: function(id, stepNo)
706     {
707         var traceLog = this._traceLogs[id];
708         if (!traceLog)
709             return "";
710         if (!this._traceLogPlayer || this._traceLogPlayer.traceLog() !== traceLog)
711             this._traceLogPlayer = new TraceLogPlayer(traceLog);
712         this._traceLogPlayer.stepTo(stepNo);
713         if (!this._replayContext) {
714             console.error("ASSERT_NOT_REACHED: replayTraceLog failed to create a replay canvas?!");
715             return "";
716         }
717         // Return current screenshot.
718         return this._replayContext.canvas.toDataURL();
719     },
720
721     /**
722      * @return {string}
723      */
724     _makeTraceLogId: function()
725     {
726         return "{\"injectedScriptId\":" + injectedScriptId + ",\"traceLogId\":" + (++this._lastTraceLogId) + "}";
727     }
728 }
729
730 var injectedScript = new InjectedScript();
731 return injectedScript;
732
733 })