Web Inspector: Canvas: send a call stack with each action instead of an array of...
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Models / RecordingAction.js
1 /*
2  * Copyright (C) 2017 Apple 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
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WI.RecordingAction = class RecordingAction extends WI.Object
27 {
28     constructor(name, parameters, swizzleTypes, trace, snapshot)
29     {
30         super();
31
32         this._payloadName = name;
33         this._payloadParameters = parameters;
34         this._payloadSwizzleTypes = swizzleTypes;
35         this._payloadTrace = trace;
36         this._payloadSnapshot = snapshot || -1;
37
38         this._name = "";
39         this._parameters = [];
40         this._trace = [];
41         this._snapshot = "";
42
43         this._valid = true;
44         this._isFunction = false;
45         this._isGetter = false;
46         this._isVisual = false;
47
48         this._contextReplacer = null;
49
50         this._states = [];
51         this._stateModifiers = new Set;
52
53         this._warning = null;
54         this._swizzled = false;
55         this._processed = false;
56     }
57
58     // Static
59
60     // Payload format: (name, parameters, swizzleTypes, [trace, [snapshot]])
61     static fromPayload(payload)
62     {
63         if (!Array.isArray(payload))
64             payload = [];
65
66         if (isNaN(payload[0]))
67             payload[0] = -1;
68
69         if (!Array.isArray(payload[1]))
70             payload[1] = [];
71
72         if (!Array.isArray(payload[2]))
73             payload[2] = [];
74
75         if (isNaN(payload[3]) || (!payload[3] && payload[3] !== 0)) {
76             // COMPATIBILITY (iOS 12.1): "trace" was sent as an array of call frames instead of a single call stack
77             if (!Array.isArray(payload[3]))
78                 payload[3] = [];
79         }
80
81         if (payload.length >= 5 && isNaN(payload[4]))
82             payload[4] = -1;
83
84         return new WI.RecordingAction(...payload);
85     }
86
87     static isFunctionForType(type, name)
88     {
89         let prototype = WI.RecordingAction._prototypeForType(type);
90         if (!prototype)
91             return false;
92         let propertyDescriptor = Object.getOwnPropertyDescriptor(prototype, name);
93         if (!propertyDescriptor)
94             return false;
95         return typeof propertyDescriptor.value === "function";
96     }
97
98     static constantNameForParameter(type, name, value, index, count)
99     {
100         let indexesForType = WI.RecordingAction._constantIndexes[type];
101         if (!indexesForType)
102             return null;
103
104         let indexesForAction = indexesForType[name];
105         if (!indexesForAction)
106             return null;
107
108         if (Array.isArray(indexesForAction) && !indexesForAction.includes(index))
109             return null;
110
111         if (typeof indexesForAction === "object") {
112             let indexesForActionVariant = indexesForAction[count];
113             if (!indexesForActionVariant)
114                 return null;
115
116             if (Array.isArray(indexesForActionVariant) && !indexesForActionVariant.includes(index))
117                 return null;
118         }
119
120         if (value === 0 && type === WI.Recording.Type.CanvasWebGL) {
121             if (name === "blendFunc" || name === "blendFuncSeparate")
122                 return "ZERO";
123             if (index === 0) {
124                 if (name === "drawArrays" || name === "drawElements")
125                     return "POINTS";
126                 if (name === "pixelStorei")
127                     return "NONE";
128             }
129         }
130
131         let prototype = WI.RecordingAction._prototypeForType(type);
132         for (let key in prototype) {
133             let descriptor = Object.getOwnPropertyDescriptor(prototype, key);
134             if (descriptor && descriptor.value === value)
135                 return key;
136         }
137
138         return null;
139     }
140
141     static _prototypeForType(type)
142     {
143         if (type === WI.Recording.Type.Canvas2D)
144             return CanvasRenderingContext2D.prototype;
145         if (type === WI.Recording.Type.CanvasBitmapRenderer)
146             return ImageBitmapRenderingContext.prototype;
147         if (type === WI.Recording.Type.CanvasWebGL)
148             return WebGLRenderingContext.prototype;
149         return null;
150     }
151
152     // Public
153
154     get name() { return this._name; }
155     get parameters() { return this._parameters; }
156     get swizzleTypes() { return this._payloadSwizzleTypes; }
157     get trace() { return this._trace; }
158     get snapshot() { return this._snapshot; }
159     get valid() { return this._valid; }
160     get isFunction() { return this._isFunction; }
161     get isGetter() { return this._isGetter; }
162     get isVisual() { return this._isVisual; }
163     get contextReplacer() { return this._contextReplacer; }
164     get states() { return this._states; }
165     get stateModifiers() { return this._stateModifiers; }
166     get warning() { return this._warning; }
167
168     get ready()
169     {
170         return this._swizzled && this._processed;
171     }
172
173     process(recording, context, states, {lastAction} = {})
174     {
175         console.assert(this._swizzled, "You must swizzle() before you can process().");
176         console.assert(!this._processed, "You should only process() once.");
177
178         this._processed = true;
179
180         if (recording.type === WI.Recording.Type.CanvasWebGL) {
181             // We add each RecordingAction to the list of visualActionIndexes after it is processed.
182             if (this._valid && this._isVisual) {
183                 let contentBefore = recording.visualActionIndexes.length ? recording.actions[recording.visualActionIndexes.lastValue].snapshot : recording.initialState.content;
184                 if (this._snapshot === contentBefore)
185                     this._warning = WI.UIString("This action causes no visual change");
186             }
187             return;
188         }
189
190         function getContent() {
191             if (context instanceof CanvasRenderingContext2D)
192                 return context.getImageData(0, 0, context.canvas.width, context.canvas.height).data;
193
194             if (context instanceof WebGLRenderingContext || context instanceof WebGL2RenderingContext) {
195                 let pixels = new Uint8Array(context.drawingBufferWidth * context.drawingBufferHeight * 4);
196                 context.readPixels(0, 0, context.canvas.width, context.canvas.height, context.RGBA, context.UNSIGNED_BYTE, pixels);
197                 return pixels;
198             }
199
200             if (context.canvas instanceof HTMLCanvasElement)
201                 return [context.canvas.toDataURL()];
202
203             console.assert("Unknown context type", context);
204             return [];
205         }
206
207         let contentBefore = null;
208         let shouldCheckHasVisualEffect = this._valid && this._isVisual;
209         if (shouldCheckHasVisualEffect)
210             contentBefore = getContent();
211
212         this.apply(context);
213
214         if (shouldCheckHasVisualEffect) {
215             let contentAfter = getContent();
216             if (Array.shallowEqual(contentBefore, contentAfter))
217                 this._warning = WI.UIString("This action causes no visual change");
218         }
219
220         if (recording.type === WI.Recording.Type.Canvas2D) {
221             let currentState = WI.RecordingState.fromContext(recording.type, context, {source: this});
222             console.assert(currentState);
223
224             if (this.name === "save")
225                 states.push(currentState);
226             else if (this.name === "restore")
227                 states.pop();
228
229             this._states = states.slice();
230             this._states.push(currentState);
231
232             let lastState = null;
233             if (lastAction) {
234                 let previousState = lastAction.states.lastValue;
235                 for (let [name, value] of currentState) {
236                     let previousValue = previousState.get(name);
237                     if (value !== previousValue && !Object.shallowEqual(value, previousValue))
238                         this._stateModifiers.add(name);
239                 }
240             }
241
242             if (WI.ImageUtilities.supportsCanvasPathDebugging()) {
243                 let currentX = currentState.currentX;
244                 let invalidX = (currentX < 0 || currentX >= context.canvas.width) && (!lastState || currentX !== lastState.currentX);
245
246                 let currentY = currentState.currentY;
247                 let invalidY = (currentY < 0 || currentY >= context.canvas.height) && (!lastState || currentY !== lastState.currentY);
248
249                 if (invalidX || invalidY)
250                     this._warning = WI.UIString("This action moves the path outside the visible area");
251             }
252         }
253     }
254
255     async swizzle(recording, lastAction)
256     {
257         console.assert(!this._swizzled, "You should only swizzle() once.");
258
259         if (!this._valid) {
260             this._swizzled = true;
261             return;
262         }
263
264         let swizzleParameter = (item, index) => {
265             return recording.swizzle(item, this._payloadSwizzleTypes[index]);
266         };
267
268         let swizzlePromises = [
269             recording.swizzle(this._payloadName, WI.Recording.Swizzle.String),
270             Promise.all(this._payloadParameters.map(swizzleParameter)),
271         ];
272
273         if (!isNaN(this._payloadTrace))
274             swizzlePromises.push(recording.swizzle(this._payloadTrace, WI.Recording.Swizzle.CallStack))
275         else {
276             // COMPATIBILITY (iOS 12.1): "trace" was sent as an array of call frames instead of a single call stack
277             swizzlePromises.push(Promise.all(this._payloadTrace.map((item) => recording.swizzle(item, WI.Recording.Swizzle.CallFrame))));
278         }
279
280         if (this._payloadSnapshot >= 0)
281             swizzlePromises.push(recording.swizzle(this._payloadSnapshot, WI.Recording.Swizzle.String));
282
283         let [name, parameters, callFrames, snapshot] = await Promise.all(swizzlePromises);
284         this._name = name;
285         this._parameters = parameters;
286         this._trace = callFrames;
287         if (this._payloadSnapshot >= 0)
288             this._snapshot = snapshot;
289
290         if (recording.type === WI.Recording.Type.Canvas2D || recording.type === WI.Recording.Type.CanvasBitmapRenderer || recording.type === WI.Recording.Type.CanvasWebGL) {
291             if (this._name === "width" || this._name === "height") {
292                 this._contextReplacer = "canvas";
293                 this._isFunction = false;
294                 this._isGetter = !this._parameters.length;
295                 this._isVisual = !this._isGetter;
296             }
297
298             // FIXME: <https://webkit.org/b/180833>
299         }
300
301         if (!this._contextReplacer) {
302             this._isFunction = WI.RecordingAction.isFunctionForType(recording.type, this._name);
303             this._isGetter = !this._isFunction && !this._parameters.length;
304
305             let visualNames = WI.RecordingAction._visualNames[recording.type];
306             this._isVisual = visualNames ? visualNames.has(this._name) : false;
307
308             if (this._valid) {
309                 let prototype = WI.RecordingAction._prototypeForType(recording.type);
310                 if (prototype && !(name in prototype)) {
311                     this.markInvalid();
312
313                     WI.Recording.synthesizeError(WI.UIString("ā€œ%sā€ is invalid.").format(name));
314                 }
315             }
316         }
317
318         if (this._valid) {
319             let parametersSpecified = this._parameters.every((parameter) => parameter !== undefined);
320             let parametersCanBeSwizzled = this._payloadSwizzleTypes.every((swizzleType) => swizzleType !== WI.Recording.Swizzle.None);
321             if (!parametersSpecified || !parametersCanBeSwizzled)
322                 this.markInvalid();
323         }
324
325         if (this._valid) {
326             let stateModifiers = WI.RecordingAction._stateModifiers[recording.type];
327             if (stateModifiers) {
328                 this._stateModifiers.add(this._name);
329                 let modifiedByAction = stateModifiers[this._name] || [];
330                 for (let item of modifiedByAction)
331                     this._stateModifiers.add(item);
332             }
333         }
334
335         this._swizzled = true;
336     }
337
338     apply(context, options = {})
339     {
340         console.assert(this._swizzled, "You must swizzle() before you can apply().");
341         console.assert(this._processed, "You must process() before you can apply().");
342
343         if (!this.valid)
344             return;
345
346         try {
347             let name = options.nameOverride || this._name;
348
349             if (this._contextReplacer)
350                 context = context[this._contextReplacer];
351
352             if (this.isFunction)
353                 context[name](...this._parameters);
354             else {
355                 if (this.isGetter)
356                     context[name];
357                 else
358                     context[name] = this._parameters[0];
359             }
360         } catch {
361             this.markInvalid();
362
363             WI.Recording.synthesizeError(WI.UIString("ā€œ%sā€ threw an error.").format(this._name));
364         }
365     }
366
367     markInvalid()
368     {
369         if (!this._valid)
370             return;
371
372         this._valid = false;
373
374         this.dispatchEventToListeners(WI.RecordingAction.Event.ValidityChanged);
375     }
376
377     getColorParameters()
378     {
379         switch (this._name) {
380         // 2D
381         case "fillStyle":
382         case "strokeStyle":
383         case "shadowColor":
384         // 2D (non-standard, legacy)
385         case "setFillColor":
386         case "setStrokeColor":
387         // WebGL
388         case "blendColor":
389         case "clearColor":
390         case "colorMask":
391             return this._parameters;
392
393         // 2D (non-standard, legacy)
394         case "setShadow":
395             return this._parameters.slice(3);
396         }
397
398         return [];
399     }
400
401     getImageParameters()
402     {
403         switch (this._name) {
404         // 2D
405         case "createImageData":
406         case "createPattern":
407         case "drawImage":
408         case "fillStyle":
409         case "putImageData":
410         case "strokeStyle":
411         // 2D (non-standard)
412         case "drawImageFromRect":
413         // BitmapRenderer
414         case "transferFromImageBitmap":
415             return this._parameters.slice(0, 1);
416
417         // WebGL
418         case "texImage2D":
419         case "texSubImage2D":
420         case "compressedTexImage2D":
421             return [this._parameters.lastValue];
422         }
423
424         return [];
425     }
426
427     toJSON()
428     {
429         let json = [this._payloadName, this._payloadParameters, this._payloadSwizzleTypes, this._payloadTrace];
430         if (this._payloadSnapshot >= 0)
431             json.push(this._payloadSnapshot);
432         return json;
433     }
434 };
435
436 WI.RecordingAction.Event = {
437     ValidityChanged: "recording-action-marked-invalid",
438 };
439
440 WI.RecordingAction._constantIndexes = {
441     [WI.Recording.Type.CanvasWebGL]: {
442         "activeTexture": true,
443         "bindBuffer": true,
444         "bindFramebuffer": true,
445         "bindRenderbuffer": true,
446         "bindTexture": true,
447         "blendEquation": true,
448         "blendEquationSeparate": true,
449         "blendFunc": true,
450         "blendFuncSeparate": true,
451         "bufferData": [0, 2],
452         "bufferSubData": [0],
453         "checkFramebufferStatus": true,
454         "compressedTexImage2D": [0, 2],
455         "compressedTexSubImage2D": [0],
456         "copyTexImage2D": [0, 2],
457         "copyTexSubImage2D": [0],
458         "createShader": true,
459         "cullFace": true,
460         "depthFunc": true,
461         "disable": true,
462         "drawArrays": [0],
463         "drawElements": [0, 2],
464         "enable": true,
465         "framebufferRenderbuffer": true,
466         "framebufferTexture2D": [0, 1, 2],
467         "frontFace": true,
468         "generateMipmap": true,
469         "getBufferParameter": true,
470         "getFramebufferAttachmentParameter": true,
471         "getParameter": true,
472         "getProgramParameter": true,
473         "getRenderbufferParameter": true,
474         "getShaderParameter": true,
475         "getShaderPrecisionFormat": true,
476         "getTexParameter": true,
477         "getVertexAttrib": [1],
478         "getVertexAttribOffset": [1],
479         "hint": true,
480         "isEnabled": true,
481         "pixelStorei": [0],
482         "readPixels": [4, 5],
483         "renderbufferStorage": [0, 1],
484         "stencilFunc": [0],
485         "stencilFuncSeparate": [0, 1],
486         "stencilMaskSeparate": [0],
487         "stencilOp": true,
488         "stencilOpSeparate": true,
489         "texImage2D": {
490             5: [0, 2, 3, 4],
491             6: [0, 2, 3, 4],
492             8: [0, 2, 6, 7],
493             9: [0, 2, 6, 7],
494         },
495         "texParameterf": [0, 1],
496         "texParameteri": [0, 1],
497         "texSubImage2D": {
498             6: [0, 4, 5],
499             7: [0, 4, 5],
500             8: [0, 6, 7],
501             9: [0, 6, 7],
502         },
503         "vertexAttribPointer": [2],
504     },
505 };
506
507 WI.RecordingAction._visualNames = {
508     [WI.Recording.Type.Canvas2D]: new Set([
509         "clearRect",
510         "drawFocusIfNeeded",
511         "drawImage",
512         "drawImageFromRect",
513         "fill",
514         "fillRect",
515         "fillText",
516         "putImageData",
517         "stroke",
518         "strokeRect",
519         "strokeText",
520     ]),
521     [WI.Recording.Type.CanvasBitmapRenderer]: new Set([
522         "transferFromImageBitmap",
523     ]),
524     [WI.Recording.Type.CanvasWebGL]: new Set([
525         "clear",
526         "drawArrays",
527         "drawElements",
528     ]),
529 };
530
531 WI.RecordingAction._stateModifiers = {
532     [WI.Recording.Type.Canvas2D]: {
533         arc: ["currentX", "currentY"],
534         arcTo: ["currentX", "currentY"],
535         beginPath: ["currentX", "currentY"],
536         bezierCurveTo: ["currentX", "currentY"],
537         clearShadow: ["shadowOffsetX", "shadowOffsetY", "shadowBlur", "shadowColor"],
538         closePath: ["currentX", "currentY"],
539         ellipse: ["currentX", "currentY"],
540         lineTo: ["currentX", "currentY"],
541         moveTo: ["currentX", "currentY"],
542         quadraticCurveTo: ["currentX", "currentY"],
543         rect: ["currentX", "currentY"],
544         resetTransform: ["transform"],
545         rotate: ["transform"],
546         scale: ["transform"],
547         setAlpha: ["globalAlpha"],
548         setCompositeOperation: ["globalCompositeOperation"],
549         setFillColor: ["fillStyle"],
550         setLineCap: ["lineCap"],
551         setLineJoin: ["lineJoin"],
552         setLineWidth: ["lineWidth"],
553         setMiterLimit: ["miterLimit"],
554         setShadow: ["shadowOffsetX", "shadowOffsetY", "shadowBlur", "shadowColor"],
555         setStrokeColor: ["strokeStyle"],
556         setTransform: ["transform"],
557         translate: ["transform"],
558     },
559 };