Web Inspector: Canvas tab: determine hasVisibleEffect for all actions immediately...
authorwebkit@devinrousso.com <webkit@devinrousso.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 1 May 2018 23:37:59 +0000 (23:37 +0000)
committerwebkit@devinrousso.com <webkit@devinrousso.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 1 May 2018 23:37:59 +0000 (23:37 +0000)
https://bugs.webkit.org/show_bug.cgi?id=182995

Reviewed by Matt Baker.

Source/WebInspectorUI:

Previously, we'd swizzle the entirety of the `WI.Recording` in one, which would usually
freeze the UI, especially for larger recordings. This patch uses `WI.YieldableTask` to split
the work and allow the rest of the UI to still be usable while `WI.Recording` are processing.
Additionally, since we no longer have to worry about hangs, we can do more work upfront,
such as calculating `hasVisibleEffect` and the current state of 2D canvases.

These changes require that all uses of `WI.Recording` call `process()` before attempting to
use any `frames`/`actions`/`initialState`, as they will have their original payload values
and will have not been swizzled or applied.

* Localizations/en.lproj/localizedStrings.js:

* UserInterface/Models/Recording.js:
(WI.Recording):
(WI.Recording.prototype.process):
(WI.Recording.prototype.createContext): Added.
(WI.Recording.prototype.async yieldableTaskWillProcessItem): Added.
(WI.Recording.prototype.async yieldableTaskDidFinish): Added.

* UserInterface/Models/RecordingAction.js:
(WI.RecordingAction):
(WI.RecordingAction.prototype.process): Added.
(WI.RecordingAction.prototype.async swizzle): Added.
(WI.RecordingAction.prototype.apply):
(WI.RecordingAction.prototype.toJSON):
(WI.RecordingAction.prototype.set state): Deleted.
(WI.RecordingAction.prototype.swizzle): Deleted.
(WI.RecordingAction.prototype.apply.getContent): Deleted.
(WI.RecordingAction.prototype.async _swizzle): Deleted.
* UserInterface/Models/RecordingInitialStateAction.js:
(WI.RecordingInitialStateAction):

* UserInterface/Views/CanvasSidebarPanel.js:
(WI.CanvasSidebarPanel):
(WI.CanvasSidebarPanel.prototype.set action):
(WI.CanvasSidebarPanel.prototype._treeOutlineSelectionDidChange):
(WI.CanvasSidebarPanel.prototype._recordingChanged):

* UserInterface/Views/CanvasSidebarPanel.css:
(.sidebar > .panel.navigation.canvas > .content > .recording-content > .indeterminate-progress-spinner):

* UserInterface/Views/RecordingActionTreeElement.js:
(WI.RecordingActionTreeElement):
(WI.RecordingActionTreeElement.prototype.onattach):
(WI.RecordingActionTreeElement.prototype._handleHasVisibleEffectChanged): Deleted.

* UserInterface/Views/RecordingContentView.js:
(WI.RecordingContentView):
(WI.RecordingContentView.prototype.get navigationItems):
(WI.RecordingContentView.prototype.updateActionIndex):
(WI.RecordingContentView.prototype.initialLayout):
(WI.RecordingContentView.prototype._generateContentCanvas2D): Added.
(WI.RecordingContentView.prototype._generateContentCanvasWebGL): Added.
(WI.RecordingContentView.prototype._updateCanvasPath):
(WI.RecordingContentView.prototype._updateProcessProgress): Added.
(WI.RecordingContentView.prototype._handleRecordingProcessedActionSwizzle): Added.
(WI.RecordingContentView.prototype._handleRecordingProcessedActionApply): Added.
(WI.RecordingContentView.supportsCanvasPathDebugging): Deleted.
(WI.RecordingContentView.prototype.async _generateContentCanvas2D): Deleted.
(WI.RecordingContentView.prototype.async _generateContentCanvasWebGL): Deleted.

* UserInterface/Views/RecordingContentView.css:
(.content-view:not(.tab).recording > .preview-container):

* UserInterface/Base/ImageUtilities.js:
(WI.ImageUtilities.supportsCanvasPathDebugging):

LayoutTests:

* inspector/canvas/resources/recording-utilities.js:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@231218 268f45cc-cd09-0410-ab3c-d52691b4dbfc

13 files changed:
LayoutTests/ChangeLog
LayoutTests/inspector/canvas/resources/recording-utilities.js
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Base/ImageUtilities.js
Source/WebInspectorUI/UserInterface/Models/Recording.js
Source/WebInspectorUI/UserInterface/Models/RecordingAction.js
Source/WebInspectorUI/UserInterface/Models/RecordingInitialStateAction.js
Source/WebInspectorUI/UserInterface/Views/CanvasSidebarPanel.css
Source/WebInspectorUI/UserInterface/Views/CanvasSidebarPanel.js
Source/WebInspectorUI/UserInterface/Views/RecordingActionTreeElement.js
Source/WebInspectorUI/UserInterface/Views/RecordingContentView.css
Source/WebInspectorUI/UserInterface/Views/RecordingContentView.js

index 8357fb5..dc1691e 100644 (file)
@@ -1,3 +1,12 @@
+2018-05-01  Devin Rousso  <webkit@devinrousso.com>
+
+        Web Inspector: Canvas tab: determine hasVisibleEffect for all actions immediately after recording is added
+        https://bugs.webkit.org/show_bug.cgi?id=182995
+
+        Reviewed by Matt Baker.
+
+        * inspector/canvas/resources/recording-utilities.js:
+
 2018-05-01  Ryan Haddad  <ryanhaddad@apple.com>
 
         Unreviewed test gardening, correct a typo in the iOS TestExpectation file.
index 80c48a3..4b35ce1 100644 (file)
@@ -124,7 +124,7 @@ TestPage.registerInitializer(() => {
             InspectorTest.assert(recording.source.recordingCollection.items.has(recording), "Recording should be in the canvas' list of recordings.");
             InspectorTest.assert(recording.frames.length === frameCount, `Recording should have ${frameCount} frames.`)
 
-            return recording.actions.then(() => {
+            return Promise.all(recording.actions.map((action) => action.swizzle(recording))).then(() => {
                 logRecording(recording, type);
             });
         }).then(resolve, reject);
index b462072..475f11d 100644 (file)
@@ -1,3 +1,77 @@
+2018-05-01  Devin Rousso  <webkit@devinrousso.com>
+
+        Web Inspector: Canvas tab: determine hasVisibleEffect for all actions immediately after recording is added
+        https://bugs.webkit.org/show_bug.cgi?id=182995
+
+        Reviewed by Matt Baker.
+
+        Previously, we'd swizzle the entirety of the `WI.Recording` in one, which would usually
+        freeze the UI, especially for larger recordings. This patch uses `WI.YieldableTask` to split
+        the work and allow the rest of the UI to still be usable while `WI.Recording` are processing.
+        Additionally, since we no longer have to worry about hangs, we can do more work upfront,
+        such as calculating `hasVisibleEffect` and the current state of 2D canvases.
+
+        These changes require that all uses of `WI.Recording` call `process()` before attempting to
+        use any `frames`/`actions`/`initialState`, as they will have their original payload values
+        and will have not been swizzled or applied.
+
+        * Localizations/en.lproj/localizedStrings.js:
+
+        * UserInterface/Models/Recording.js:
+        (WI.Recording):
+        (WI.Recording.prototype.process):
+        (WI.Recording.prototype.createContext): Added.
+        (WI.Recording.prototype.async yieldableTaskWillProcessItem): Added.
+        (WI.Recording.prototype.async yieldableTaskDidFinish): Added.
+
+        * UserInterface/Models/RecordingAction.js:
+        (WI.RecordingAction):
+        (WI.RecordingAction.prototype.process): Added.
+        (WI.RecordingAction.prototype.async swizzle): Added.
+        (WI.RecordingAction.prototype.apply):
+        (WI.RecordingAction.prototype.toJSON):
+        (WI.RecordingAction.prototype.set state): Deleted.
+        (WI.RecordingAction.prototype.swizzle): Deleted.
+        (WI.RecordingAction.prototype.apply.getContent): Deleted.
+        (WI.RecordingAction.prototype.async _swizzle): Deleted.
+        * UserInterface/Models/RecordingInitialStateAction.js:
+        (WI.RecordingInitialStateAction):
+
+        * UserInterface/Views/CanvasSidebarPanel.js:
+        (WI.CanvasSidebarPanel):
+        (WI.CanvasSidebarPanel.prototype.set action):
+        (WI.CanvasSidebarPanel.prototype._treeOutlineSelectionDidChange):
+        (WI.CanvasSidebarPanel.prototype._recordingChanged):
+
+        * UserInterface/Views/CanvasSidebarPanel.css:
+        (.sidebar > .panel.navigation.canvas > .content > .recording-content > .indeterminate-progress-spinner):
+
+        * UserInterface/Views/RecordingActionTreeElement.js:
+        (WI.RecordingActionTreeElement):
+        (WI.RecordingActionTreeElement.prototype.onattach):
+        (WI.RecordingActionTreeElement.prototype._handleHasVisibleEffectChanged): Deleted.
+
+        * UserInterface/Views/RecordingContentView.js:
+        (WI.RecordingContentView):
+        (WI.RecordingContentView.prototype.get navigationItems):
+        (WI.RecordingContentView.prototype.updateActionIndex):
+        (WI.RecordingContentView.prototype.initialLayout):
+        (WI.RecordingContentView.prototype._generateContentCanvas2D): Added.
+        (WI.RecordingContentView.prototype._generateContentCanvasWebGL): Added.
+        (WI.RecordingContentView.prototype._updateCanvasPath):
+        (WI.RecordingContentView.prototype._updateProcessProgress): Added.
+        (WI.RecordingContentView.prototype._handleRecordingProcessedActionSwizzle): Added.
+        (WI.RecordingContentView.prototype._handleRecordingProcessedActionApply): Added.
+        (WI.RecordingContentView.supportsCanvasPathDebugging): Deleted.
+        (WI.RecordingContentView.prototype.async _generateContentCanvas2D): Deleted.
+        (WI.RecordingContentView.prototype.async _generateContentCanvasWebGL): Deleted.
+
+        * UserInterface/Views/RecordingContentView.css:
+        (.content-view:not(.tab).recording > .preview-container):
+
+        * UserInterface/Base/ImageUtilities.js:
+        (WI.ImageUtilities.supportsCanvasPathDebugging):
+
 2018-04-26  Jer Noble  <jer.noble@apple.com>
 
         Unreviewed build fix; fix WebInspectorUI copy resources step after r231063.
index c2e7195..803d9d4 100644 (file)
@@ -562,6 +562,7 @@ localizedStrings["Live"] = "Live";
 localizedStrings["Live Size"] = "Live Size";
 localizedStrings["Load \u2014 %s"] = "Load \u2014 %s";
 localizedStrings["Load cancelled"] = "Load cancelled";
+localizedStrings["Loading Recording"] = "Loading Recording";
 localizedStrings["Local File"] = "Local File";
 localizedStrings["Local Storage"] = "Local Storage";
 localizedStrings["Local Variables"] = "Local Variables";
@@ -723,6 +724,7 @@ localizedStrings["Probe Expression"] = "Probe Expression";
 localizedStrings["Probe Sample Recorded"] = "Probe Sample Recorded";
 localizedStrings["Probes"] = "Probes";
 localizedStrings["Processing Instruction"] = "Processing Instruction";
+localizedStrings["Processing Recording"] = "Processing Recording";
 localizedStrings["Program %d"] = "Program %d";
 localizedStrings["Properties"] = "Properties";
 localizedStrings["Property"] = "Property";
index fb19b42..0f9f6cc 100644 (file)
@@ -126,6 +126,14 @@ WI.ImageUtilities = class ImageUtilities {
         });
         return image;
     }
+
+    static supportsCanvasPathDebugging()
+    {
+        return "getPath" in CanvasRenderingContext2D.prototype
+            && "setPath" in CanvasRenderingContext2D.prototype
+            && "currentX" in CanvasRenderingContext2D.prototype
+            && "currentY" in CanvasRenderingContext2D.prototype;
+    }
 };
 
 WI.ImageUtilities._scratchContext2D = null;
index 58238cd..23f9fd5 100644 (file)
  * THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-WI.Recording = class Recording
+WI.Recording = class Recording extends WI.Object
 {
     constructor(version, type, initialState, frames, data)
     {
+        super();
+
         this._version = version;
         this._type = type;
         this._initialState = initialState;
@@ -35,37 +37,14 @@ WI.Recording = class Recording
         this._displayName = WI.UIString("Recording");
 
         this._swizzle = [];
+        this._actions = [new WI.RecordingInitialStateAction].concat(...this._frames.map((frame) => frame.actions));
         this._visualActionIndexes = [];
         this._source = null;
 
-        let actions = [new WI.RecordingInitialStateAction].concat(...this._frames.map((frame) => frame.actions));
-        this._actions = Promise.all(actions.map((action) => action.swizzle(this))).then(() => {
-            actions.forEach((action, index) => {
-                if (!action.valid)
-                    return;
-
-                let prototype = null;
-                if (this._type === WI.Recording.Type.Canvas2D)
-                    prototype = CanvasRenderingContext2D.prototype;
-                else if (this._type === WI.Recording.Type.CanvasWebGL)
-                    prototype = WebGLRenderingContext.prototype;
-
-                if (prototype) {
-                    let validName = action.name in prototype;
-                    let validFunction = !action.isFunction || typeof prototype[action.name] === "function";
-                    if (!validName || !validFunction) {
-                        action.markInvalid();
-
-                        WI.Recording.synthesizeError(WI.UIString("“%s” is invalid.").format(this._name));
-                    }
-                }
-
-                if (action.isVisual)
-                    this._visualActionIndexes.push(index);
-            });
-
-            return actions;
-        });
+        this._swizzleTask = null;
+        this._applyTask = null;
+        this._processContext = null;
+        this._processPromise = null;
     }
 
     static fromPayload(payload, frames)
@@ -177,13 +156,26 @@ WI.Recording = class Recording
     get initialState() { return this._initialState; }
     get frames() { return this._frames; }
     get data() { return this._data; }
-    get visualActionIndexes() { return this._visualActionIndexes; }
-
     get actions() { return this._actions; }
+    get visualActionIndexes() { return this._visualActionIndexes; }
 
     get source() { return this._source; }
     set source(source) { this._source = source; }
 
+    process()
+    {
+        if (!this._processPromise) {
+            this._processPromise = new WI.WrappedPromise;
+
+            let items = this._actions.map((action, index) => { return {action, index} });
+            this._swizzleTask = new WI.YieldableTask(this, items);
+            this._applyTask = new WI.YieldableTask(this, items);
+
+            this._swizzleTask.start();
+        }
+        return this._processPromise.promise;
+    }
+
     createDisplayName(suggestedName)
     {
         let recordingNameSet;
@@ -300,6 +292,27 @@ WI.Recording = class Recording
         return this._swizzle[index][type];
     }
 
+    createContext()
+    {
+        let createCanvasContext = (type) => {
+            let canvas = document.createElement("canvas");
+            if ("width" in this._initialState.attributes)
+                canvas.width = this._initialState.attributes.width;
+            if ("height" in this._initialState.attributes)
+                canvas.height = this._initialState.attributes.height;
+            return canvas.getContext(type, ...this._initialState.parameters);
+        };
+
+        if (this._type === WI.Recording.Type.Canvas2D)
+            return createCanvasContext("2d");
+
+        if (this._type === WI.Recording.Type.CanvasWebGL)
+            return createCanvasContext("webgl");
+
+        console.error("Unknown recording type", this._type);
+        return null;
+    }
+
     toJSON()
     {
         let initialState = {};
@@ -318,6 +331,99 @@ WI.Recording = class Recording
             data: this._data,
         };
     }
+
+    // YieldableTask delegate
+
+    async yieldableTaskWillProcessItem(task, item)
+    {
+        if (task === this._swizzleTask) {
+            await item.action.swizzle(this);
+
+            this.dispatchEventToListeners(WI.Recording.Event.ProcessedActionSwizzle, {index: item.index});
+        } else if (task === this._applyTask) {
+            item.action.process(this, this._processContext);
+
+            if (item.action.isVisual)
+                this._visualActionIndexes.push(item.index);
+
+            this.dispatchEventToListeners(WI.Recording.Event.ProcessedActionApply, {index: item.index});
+        }
+    }
+
+    async yieldableTaskDidFinish(task)
+    {
+        if (task === this._swizzleTask) {
+            this._swizzleTask = null;
+
+            this._processContext = this.createContext();
+
+            if (this._type === WI.Recording.Type.Canvas2D) {
+                let initialContent = await WI.ImageUtilities.promisifyLoad(this._initialState.content);
+                this._processContext.drawImage(initialContent, 0, 0);
+
+                for (let [key, value] of Object.entries(this._initialState.attributes)) {
+                    switch (key) {
+                    case "setTransform":
+                        value = [await this.swizzle(value, WI.Recording.Swizzle.DOMMatrix)];
+                        break;
+
+                    case "fillStyle":
+                    case "strokeStyle":
+                            let [gradient, pattern, string] = await Promise.all([
+                                this.swizzle(value, WI.Recording.Swizzle.CanvasGradient),
+                                this.swizzle(value, WI.Recording.Swizzle.CanvasPattern),
+                                this.swizzle(value, WI.Recording.Swizzle.String),
+                            ]);
+                            if (gradient && !pattern)
+                                value = gradient;
+                            else if (pattern && !gradient)
+                                value = pattern;
+                            else
+                                value = string;
+                        break;
+
+                    case "direction":
+                    case "font":
+                    case "globalCompositeOperation":
+                    case "imageSmoothingEnabled":
+                    case "imageSmoothingQuality":
+                    case "lineCap":
+                    case "lineJoin":
+                    case "shadowColor":
+                    case "textAlign":
+                    case "textBaseline":
+                        value = await this.swizzle(value, WI.Recording.Swizzle.String);
+                        break;
+
+                    case "setPath":
+                        value = [await this.swizzle(value[0], WI.Recording.Swizzle.Path2D)];
+                        break;
+                    }
+
+                    if (value === undefined || (Array.isArray(value) && value.includes(undefined)))
+                        continue;
+
+                    try {
+                        if (WI.RecordingAction.isFunctionForType(this._type, key))
+                            this._processContext[key](...value);
+                        else
+                            this._processContext[key] = value;
+                    } catch { }
+                }
+            }
+
+            this._applyTask.start();
+        } else if (task === this._applyTask) {
+            this._applyTask = null;
+            this._processContext = null;
+            this._processPromise.resolve();
+        }
+    }
+};
+
+WI.Recording.Event = {
+    ProcessedActionApply: "recording-processed-action-apply",
+    ProcessedActionSwizzle: "recording-processed-action-swizzle",
 };
 
 WI.Recording._importedRecordingNameSet = new Set;
index 27db60f..404c301 100644 (file)
@@ -41,12 +41,12 @@ WI.RecordingAction = class RecordingAction extends WI.Object
         this._snapshot = "";
 
         this._valid = true;
-        this._swizzledPromise = null;
-
         this._isFunction = false;
         this._isGetter = false;
         this._isVisual = false;
         this._hasVisibleEffect = undefined;
+
+        this._state = null;
         this._stateModifiers = new Set;
     }
 
@@ -97,31 +97,19 @@ WI.RecordingAction = class RecordingAction extends WI.Object
     get isGetter() { return this._isGetter; }
     get isVisual() { return this._isVisual; }
     get hasVisibleEffect() { return this._hasVisibleEffect; }
-    get stateModifiers() { return this._stateModifiers; }
-
     get state() { return this._state; }
-    set state(state) { this._state = state; }
-
-    markInvalid()
-    {
-        let wasValid = this._valid;
-        this._valid = false;
-
-        if (wasValid)
-            this.dispatchEventToListeners(WI.RecordingAction.Event.ValidityChanged);
-    }
-
-    swizzle(recording)
-    {
-        if (!this._swizzledPromise)
-            this._swizzledPromise = this._swizzle(recording);
-        return this._swizzledPromise;
-    }
+    get stateModifiers() { return this._stateModifiers; }
 
-    apply(context, options = {})
+    process(recording, context)
     {
-        if (!this.valid)
+        if (recording.type === WI.Recording.Type.CanvasWebGL) {
+            // We add each RecordingAction to the list of visualActionIndexes after it is processed.
+            if (this._valid && this._isVisual) {
+                let contentBefore = recording.visualActionIndexes.length ? recording.visualActionIndexes.lastValue.snapshot : recording.initialState.content;
+                this._hasVisibleEffect = this._snapshot !== contentBefore;
+            }
             return;
+        }
 
         function getContent() {
             if (context instanceof CanvasRenderingContext2D) {
@@ -143,45 +131,56 @@ WI.RecordingAction = class RecordingAction extends WI.Object
         }
 
         let contentBefore = null;
-        let shouldCheckForChange = this._isVisual && this._hasVisibleEffect === undefined;
-        if (shouldCheckForChange)
+        if (this._valid && this._isVisual)
             contentBefore = getContent();
 
-        try {
-            let name = options.nameOverride || this._name;
-            if (this.isFunction)
-                context[name](...this._parameters);
-            else {
-                if (this.isGetter)
-                    context[name];
-                else
-                    context[name] = this._parameters[0];
-            }
-
-            if (shouldCheckForChange) {
-                this._hasVisibleEffect = !Array.shallowEqual(contentBefore, getContent());
-                if (!this._hasVisibleEffect)
-                    this.dispatchEventToListeners(WI.RecordingAction.Event.HasVisibleEffectChanged);
-            }
-        } catch {
-            this.markInvalid();
-
-            WI.Recording.synthesizeError(WI.UIString("“%s” threw an error.").format(this._name));
+        this.apply(context);
+
+        if (this._valid && this._isVisual)
+            this._hasVisibleEffect = !Array.shallowEqual(contentBefore, getContent());
+
+        if (recording.type === WI.Recording.Type.Canvas2D) {
+            let matrix = context.getTransform();
+
+            this._state = {
+                currentX: context.currentX,
+                currentY: context.currentY,
+                direction: context.direction,
+                fillStyle: context.fillStyle,
+                font: context.font,
+                globalAlpha: context.globalAlpha,
+                globalCompositeOperation: context.globalCompositeOperation,
+                imageSmoothingEnabled: context.imageSmoothingEnabled,
+                imageSmoothingQuality: context.imageSmoothingQuality,
+                lineCap: context.lineCap,
+                lineDash: context.getLineDash(),
+                lineDashOffset: context.lineDashOffset,
+                lineJoin: context.lineJoin,
+                lineWidth: context.lineWidth,
+                miterLimit: context.miterLimit,
+                shadowBlur: context.shadowBlur,
+                shadowColor: context.shadowColor,
+                shadowOffsetX: context.shadowOffsetX,
+                shadowOffsetY: context.shadowOffsetY,
+                strokeStyle: context.strokeStyle,
+                textAlign: context.textAlign,
+                textBaseline: context.textBaseline,
+                transform: [matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f],
+                webkitImageSmoothingEnabled: context.webkitImageSmoothingEnabled,
+                webkitLineDash: context.webkitLineDash,
+                webkitLineDashOffset: context.webkitLineDashOffset,
+            };
+
+            if (WI.ImageUtilities.supportsCanvasPathDebugging())
+                this._state.setPath = [context.getPath()];
         }
     }
 
-    toJSON()
+    async swizzle(recording)
     {
-        let json = [this._payloadName, this._payloadParameters, this._payloadSwizzleTypes, this._payloadTrace];
-        if (this._payloadSnapshot >= 0)
-            json.push(this._payloadSnapshot);
-        return json;
-    }
-
-    // Private
+        if (!this._valid)
+            return;
 
-    async _swizzle(recording)
-    {
         let swizzleParameter = (item, index) => {
             return recording.swizzle(item, this._payloadSwizzleTypes[index]);
         };
@@ -215,6 +214,30 @@ WI.RecordingAction = class RecordingAction extends WI.Object
         if (this._payloadSnapshot >= 0)
             this._snapshot = snapshot;
 
+        this._isFunction = WI.RecordingAction.isFunctionForType(recording.type, this._name);
+        this._isGetter = !this._isFunction && !this._parameters.length;
+
+        let visualNames = WI.RecordingAction._visualNames[recording.type];
+        this._isVisual = visualNames ? visualNames.has(this._name) : false;
+
+        if (this._valid) {
+            let prototype = null;
+            if (recording.type === WI.Recording.Type.Canvas2D)
+                prototype = CanvasRenderingContext2D.prototype;
+            else if (recording.type === WI.Recording.Type.CanvasWebGL)
+                prototype = WebGLRenderingContext.prototype;
+
+            if (prototype) {
+                let validName = name in prototype;
+                let validFunction = !this._isFunction || typeof prototype[name] === "function";
+                if (!validName || !validFunction) {
+                    this.markInvalid();
+
+                    WI.Recording.synthesizeError(WI.UIString("“%s” is invalid.").format(name));
+                }
+            }
+        }
+
         if (this._valid) {
             let parametersSpecified = this._parameters.every((parameter) => parameter !== undefined);
             let parametersCanBeSwizzled = this._payloadSwizzleTypes.every((swizzleType) => swizzleType !== WI.Recording.Swizzle.None);
@@ -222,21 +245,49 @@ WI.RecordingAction = class RecordingAction extends WI.Object
                 this.markInvalid();
         }
 
-        this._isFunction = WI.RecordingAction.isFunctionForType(recording.type, this._name);
-        this._isGetter = !this._isFunction && !this._parameters.length;
+        if (this._valid) {
+            let stateModifiers = WI.RecordingAction._stateModifiers[recording.type];
+            if (stateModifiers) {
+                this._stateModifiers.add(this._name);
+                let modifiedByAction = stateModifiers[this._name] || [];
+                for (let item of modifiedByAction)
+                    this._stateModifiers.add(item);
+            }
+        }
+    }
 
-        let visualNames = WI.RecordingAction._visualNames[recording.type];
-        this._isVisual = visualNames ? visualNames.has(this._name) : false;
+    apply(context, options = {})
+    {
+        if (!this.valid)
+            return;
 
-        this._stateModifiers = new Set([this._name]);
-        let stateModifiers = WI.RecordingAction._stateModifiers[recording.type];
-        if (stateModifiers) {
-            let modifiedByAction = stateModifiers[this._name] || [];
-            for (let item of modifiedByAction)
-                this._stateModifiers.add(item);
+        try {
+            let name = options.nameOverride || this._name;
+            if (this.isFunction)
+                context[name](...this._parameters);
+            else {
+                if (this.isGetter)
+                    context[name];
+                else
+                    context[name] = this._parameters[0];
+            }
+        } catch {
+            this.markInvalid();
+
+            WI.Recording.synthesizeError(WI.UIString("“%s” threw an error.").format(this._name));
         }
     }
 
+    markInvalid()
+    {
+        if (!this._valid)
+            return;
+
+        this._valid = false;
+
+        this.dispatchEventToListeners(WI.RecordingAction.Event.ValidityChanged);
+    }
+
     getColorParameters()
     {
         switch (this._name) {
@@ -277,6 +328,14 @@ WI.RecordingAction = class RecordingAction extends WI.Object
 
         return [];
     }
+
+    toJSON()
+    {
+        let json = [this._payloadName, this._payloadParameters, this._payloadSwizzleTypes, this._payloadTrace];
+        if (this._payloadSnapshot >= 0)
+            json.push(this._payloadSnapshot);
+        return json;
+    }
 };
 
 WI.RecordingAction.Event = {
@@ -310,6 +369,7 @@ WI.RecordingAction._functionNames = {
         "fillText",
         "getImageData",
         "getLineDash",
+        "getPath",
         "isPointInPath",
         "isPointInPath",
         "isPointInStroke",
@@ -333,6 +393,7 @@ WI.RecordingAction._functionNames = {
         "setLineJoin",
         "setLineWidth",
         "setMiterLimit",
+        "setPath",
         "setShadow",
         "setStrokeColor",
         "setTransform",
index 8bc518e..d549854 100644 (file)
@@ -32,6 +32,5 @@ WI.RecordingInitialStateAction = class RecordingInitialStateAction extends WI.Re
         this._name = WI.UIString("Initial State");
 
         this._valid = false;
-        this._swizzledPromise = Promise.resolve();
     }
 };
index 3ea897f..cab3dbc 100644 (file)
@@ -71,3 +71,7 @@
 .sidebar > .panel.navigation.canvas > .content > .recording-content > .tree-outline .item.folder-icon > .status {
     line-height: 16px;
 }
+
+.sidebar > .panel.navigation.canvas > .content > .recording-content > .indeterminate-progress-spinner {
+    margin: 16px auto;
+}
index 440dd6e..67fc08d 100644 (file)
@@ -52,20 +52,23 @@ WI.CanvasSidebarPanel = class CanvasSidebarPanel extends WI.NavigationSidebarPan
         this._recordingNavigationBar.element.classList.add("hidden");
         this.contentView.addSubview(this._recordingNavigationBar);
 
-        let recordingContent = this.contentView.element.appendChild(document.createElement("div"));
-        recordingContent.className = "recording-content";
+        this._recordingContentContainer = this.contentView.element.appendChild(document.createElement("div"));
+        this._recordingContentContainer.className = "recording-content";
 
         this._recordingTreeOutline = this.contentTreeOutline;
-        recordingContent.appendChild(this._recordingTreeOutline.element);
+        this._recordingContentContainer.appendChild(this._recordingTreeOutline.element);
 
         this._recordingTreeOutline.customIndent = true;
-        this._recordingTreeOutline.registerScrollVirtualizer(recordingContent, 20);
+        this._recordingTreeOutline.registerScrollVirtualizer(this._recordingContentContainer, 20);
 
         this._canvasTreeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._treeOutlineSelectionDidChange, this);
         this._recordingTreeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._treeOutlineSelectionDidChange, this);
 
         WI.canvasManager.addEventListener(WI.CanvasManager.Event.RecordingStarted, this._updateRecordNavigationItem, this);
         WI.canvasManager.addEventListener(WI.CanvasManager.Event.RecordingStopped, this._updateRecordNavigationItem, this);
+
+        this._recordingProcessPromise = null;
+        this._recordingProcessSpinner = null;
     }
 
     // Public
@@ -108,7 +111,7 @@ WI.CanvasSidebarPanel = class CanvasSidebarPanel extends WI.NavigationSidebarPan
 
     set action(action)
     {
-        if (!this._recording)
+        if (!this._recording || this._recordingProcessPromise)
             return;
 
         let selectedTreeElement = this._recordingTreeOutline.selectedTreeElement;
@@ -286,7 +289,8 @@ WI.CanvasSidebarPanel = class CanvasSidebarPanel extends WI.NavigationSidebarPan
         console.assert(this._recording, "Missing recording for action tree element.", treeElement);
         this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol] = treeElement.representedObject;
 
-        let recordingContentView = this.contentBrowser.showContentViewForRepresentedObject(this._recording);
+        const onlyExisting = true;
+        let recordingContentView = this.contentBrowser.contentViewForRepresentedObject(this._recording, onlyExisting);
         if (recordingContentView)
             recordingContentView.updateActionIndex(treeElement.index);
     }
@@ -327,16 +331,28 @@ WI.CanvasSidebarPanel = class CanvasSidebarPanel extends WI.NavigationSidebarPan
         if (!this._recording)
             return;
 
+        if (!this._recordingProcessSpinner) {
+            this._recordingProcessSpinner = new WI.IndeterminateProgressSpinner;
+            this._recordingContentContainer.appendChild(this._recordingProcessSpinner.element);
+        }
+
+        this.contentBrowser.showContentViewForRepresentedObject(this._recording);
+
         let recording = this._recording;
 
-        this._recording.actions.then((actions) => {
-            if (recording !== this._recording)
+        let promise = this._recording.process().then(() => {
+            if (recording !== this._recording || promise !== this._recordingProcessPromise)
                 return;
 
-            this._recordingTreeOutline.element.dataset.indent = Number.countDigits(actions.length);
+            if (this._recordingProcessSpinner) {
+                this._recordingProcessSpinner.element.remove();
+                this._recordingProcessSpinner = null;
+            }
+
+            this._recordingTreeOutline.element.dataset.indent = Number.countDigits(this._recording.actions.length);
 
-            if (actions[0] instanceof WI.RecordingInitialStateAction)
-                this._recordingTreeOutline.appendChild(new WI.RecordingActionTreeElement(actions[0], 0, this._recording.type));
+            if (this._recording.actions[0] instanceof WI.RecordingInitialStateAction)
+                this._recordingTreeOutline.appendChild(new WI.RecordingActionTreeElement(this._recording.actions[0], 0, this._recording.type));
 
             let cumulativeActionIndex = 1;
             this._recording.frames.forEach((frame, frameIndex) => {
@@ -366,8 +382,12 @@ WI.CanvasSidebarPanel = class CanvasSidebarPanel extends WI.NavigationSidebarPan
                 scopeBarItem.selected = true;
             }
 
-            this.action = this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol] || actions[0];
+            this.action = this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol] || this._recording.actions[0];
+
+            this._recordingProcessPromise = null;
         });
+
+        this._recordingProcessPromise = promise;
     }
 
     _updateRecordNavigationItem()
index 13353a3..68d18f0 100644 (file)
@@ -39,7 +39,6 @@ WI.RecordingActionTreeElement = class RecordingActionTreeElement extends WI.Gene
         this._copyText = copyText;
 
         this.representedObject.addEventListener(WI.RecordingAction.Event.ValidityChanged, this._handleValidityChanged, this);
-        this.representedObject.addEventListener(WI.RecordingAction.Event.HasVisibleEffectChanged, this._handleHasVisibleEffectChanged, this);
     }
 
     // Static
@@ -399,6 +398,13 @@ WI.RecordingActionTreeElement = class RecordingActionTreeElement extends WI.Gene
         super.onattach();
 
         this.element.dataset.index = this._index.toLocaleString();
+
+        if (this.representedObject.valid && this.representedObject.isVisual && !this.representedObject.hasVisibleEffect) {
+            this.addClassName("no-visible-effect");
+
+            const title = WI.UIString("This action causes no visual change");
+            this.status = WI.ImageUtilities.useSVGSymbol("Images/Warning.svg", "warning", title);
+        }
     }
 
     populateContextMenu(contextMenu, event)
@@ -430,14 +436,6 @@ WI.RecordingActionTreeElement = class RecordingActionTreeElement extends WI.Gene
     {
         this.addClassName("invalid");
     }
-
-    _handleHasVisibleEffectChanged(event)
-    {
-        this.addClassName("no-visible-effect");
-
-        this.status = WI.ImageUtilities.useSVGSymbol("Images/Warning.svg", "warning");
-        this.status.title = WI.UIString("This action causes no visual change");
-    }
 };
 
 WI.RecordingActionTreeElement._memoizedActionClassNames = new Map;
index d224b38..7c9b4aa 100644 (file)
@@ -67,6 +67,9 @@
     flex: 1;
     justify-content: center;
     align-items: center;
+    position: relative;
+    width: -webkit-fill-available;
+    height: -webkit-fill-available;
 }
 
 .content-view:not(.tab).recording :matches(img, canvas) {
index f0ea465..97d6c7b 100644 (file)
@@ -42,7 +42,7 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
         let isCanvas2D = this.representedObject.type === WI.Recording.Type.Canvas2D;
         let isCanvasWebGL = this.representedObject.type === WI.Recording.Type.CanvasWebGL;
         if (isCanvas2D || isCanvasWebGL) {
-            if (isCanvas2D && WI.RecordingContentView.supportsCanvasPathDebugging()) {
+            if (isCanvas2D && WI.ImageUtilities.supportsCanvasPathDebugging()) {
                 this._pathContext = null;
 
                 this._showPathButtonNavigationItem = new WI.ActivateButtonNavigationItem("show-path", WI.UIString("Show Path"), WI.UIString("Hide Path"), "Images/Path.svg", 16, 16);
@@ -61,15 +61,13 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
             this._exportButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.High;
             this._exportButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => { this._exportRecording(); });
         }
+
+        this._processing = true;
+        this._processMessageTextView = null;
     }
 
     // Static
 
-    static supportsCanvasPathDebugging()
-    {
-        return "currentX" in CanvasRenderingContext2D.prototype && "currentY" in CanvasRenderingContext2D.prototype;
-    }
-
     static _actionModifiesPath(recordingAction)
     {
         switch (recordingAction.name) {
@@ -99,7 +97,7 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
             return [];
 
         let navigationItems = [this._exportButtonNavigationItem, new WI.DividerNavigationItem];
-        if (isCanvas2D && WI.RecordingContentView.supportsCanvasPathDebugging())
+        if (isCanvas2D && WI.ImageUtilities.supportsCanvasPathDebugging())
             navigationItems.push(this._showPathButtonNavigationItem);
 
         navigationItems.push(this._showGridButtonNavigationItem);
@@ -119,19 +117,25 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
         if (this._index === index)
             return;
 
-        this.representedObject.actions.then((actions) => {
-            console.assert(index >= 0 && index < actions.length);
-            if (index < 0 || index >= actions.length)
-                return;
+        console.assert(index >= 0 && index < this.representedObject.actions.length);
+        if (index < 0 || index >= this.representedObject.actions.length)
+            return;
 
-            this._index = index;
-            this._updateSliderValue();
+        this._index = index;
 
-            if (this.representedObject.type === WI.Recording.Type.Canvas2D)
-                this._throttler._generateContentCanvas2D(index, actions);
-            else if (this.representedObject.type === WI.Recording.Type.CanvasWebGL)
-                this._throttler._generateContentCanvasWebGL(index, actions);
-        });
+        if (this._processing)
+            return;
+
+        this._updateSliderValue();
+
+        if (this.representedObject.type === WI.Recording.Type.Canvas2D)
+            this._throttler._generateContentCanvas2D(index);
+        else if (this.representedObject.type === WI.Recording.Type.CanvasWebGL)
+            this._throttler._generateContentCanvasWebGL(index);
+
+        this._action = this.representedObject.actions[this._index];
+
+        this.dispatchEventToListeners(WI.ContentView.Event.SupplementalRepresentedObjectsDidChange);
     }
 
     shown()
@@ -188,10 +192,24 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
         this._sliderElement.min = 0;
         this._sliderElement.max = 0;
 
-        this.representedObject.actions.then(() => {
+        this.representedObject.addEventListener(WI.Recording.Event.ProcessedActionSwizzle, this._handleRecordingProcessedActionSwizzle, this);
+        this.representedObject.addEventListener(WI.Recording.Event.ProcessedActionApply, this._handleRecordingProcessedActionApply, this);
+
+        this.representedObject.process().then(() => {
+            if (this._processMessageTextView)
+                this._processMessageTextView.remove();
+
             sliderContainer.classList.remove("hidden");
             this._sliderElement.max = this.representedObject.visualActionIndexes.length;
             this._updateSliderValue();
+
+            this._processing = false;
+
+            let index = this._index;
+            if (!isNaN(index)) {
+                this._index = NaN;
+                this.updateActionIndex(index);
+            }
         });
     }
 
@@ -214,14 +232,14 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
         });
     }
 
-    async _generateContentCanvas2D(index, actions)
+    _generateContentCanvas2D(index)
     {
         let imageLoad = (event) => {
             // Loading took too long and the current action index has already changed.
             if (index !== this._index)
                 return;
 
-            this._generateContentCanvas2D(index, actions);
+            this._generateContentCanvas2D(index);
         };
 
         let initialState = this.representedObject.initialState;
@@ -235,9 +253,11 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
         let snapshotIndex = Math.floor(index / WI.RecordingContentView.SnapshotInterval);
         let snapshot = this._snapshots[snapshotIndex];
 
-        let showCanvasPath = WI.RecordingContentView.supportsCanvasPathDebugging() && WI.settings.showCanvasPath.value;
+        let showCanvasPath = WI.ImageUtilities.supportsCanvasPathDebugging() && WI.settings.showCanvasPath.value;
         let indexOfLastBeginPathAction = Infinity;
 
+        let actions = this.representedObject.actions;
+
         let applyActions = (from, to, callback) => {
             let saveCount = 0;
             snapshot.context.save();
@@ -331,43 +351,9 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
             } else if (this._pathContext)
                 this._pathContext.canvas.remove();
 
-            let state = {
-                currentX: snapshot.context.currentX,
-                currentY: snapshot.context.currentY,
-                direction: snapshot.context.direction,
-                fillStyle: snapshot.context.fillStyle,
-                font: snapshot.context.font,
-                globalAlpha: snapshot.context.globalAlpha,
-                globalCompositeOperation: snapshot.context.globalCompositeOperation,
-                imageSmoothingEnabled: snapshot.context.imageSmoothingEnabled,
-                imageSmoothingQuality: snapshot.context.imageSmoothingQuality,
-                lineCap: snapshot.context.lineCap,
-                lineDash: snapshot.context.getLineDash(),
-                lineDashOffset: snapshot.context.lineDashOffset,
-                lineJoin: snapshot.context.lineJoin,
-                lineWidth: snapshot.context.lineWidth,
-                miterLimit: snapshot.context.miterLimit,
-                shadowBlur: snapshot.context.shadowBlur,
-                shadowColor: snapshot.context.shadowColor,
-                shadowOffsetX: snapshot.context.shadowOffsetX,
-                shadowOffsetY: snapshot.context.shadowOffsetY,
-                strokeStyle: snapshot.context.strokeStyle,
-                textAlign: snapshot.context.textAlign,
-                textBaseline: snapshot.context.textBaseline,
-                transform: snapshot.context.getTransform(),
-                webkitImageSmoothingEnabled: snapshot.context.webkitImageSmoothingEnabled,
-                webkitLineDash: snapshot.context.webkitLineDash,
-                webkitLineDashOffset: snapshot.context.webkitLineDashOffset,
-            };
-
-            if (WI.RecordingContentView.supportsCanvasPathDebugging())
-                state.setPath = [snapshot.context.getPath()];
-
             snapshot.context.restore();
             while (saveCount-- > 0)
                 snapshot.context.restore();
-
-            return state;
         };
 
         if (!snapshot) {
@@ -376,12 +362,8 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
             while (snapshot.index && actions[snapshot.index].name !== "beginPath")
                 --snapshot.index;
 
-            snapshot.element = document.createElement("canvas");
-            snapshot.context = snapshot.element.getContext("2d", ...initialState.parameters);
-            if ("width" in initialState.attributes)
-                snapshot.element.width = initialState.attributes.width;
-            if ("height" in initialState.attributes)
-                snapshot.element.height = initialState.attributes.height;
+            snapshot.context = this.representedObject.createContext();
+            snapshot.element = snapshot.context.canvas;
 
             let lastSnapshotIndex = snapshotIndex;
             while (--lastSnapshotIndex >= 0) {
@@ -392,58 +374,16 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
             let startIndex = 0;
             if (lastSnapshotIndex < 0) {
                 snapshot.content = this._initialContent;
-                snapshot.state = {};
-
-                for (let key in initialState.attributes) {
-                    let value = initialState.attributes[key];
-
-                    switch (key) {
-                    case "setTransform":
-                        value = [await this.representedObject.swizzle(value, WI.Recording.Swizzle.DOMMatrix)];
-                        break;
-
-                    case "fillStyle":
-                    case "strokeStyle":
-                        if (Array.isArray(value)) {
-                            let canvasStyle = await this.representedObject.swizzle(value[0], WI.Recording.Swizzle.String);
-                            if (canvasStyle.includes("gradient"))
-                                value = await this.representedObject.swizzle(value, WI.Recording.Swizzle.CanvasGradient);
-                            else if (canvasStyle === "pattern")
-                                value = await this.representedObject.swizzle(value, WI.Recording.Swizzle.CanvasPattern);
-                        } else
-                            value = await this.representedObject.swizzle(value, WI.Recording.Swizzle.String);
-                        break;
-
-                    case "direction":
-                    case "font":
-                    case "globalCompositeOperation":
-                    case "imageSmoothingEnabled":
-                    case "imageSmoothingQuality":
-                    case "lineCap":
-                    case "lineJoin":
-                    case "shadowColor":
-                    case "textAlign":
-                    case "textBaseline":
-                        value = await this.representedObject.swizzle(value, WI.Recording.Swizzle.String);
-                        break;
-
-                    case "setPath":
-                        value = [await this.representedObject.swizzle(value[0], WI.Recording.Swizzle.Path2D)];
-                        break;
-                    }
-
-                    if (value === undefined || (Array.isArray(value) && value.includes(undefined)))
-                        continue;
-
-                    snapshot.state[key] = value;
-                }
+                snapshot.state = actions[0].state;
             } else {
                 snapshot.content = this._snapshots[lastSnapshotIndex].content;
                 snapshot.state = this._snapshots[lastSnapshotIndex].state;
                 startIndex = this._snapshots[lastSnapshotIndex].index;
             }
 
-            snapshot.state = applyActions(startIndex, snapshot.index - 1);
+            applyActions(startIndex, snapshot.index - 1);
+            if (snapshot.index > 0)
+                snapshot.state = actions[snapshot.index - 1].state;
 
             snapshot.content = new Image;
             snapshot.content.src = snapshot.element.toDataURL();
@@ -459,27 +399,20 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
                 --indexOfLastBeginPathAction;
         }
 
-        this._action = actions[this._index];
-
-        let state = applyActions(snapshot.index, this._index);
-        console.assert(!this._action.state || Object.shallowEqual(this._action.state, state));
-        if (!this._action.state)
-            this._action.state = state;
-
-        this.dispatchEventToListeners(WI.ContentView.Event.SupplementalRepresentedObjectsDidChange);
+        applyActions(snapshot.index, this._index);
 
         this._previewContainer.appendChild(snapshot.element);
         this._updateImageGrid();
     }
 
-    async _generateContentCanvasWebGL(index, actions)
+    _generateContentCanvasWebGL(index)
     {
         let imageLoad = (event) => {
             // Loading took too long and the current action index has already changed.
             if (index !== this._index)
                 return;
 
-            this._generateContentCanvasWebGL(index, actions);
+            this._generateContentCanvasWebGL(index);
         };
 
         let initialState = this.representedObject.initialState;
@@ -490,6 +423,8 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
             return;
         }
 
+        let actions = this.representedObject.actions;
+
         let visualIndex = index;
         while (!actions[visualIndex].isVisual && !(actions[visualIndex] instanceof WI.RecordingInitialStateAction))
             visualIndex--;
@@ -513,18 +448,14 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
 
             this._updateImageGrid();
         }
-
-        this.dispatchEventToListeners(WI.ContentView.Event.SupplementalRepresentedObjectsDidChange);
     }
 
     _updateCanvasPath()
     {
         let activated = WI.settings.showCanvasPath.value;
-        if (this._showPathButtonNavigationItem.activated !== activated) {
-            this.representedObject.actions.then((actions) => {
-                this._generateContentCanvas2D(this._index, actions);
-            });
-        }
+
+        if (this._showPathButtonNavigationItem.activated !== activated && !this._processing)
+            this._generateContentCanvas2D(this._index);
 
         this._showPathButtonNavigationItem.activated = activated;
     }
@@ -555,6 +486,18 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
         this._sliderValueElement.textContent = WI.UIString("%d of %d").format(visualActionIndex, visualActionIndexes.length);
     }
 
+    _updateProcessProgress(message, index)
+    {
+        if (this._processMessageTextView)
+            this._processMessageTextView.remove();
+
+        this._processMessageTextView = WI.createMessageTextView(message);
+        this.element.appendChild(this._processMessageTextView);
+
+        this._processProgressElement = this._processMessageTextView.appendChild(document.createElement("progress"));
+        this._processProgressElement.value = index / this.representedObject.actions.length;
+    }
+
     _showPathButtonClicked(event)
     {
         WI.settings.showCanvasPath.value = !this._showPathButtonNavigationItem.activated;
@@ -579,6 +522,16 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
 
         this.updateActionIndex(index);
     }
+
+    _handleRecordingProcessedActionSwizzle(event)
+    {
+        this._updateProcessProgress(WI.UIString("Loading Recording"), event.data.index);
+    }
+
+    _handleRecordingProcessedActionApply(event)
+    {
+        this._updateProcessProgress(WI.UIString("Processing Recording"), event.data.index);
+    }
 };
 
 WI.RecordingContentView.SnapshotInterval = 5000;