2 * Copyright (C) 2017 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
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.
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.
26 WI.Recording = class Recording extends WI.Object
28 constructor(version, type, initialState, frames, data)
32 this._version = version;
34 this._initialState = initialState;
35 this._frames = frames;
37 this._displayName = WI.UIString("Recording");
40 this._actions = [new WI.RecordingInitialStateAction].concat(...this._frames.map((frame) => frame.actions));
41 this._visualActionIndexes = [];
44 this._processContext = null;
45 this._processStates = [];
46 this._processing = false;
49 static fromPayload(payload, frames)
51 if (typeof payload !== "object" || payload === null)
54 if (isNaN(payload.version) || payload.version <= 0)
58 switch (payload.type) {
59 case RecordingAgent.Type.Canvas2D:
60 type = WI.Recording.Type.Canvas2D;
62 case RecordingAgent.Type.CanvasBitmapRenderer:
63 type = WI.Recording.Type.CanvasBitmapRenderer;
65 case RecordingAgent.Type.CanvasWebGL:
66 type = WI.Recording.Type.CanvasWebGL;
69 type = String(payload.type);
73 if (typeof payload.initialState !== "object" || payload.initialState === null)
74 payload.initialState = {};
75 if (typeof payload.initialState.attributes !== "object" || payload.initialState.attributes === null)
76 payload.initialState.attributes = {};
77 if (!Array.isArray(payload.initialState.states) || payload.initialState.states.some((item) => typeof item !== "object" || item === null)) {
78 payload.initialState.states = [];
80 // COMPATIBILITY (iOS 12.0): Recording.InitialState.states did not exist yet
81 if (!isEmptyObject(payload.initialState.attributes)) {
82 let {width, height, ...state} = payload.initialState.attributes;
83 if (!isEmptyObject(state))
84 payload.initialState.states.push(state);
87 if (!Array.isArray(payload.initialState.parameters))
88 payload.initialState.parameters = [];
89 if (typeof payload.initialState.content !== "string")
90 payload.initialState.content = "";
92 if (!Array.isArray(payload.frames))
95 if (!Array.isArray(payload.data))
99 frames = payload.frames.map(WI.RecordingFrame.fromPayload)
101 return new WI.Recording(payload.version, type, payload.initialState, frames, payload.data);
104 static displayNameForSwizzleType(swizzleType)
106 switch (swizzleType) {
107 case WI.Recording.Swizzle.None:
108 return WI.unlocalizedString("None");
109 case WI.Recording.Swizzle.Number:
110 return WI.unlocalizedString("Number");
111 case WI.Recording.Swizzle.Boolean:
112 return WI.unlocalizedString("Boolean");
113 case WI.Recording.Swizzle.String:
114 return WI.unlocalizedString("String");
115 case WI.Recording.Swizzle.Array:
116 return WI.unlocalizedString("Array");
117 case WI.Recording.Swizzle.TypedArray:
118 return WI.unlocalizedString("TypedArray");
119 case WI.Recording.Swizzle.Image:
120 return WI.unlocalizedString("Image");
121 case WI.Recording.Swizzle.ImageData:
122 return WI.unlocalizedString("ImageData");
123 case WI.Recording.Swizzle.DOMMatrix:
124 return WI.unlocalizedString("DOMMatrix");
125 case WI.Recording.Swizzle.Path2D:
126 return WI.unlocalizedString("Path2D");
127 case WI.Recording.Swizzle.CanvasGradient:
128 return WI.unlocalizedString("CanvasGradient");
129 case WI.Recording.Swizzle.CanvasPattern:
130 return WI.unlocalizedString("CanvasPattern");
131 case WI.Recording.Swizzle.WebGLBuffer:
132 return WI.unlocalizedString("WebGLBuffer");
133 case WI.Recording.Swizzle.WebGLFramebuffer:
134 return WI.unlocalizedString("WebGLFramebuffer");
135 case WI.Recording.Swizzle.WebGLRenderbuffer:
136 return WI.unlocalizedString("WebGLRenderbuffer");
137 case WI.Recording.Swizzle.WebGLTexture:
138 return WI.unlocalizedString("WebGLTexture");
139 case WI.Recording.Swizzle.WebGLShader:
140 return WI.unlocalizedString("WebGLShader");
141 case WI.Recording.Swizzle.WebGLProgram:
142 return WI.unlocalizedString("WebGLProgram");
143 case WI.Recording.Swizzle.WebGLUniformLocation:
144 return WI.unlocalizedString("WebGLUniformLocation");
145 case WI.Recording.Swizzle.ImageBitmap:
146 return WI.unlocalizedString("ImageBitmap");
148 console.error("Unknown swizzle type", swizzleType);
153 static synthesizeError(message)
155 const target = WI.mainTarget;
156 const source = WI.ConsoleMessage.MessageSource.Other;
157 const level = WI.ConsoleMessage.MessageLevel.Error;
158 let consoleMessage = new WI.ConsoleMessage(target, source, level, WI.UIString("Recording error: %s").format(message));
159 consoleMessage.shouldRevealConsole = true;
161 WI.consoleLogViewController.appendConsoleMessage(consoleMessage);
166 get displayName() { return this._displayName; }
167 get type() { return this._type; }
168 get initialState() { return this._initialState; }
169 get frames() { return this._frames; }
170 get data() { return this._data; }
171 get actions() { return this._actions; }
172 get visualActionIndexes() { return this._visualActionIndexes; }
174 get source() { return this._source; }
175 set source(source) { this._source = source; }
177 get processing() { return this._processing; }
181 return this._actions.lastValue.ready;
186 console.assert(!this._processing, "Cannot start an already started process().");
187 console.assert(!this.ready, "Cannot start a completed process().");
188 if (this._processing || this.ready)
191 this._processing = true;
198 console.assert(this._processing, "Cannot stop an already stopped process().");
199 console.assert(!this.ready, "Cannot stop a completed process().");
200 if (!this._processing || this.ready)
203 this._processing = false;
206 createDisplayName(suggestedName)
208 let recordingNameSet;
210 recordingNameSet = this._source[WI.Recording.CanvasRecordingNamesSymbol];
211 if (!recordingNameSet)
212 this._source[WI.Recording.CanvasRecordingNamesSymbol] = recordingNameSet = new Set;
214 recordingNameSet = WI.Recording._importedRecordingNameSet;
218 name = suggestedName;
219 let duplicateNumber = 2;
220 while (recordingNameSet.has(name))
221 name = `${suggestedName} (${duplicateNumber++})`;
223 let recordingNumber = 1;
225 name = WI.UIString("Recording %d").format(recordingNumber++);
226 } while (recordingNameSet.has(name));
229 recordingNameSet.add(name);
230 this._displayName = name;
233 async swizzle(index, type)
235 if (typeof this._swizzle[index] !== "object")
236 this._swizzle[index] = {};
238 if (type === WI.Recording.Swizzle.Number)
239 return parseFloat(index);
241 if (type === WI.Recording.Swizzle.Boolean)
244 if (type === WI.Recording.Swizzle.Array)
245 return Array.isArray(index) ? index : [];
247 if (type === WI.Recording.Swizzle.DOMMatrix)
248 return new DOMMatrix(index);
250 // FIXME: <https://webkit.org/b/176009> Web Inspector: send data for WebGL objects during a recording instead of a placeholder string
251 if (type === WI.Recording.Swizzle.TypedArray
252 || type === WI.Recording.Swizzle.WebGLBuffer
253 || type === WI.Recording.Swizzle.WebGLFramebuffer
254 || type === WI.Recording.Swizzle.WebGLRenderbuffer
255 || type === WI.Recording.Swizzle.WebGLTexture
256 || type === WI.Recording.Swizzle.WebGLShader
257 || type === WI.Recording.Swizzle.WebGLProgram
258 || type === WI.Recording.Swizzle.WebGLUniformLocation) {
262 if (!(type in this._swizzle[index])) {
264 let data = this._data[index];
266 case WI.Recording.Swizzle.None:
267 this._swizzle[index][type] = data;
270 case WI.Recording.Swizzle.String:
271 this._swizzle[index][type] = String(data);
274 case WI.Recording.Swizzle.Image:
275 this._swizzle[index][type] = await WI.ImageUtilities.promisifyLoad(data);
278 case WI.Recording.Swizzle.ImageData:
279 this._swizzle[index][type] = new ImageData(new Uint8ClampedArray(data[0]), parseInt(data[1]), parseInt(data[2]));
282 case WI.Recording.Swizzle.Path2D:
283 this._swizzle[index][type] = new Path2D(data);
286 case WI.Recording.Swizzle.CanvasGradient:
287 var gradientType = await this.swizzle(data[0], WI.Recording.Swizzle.String);
289 WI.ImageUtilities.scratchCanvasContext2D((context) => {
290 this._swizzle[index][type] = gradientType === "radial-gradient" ? context.createRadialGradient(...data[1]) : context.createLinearGradient(...data[1]);
293 for (let stop of data[2]) {
294 let color = await this.swizzle(stop[1], WI.Recording.Swizzle.String);
295 this._swizzle[index][type].addColorStop(stop[0], color);
299 case WI.Recording.Swizzle.CanvasPattern:
300 var [image, repeat] = await Promise.all([
301 this.swizzle(data[0], WI.Recording.Swizzle.Image),
302 this.swizzle(data[1], WI.Recording.Swizzle.String),
305 WI.ImageUtilities.scratchCanvasContext2D((context) => {
306 this._swizzle[index][type] = context.createPattern(image, repeat);
307 this._swizzle[index][type].__image = image;
311 case WI.Recording.Swizzle.ImageBitmap:
312 var image = await this.swizzle(index, WI.Recording.Swizzle.Image);
313 this._swizzle[index][type] = await createImageBitmap(image);
316 case WI.Recording.Swizzle.CallStack: {
317 let array = await this.swizzle(data, WI.Recording.Swizzle.Array);
318 this._swizzle[index][type] = await Promise.all(array.map((item) => this.swizzle(item, WI.Recording.Swizzle.CallFrame)));
322 case WI.Recording.Swizzle.CallFrame: {
323 let array = await this.swizzle(data, WI.Recording.Swizzle.Array);
324 let [functionName, url] = await Promise.all([
325 this.swizzle(array[0], WI.Recording.Swizzle.String),
326 this.swizzle(array[1], WI.Recording.Swizzle.String),
328 this._swizzle[index][type] = WI.CallFrame.fromPayload(WI.assumingMainTarget(), {
331 lineNumber: array[2],
332 columnNumber: array[3],
340 return this._swizzle[index][type];
345 let createCanvasContext = (type) => {
346 let canvas = document.createElement("canvas");
347 if ("width" in this._initialState.attributes)
348 canvas.width = this._initialState.attributes.width;
349 if ("height" in this._initialState.attributes)
350 canvas.height = this._initialState.attributes.height;
351 return canvas.getContext(type, ...this._initialState.parameters);
354 if (this._type === WI.Recording.Type.Canvas2D)
355 return createCanvasContext("2d");
357 if (this._type === WI.Recording.Type.BitmapRenderer)
358 return createCanvasContext("bitmaprenderer");
360 if (this._type === WI.Recording.Type.CanvasWebGL)
361 return createCanvasContext("webgl");
363 console.error("Unknown recording type", this._type);
369 let initialState = {};
370 if (!isEmptyObject(this._initialState.attributes))
371 initialState.attributes = this._initialState.attributes;
372 if (this._initialState.states.length)
373 initialState.states = this._initialState.states;
374 if (this._initialState.parameters.length)
375 initialState.parameters = this._initialState.parameters;
376 if (this._initialState.content && this._initialState.content.length)
377 initialState.content = this._initialState.content;
380 version: this._version,
383 frames: this._frames.map((frame) => frame.toJSON()),
392 if (!this._processContext) {
393 this._processContext = this.createContext();
395 if (this._type === WI.Recording.Type.Canvas2D) {
396 let initialContent = await WI.ImageUtilities.promisifyLoad(this._initialState.content);
397 this._processContext.drawImage(initialContent, 0, 0);
399 for (let initialState of this._initialState.states) {
400 let state = await WI.RecordingState.swizzleInitialState(this, initialState);
401 state.apply(this._type, this._processContext);
403 // The last state represents the current state, which should not be saved.
404 if (initialState !== this._initialState.states.lastValue) {
405 this._processContext.save();
406 this._processStates.push(WI.RecordingState.fromContext(this._type, this._processContext));
412 // The first action is always a WI.RecordingInitialStateAction, which doesn't need to swizzle().
413 // Since it is not associated with a WI.RecordingFrame, it has to manually process().
414 if (!this._actions[0].ready) {
415 this._actions[0].process(this, this._processContext, this._processStates);
416 this.dispatchEventToListeners(WI.Recording.Event.ProcessedAction, {action: this._actions[0], index: 0});
419 const workInterval = 10;
420 let startTime = Date.now();
422 let cumulativeActionIndex = 0;
423 let lastAction = this._actions[cumulativeActionIndex];
424 for (let frameIndex = 0; frameIndex < this._frames.length; ++frameIndex) {
425 let frame = this._frames[frameIndex];
427 if (frame.actions.lastValue.ready) {
428 cumulativeActionIndex += frame.actions.length;
429 lastAction = frame.actions.lastValue;
433 for (let actionIndex = 0; actionIndex < frame.actions.length; ++actionIndex) {
434 ++cumulativeActionIndex;
436 let action = frame.actions[actionIndex];
442 await action.swizzle(this);
444 action.process(this, this._processContext, this._processStates, {lastAction});
447 this._visualActionIndexes.push(cumulativeActionIndex);
450 this.dispatchEventToListeners(WI.Recording.Event.StartProcessingFrame, {frame, index: frameIndex});
452 this.dispatchEventToListeners(WI.Recording.Event.ProcessedAction, {action, index: cumulativeActionIndex});
454 if (Date.now() - startTime > workInterval) {
455 await Promise.delay(); // yield
457 startTime = Date.now();
462 if (!this._processing)
466 if (!this._processing)
470 this._processContext = null;
471 this._processing = false;
475 WI.Recording.Event = {
476 ProcessedAction: "recording-processed-action",
477 StartProcessingFrame: "recording-start-processing-frame",
480 WI.Recording._importedRecordingNameSet = new Set;
482 WI.Recording.CanvasRecordingNamesSymbol = Symbol("canvas-recording-names");
484 WI.Recording.Type = {
485 Canvas2D: "canvas-2d",
486 CanvasBitmapRenderer: "canvas-bitmaprenderer",
487 CanvasWebGL: "canvas-webgl",
490 // Keep this in sync with WebCore::RecordingSwizzleTypes.
491 WI.Recording.Swizzle = {
505 WebGLFramebuffer: 13,
506 WebGLRenderbuffer: 14,
510 WebGLUniformLocation: 18,
513 // Special frontend-only swizzle types.
514 CallStack: Symbol("CallStack"),
515 CallFrame: Symbol("CallFrame"),