Changeset 231218 in webkit


Ignore:
Timestamp:
May 1, 2018 4:37:59 PM (6 years ago)
Author:
webkit@devinrousso.com
Message:

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.

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:
Location:
trunk
Files:
13 edited

Legend:

Unmodified
Added
Removed
  • trunk/LayoutTests/ChangeLog

    r231203 r231218  
     12018-05-01  Devin Rousso  <webkit@devinrousso.com>
     2
     3        Web Inspector: Canvas tab: determine hasVisibleEffect for all actions immediately after recording is added
     4        https://bugs.webkit.org/show_bug.cgi?id=182995
     5
     6        Reviewed by Matt Baker.
     7
     8        * inspector/canvas/resources/recording-utilities.js:
     9
    1102018-05-01  Ryan Haddad  <ryanhaddad@apple.com>
    211
  • trunk/LayoutTests/inspector/canvas/resources/recording-utilities.js

    r225892 r231218  
    125125            InspectorTest.assert(recording.frames.length === frameCount, `Recording should have ${frameCount} frames.`)
    126126
    127             return recording.actions.then(() => {
     127            return Promise.all(recording.actions.map((action) => action.swizzle(recording))).then(() => {
    128128                logRecording(recording, type);
    129129            });
  • trunk/Source/WebInspectorUI/ChangeLog

    r231081 r231218  
     12018-05-01  Devin Rousso  <webkit@devinrousso.com>
     2
     3        Web Inspector: Canvas tab: determine hasVisibleEffect for all actions immediately after recording is added
     4        https://bugs.webkit.org/show_bug.cgi?id=182995
     5
     6        Reviewed by Matt Baker.
     7
     8        Previously, we'd swizzle the entirety of the `WI.Recording` in one, which would usually
     9        freeze the UI, especially for larger recordings. This patch uses `WI.YieldableTask` to split
     10        the work and allow the rest of the UI to still be usable while `WI.Recording` are processing.
     11        Additionally, since we no longer have to worry about hangs, we can do more work upfront,
     12        such as calculating `hasVisibleEffect` and the current state of 2D canvases.
     13
     14        These changes require that all uses of `WI.Recording` call `process()` before attempting to
     15        use any `frames`/`actions`/`initialState`, as they will have their original payload values
     16        and will have not been swizzled or applied.
     17
     18        * Localizations/en.lproj/localizedStrings.js:
     19
     20        * UserInterface/Models/Recording.js:
     21        (WI.Recording):
     22        (WI.Recording.prototype.process):
     23        (WI.Recording.prototype.createContext): Added.
     24        (WI.Recording.prototype.async yieldableTaskWillProcessItem): Added.
     25        (WI.Recording.prototype.async yieldableTaskDidFinish): Added.
     26
     27        * UserInterface/Models/RecordingAction.js:
     28        (WI.RecordingAction):
     29        (WI.RecordingAction.prototype.process): Added.
     30        (WI.RecordingAction.prototype.async swizzle): Added.
     31        (WI.RecordingAction.prototype.apply):
     32        (WI.RecordingAction.prototype.toJSON):
     33        (WI.RecordingAction.prototype.set state): Deleted.
     34        (WI.RecordingAction.prototype.swizzle): Deleted.
     35        (WI.RecordingAction.prototype.apply.getContent): Deleted.
     36        (WI.RecordingAction.prototype.async _swizzle): Deleted.
     37        * UserInterface/Models/RecordingInitialStateAction.js:
     38        (WI.RecordingInitialStateAction):
     39
     40        * UserInterface/Views/CanvasSidebarPanel.js:
     41        (WI.CanvasSidebarPanel):
     42        (WI.CanvasSidebarPanel.prototype.set action):
     43        (WI.CanvasSidebarPanel.prototype._treeOutlineSelectionDidChange):
     44        (WI.CanvasSidebarPanel.prototype._recordingChanged):
     45
     46        * UserInterface/Views/CanvasSidebarPanel.css:
     47        (.sidebar > .panel.navigation.canvas > .content > .recording-content > .indeterminate-progress-spinner):
     48
     49        * UserInterface/Views/RecordingActionTreeElement.js:
     50        (WI.RecordingActionTreeElement):
     51        (WI.RecordingActionTreeElement.prototype.onattach):
     52        (WI.RecordingActionTreeElement.prototype._handleHasVisibleEffectChanged): Deleted.
     53
     54        * UserInterface/Views/RecordingContentView.js:
     55        (WI.RecordingContentView):
     56        (WI.RecordingContentView.prototype.get navigationItems):
     57        (WI.RecordingContentView.prototype.updateActionIndex):
     58        (WI.RecordingContentView.prototype.initialLayout):
     59        (WI.RecordingContentView.prototype._generateContentCanvas2D): Added.
     60        (WI.RecordingContentView.prototype._generateContentCanvasWebGL): Added.
     61        (WI.RecordingContentView.prototype._updateCanvasPath):
     62        (WI.RecordingContentView.prototype._updateProcessProgress): Added.
     63        (WI.RecordingContentView.prototype._handleRecordingProcessedActionSwizzle): Added.
     64        (WI.RecordingContentView.prototype._handleRecordingProcessedActionApply): Added.
     65        (WI.RecordingContentView.supportsCanvasPathDebugging): Deleted.
     66        (WI.RecordingContentView.prototype.async _generateContentCanvas2D): Deleted.
     67        (WI.RecordingContentView.prototype.async _generateContentCanvasWebGL): Deleted.
     68
     69        * UserInterface/Views/RecordingContentView.css:
     70        (.content-view:not(.tab).recording > .preview-container):
     71
     72        * UserInterface/Base/ImageUtilities.js:
     73        (WI.ImageUtilities.supportsCanvasPathDebugging):
     74
    1752018-04-26  Jer Noble  <jer.noble@apple.com>
    276
  • trunk/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js

    r230228 r231218  
    563563localizedStrings["Load \u2014 %s"] = "Load \u2014 %s";
    564564localizedStrings["Load cancelled"] = "Load cancelled";
     565localizedStrings["Loading Recording"] = "Loading Recording";
    565566localizedStrings["Local File"] = "Local File";
    566567localizedStrings["Local Storage"] = "Local Storage";
     
    724725localizedStrings["Probes"] = "Probes";
    725726localizedStrings["Processing Instruction"] = "Processing Instruction";
     727localizedStrings["Processing Recording"] = "Processing Recording";
    726728localizedStrings["Program %d"] = "Program %d";
    727729localizedStrings["Properties"] = "Properties";
  • trunk/Source/WebInspectorUI/UserInterface/Base/ImageUtilities.js

    r225884 r231218  
    127127        return image;
    128128    }
     129
     130    static supportsCanvasPathDebugging()
     131    {
     132        return "getPath" in CanvasRenderingContext2D.prototype
     133            && "setPath" in CanvasRenderingContext2D.prototype
     134            && "currentX" in CanvasRenderingContext2D.prototype
     135            && "currentY" in CanvasRenderingContext2D.prototype;
     136    }
    129137};
    130138
  • trunk/Source/WebInspectorUI/UserInterface/Models/Recording.js

    r225884 r231218  
    2424 */
    2525
    26 WI.Recording = class Recording
     26WI.Recording = class Recording extends WI.Object
    2727{
    2828    constructor(version, type, initialState, frames, data)
    2929    {
     30        super();
     31
    3032        this._version = version;
    3133        this._type = type;
     
    3638
    3739        this._swizzle = [];
     40        this._actions = [new WI.RecordingInitialStateAction].concat(...this._frames.map((frame) => frame.actions));
    3841        this._visualActionIndexes = [];
    3942        this._source = null;
    4043
    41         let actions = [new WI.RecordingInitialStateAction].concat(...this._frames.map((frame) => frame.actions));
    42         this._actions = Promise.all(actions.map((action) => action.swizzle(this))).then(() => {
    43             actions.forEach((action, index) => {
    44                 if (!action.valid)
    45                     return;
    46 
    47                 let prototype = null;
    48                 if (this._type === WI.Recording.Type.Canvas2D)
    49                     prototype = CanvasRenderingContext2D.prototype;
    50                 else if (this._type === WI.Recording.Type.CanvasWebGL)
    51                     prototype = WebGLRenderingContext.prototype;
    52 
    53                 if (prototype) {
    54                     let validName = action.name in prototype;
    55                     let validFunction = !action.isFunction || typeof prototype[action.name] === "function";
    56                     if (!validName || !validFunction) {
    57                         action.markInvalid();
    58 
    59                         WI.Recording.synthesizeError(WI.UIString("“%s” is invalid.").format(this._name));
    60                     }
    61                 }
    62 
    63                 if (action.isVisual)
    64                     this._visualActionIndexes.push(index);
    65             });
    66 
    67             return actions;
    68         });
     44        this._swizzleTask = null;
     45        this._applyTask = null;
     46        this._processContext = null;
     47        this._processPromise = null;
    6948    }
    7049
     
    178157    get frames() { return this._frames; }
    179158    get data() { return this._data; }
     159    get actions() { return this._actions; }
    180160    get visualActionIndexes() { return this._visualActionIndexes; }
    181 
    182     get actions() { return this._actions; }
    183161
    184162    get source() { return this._source; }
    185163    set source(source) { this._source = source; }
     164
     165    process()
     166    {
     167        if (!this._processPromise) {
     168            this._processPromise = new WI.WrappedPromise;
     169
     170            let items = this._actions.map((action, index) => { return {action, index} });
     171            this._swizzleTask = new WI.YieldableTask(this, items);
     172            this._applyTask = new WI.YieldableTask(this, items);
     173
     174            this._swizzleTask.start();
     175        }
     176        return this._processPromise.promise;
     177    }
    186178
    187179    createDisplayName(suggestedName)
     
    301293    }
    302294
     295    createContext()
     296    {
     297        let createCanvasContext = (type) => {
     298            let canvas = document.createElement("canvas");
     299            if ("width" in this._initialState.attributes)
     300                canvas.width = this._initialState.attributes.width;
     301            if ("height" in this._initialState.attributes)
     302                canvas.height = this._initialState.attributes.height;
     303            return canvas.getContext(type, ...this._initialState.parameters);
     304        };
     305
     306        if (this._type === WI.Recording.Type.Canvas2D)
     307            return createCanvasContext("2d");
     308
     309        if (this._type === WI.Recording.Type.CanvasWebGL)
     310            return createCanvasContext("webgl");
     311
     312        console.error("Unknown recording type", this._type);
     313        return null;
     314    }
     315
    303316    toJSON()
    304317    {
     
    319332        };
    320333    }
     334
     335    // YieldableTask delegate
     336
     337    async yieldableTaskWillProcessItem(task, item)
     338    {
     339        if (task === this._swizzleTask) {
     340            await item.action.swizzle(this);
     341
     342            this.dispatchEventToListeners(WI.Recording.Event.ProcessedActionSwizzle, {index: item.index});
     343        } else if (task === this._applyTask) {
     344            item.action.process(this, this._processContext);
     345
     346            if (item.action.isVisual)
     347                this._visualActionIndexes.push(item.index);
     348
     349            this.dispatchEventToListeners(WI.Recording.Event.ProcessedActionApply, {index: item.index});
     350        }
     351    }
     352
     353    async yieldableTaskDidFinish(task)
     354    {
     355        if (task === this._swizzleTask) {
     356            this._swizzleTask = null;
     357
     358            this._processContext = this.createContext();
     359
     360            if (this._type === WI.Recording.Type.Canvas2D) {
     361                let initialContent = await WI.ImageUtilities.promisifyLoad(this._initialState.content);
     362                this._processContext.drawImage(initialContent, 0, 0);
     363
     364                for (let [key, value] of Object.entries(this._initialState.attributes)) {
     365                    switch (key) {
     366                    case "setTransform":
     367                        value = [await this.swizzle(value, WI.Recording.Swizzle.DOMMatrix)];
     368                        break;
     369
     370                    case "fillStyle":
     371                    case "strokeStyle":
     372                            let [gradient, pattern, string] = await Promise.all([
     373                                this.swizzle(value, WI.Recording.Swizzle.CanvasGradient),
     374                                this.swizzle(value, WI.Recording.Swizzle.CanvasPattern),
     375                                this.swizzle(value, WI.Recording.Swizzle.String),
     376                            ]);
     377                            if (gradient && !pattern)
     378                                value = gradient;
     379                            else if (pattern && !gradient)
     380                                value = pattern;
     381                            else
     382                                value = string;
     383                        break;
     384
     385                    case "direction":
     386                    case "font":
     387                    case "globalCompositeOperation":
     388                    case "imageSmoothingEnabled":
     389                    case "imageSmoothingQuality":
     390                    case "lineCap":
     391                    case "lineJoin":
     392                    case "shadowColor":
     393                    case "textAlign":
     394                    case "textBaseline":
     395                        value = await this.swizzle(value, WI.Recording.Swizzle.String);
     396                        break;
     397
     398                    case "setPath":
     399                        value = [await this.swizzle(value[0], WI.Recording.Swizzle.Path2D)];
     400                        break;
     401                    }
     402
     403                    if (value === undefined || (Array.isArray(value) && value.includes(undefined)))
     404                        continue;
     405
     406                    try {
     407                        if (WI.RecordingAction.isFunctionForType(this._type, key))
     408                            this._processContext[key](...value);
     409                        else
     410                            this._processContext[key] = value;
     411                    } catch { }
     412                }
     413            }
     414
     415            this._applyTask.start();
     416        } else if (task === this._applyTask) {
     417            this._applyTask = null;
     418            this._processContext = null;
     419            this._processPromise.resolve();
     420        }
     421    }
     422};
     423
     424WI.Recording.Event = {
     425    ProcessedActionApply: "recording-processed-action-apply",
     426    ProcessedActionSwizzle: "recording-processed-action-swizzle",
    321427};
    322428
  • trunk/Source/WebInspectorUI/UserInterface/Models/RecordingAction.js

    r228301 r231218  
    4242
    4343        this._valid = true;
    44         this._swizzledPromise = null;
    45 
    4644        this._isFunction = false;
    4745        this._isGetter = false;
    4846        this._isVisual = false;
    4947        this._hasVisibleEffect = undefined;
     48
     49        this._state = null;
    5050        this._stateModifiers = new Set;
    5151    }
     
    9898    get isVisual() { return this._isVisual; }
    9999    get hasVisibleEffect() { return this._hasVisibleEffect; }
     100    get state() { return this._state; }
    100101    get stateModifiers() { return this._stateModifiers; }
    101102
    102     get state() { return this._state; }
    103     set state(state) { this._state = state; }
    104 
    105     markInvalid()
    106     {
    107         let wasValid = this._valid;
    108         this._valid = false;
    109 
    110         if (wasValid)
    111             this.dispatchEventToListeners(WI.RecordingAction.Event.ValidityChanged);
    112     }
    113 
    114     swizzle(recording)
    115     {
    116         if (!this._swizzledPromise)
    117             this._swizzledPromise = this._swizzle(recording);
    118         return this._swizzledPromise;
    119     }
    120 
    121     apply(context, options = {})
    122     {
    123         if (!this.valid)
     103    process(recording, context)
     104    {
     105        if (recording.type === WI.Recording.Type.CanvasWebGL) {
     106            // We add each RecordingAction to the list of visualActionIndexes after it is processed.
     107            if (this._valid && this._isVisual) {
     108                let contentBefore = recording.visualActionIndexes.length ? recording.visualActionIndexes.lastValue.snapshot : recording.initialState.content;
     109                this._hasVisibleEffect = this._snapshot !== contentBefore;
     110            }
    124111            return;
     112        }
    125113
    126114        function getContent() {
     
    144132
    145133        let contentBefore = null;
    146         let shouldCheckForChange = this._isVisual && this._hasVisibleEffect === undefined;
    147         if (shouldCheckForChange)
     134        if (this._valid && this._isVisual)
    148135            contentBefore = getContent();
    149136
    150         try {
    151             let name = options.nameOverride || this._name;
    152             if (this.isFunction)
    153                 context[name](...this._parameters);
    154             else {
    155                 if (this.isGetter)
    156                     context[name];
    157                 else
    158                     context[name] = this._parameters[0];
    159             }
    160 
    161             if (shouldCheckForChange) {
    162                 this._hasVisibleEffect = !Array.shallowEqual(contentBefore, getContent());
    163                 if (!this._hasVisibleEffect)
    164                     this.dispatchEventToListeners(WI.RecordingAction.Event.HasVisibleEffectChanged);
    165             }
    166         } catch {
    167             this.markInvalid();
    168 
    169             WI.Recording.synthesizeError(WI.UIString("“%s” threw an error.").format(this._name));
    170         }
    171     }
    172 
    173     toJSON()
    174     {
    175         let json = [this._payloadName, this._payloadParameters, this._payloadSwizzleTypes, this._payloadTrace];
    176         if (this._payloadSnapshot >= 0)
    177             json.push(this._payloadSnapshot);
    178         return json;
    179     }
    180 
    181     // Private
    182 
    183     async _swizzle(recording)
    184     {
     137        this.apply(context);
     138
     139        if (this._valid && this._isVisual)
     140            this._hasVisibleEffect = !Array.shallowEqual(contentBefore, getContent());
     141
     142        if (recording.type === WI.Recording.Type.Canvas2D) {
     143            let matrix = context.getTransform();
     144
     145            this._state = {
     146                currentX: context.currentX,
     147                currentY: context.currentY,
     148                direction: context.direction,
     149                fillStyle: context.fillStyle,
     150                font: context.font,
     151                globalAlpha: context.globalAlpha,
     152                globalCompositeOperation: context.globalCompositeOperation,
     153                imageSmoothingEnabled: context.imageSmoothingEnabled,
     154                imageSmoothingQuality: context.imageSmoothingQuality,
     155                lineCap: context.lineCap,
     156                lineDash: context.getLineDash(),
     157                lineDashOffset: context.lineDashOffset,
     158                lineJoin: context.lineJoin,
     159                lineWidth: context.lineWidth,
     160                miterLimit: context.miterLimit,
     161                shadowBlur: context.shadowBlur,
     162                shadowColor: context.shadowColor,
     163                shadowOffsetX: context.shadowOffsetX,
     164                shadowOffsetY: context.shadowOffsetY,
     165                strokeStyle: context.strokeStyle,
     166                textAlign: context.textAlign,
     167                textBaseline: context.textBaseline,
     168                transform: [matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f],
     169                webkitImageSmoothingEnabled: context.webkitImageSmoothingEnabled,
     170                webkitLineDash: context.webkitLineDash,
     171                webkitLineDashOffset: context.webkitLineDashOffset,
     172            };
     173
     174            if (WI.ImageUtilities.supportsCanvasPathDebugging())
     175                this._state.setPath = [context.getPath()];
     176        }
     177    }
     178
     179    async swizzle(recording)
     180    {
     181        if (!this._valid)
     182            return;
     183
    185184        let swizzleParameter = (item, index) => {
    186185            return recording.swizzle(item, this._payloadSwizzleTypes[index]);
     
    216215            this._snapshot = snapshot;
    217216
     217        this._isFunction = WI.RecordingAction.isFunctionForType(recording.type, this._name);
     218        this._isGetter = !this._isFunction && !this._parameters.length;
     219
     220        let visualNames = WI.RecordingAction._visualNames[recording.type];
     221        this._isVisual = visualNames ? visualNames.has(this._name) : false;
     222
     223        if (this._valid) {
     224            let prototype = null;
     225            if (recording.type === WI.Recording.Type.Canvas2D)
     226                prototype = CanvasRenderingContext2D.prototype;
     227            else if (recording.type === WI.Recording.Type.CanvasWebGL)
     228                prototype = WebGLRenderingContext.prototype;
     229
     230            if (prototype) {
     231                let validName = name in prototype;
     232                let validFunction = !this._isFunction || typeof prototype[name] === "function";
     233                if (!validName || !validFunction) {
     234                    this.markInvalid();
     235
     236                    WI.Recording.synthesizeError(WI.UIString("“%s” is invalid.").format(name));
     237                }
     238            }
     239        }
     240
    218241        if (this._valid) {
    219242            let parametersSpecified = this._parameters.every((parameter) => parameter !== undefined);
     
    223246        }
    224247
    225         this._isFunction = WI.RecordingAction.isFunctionForType(recording.type, this._name);
    226         this._isGetter = !this._isFunction && !this._parameters.length;
    227 
    228         let visualNames = WI.RecordingAction._visualNames[recording.type];
    229         this._isVisual = visualNames ? visualNames.has(this._name) : false;
    230 
    231         this._stateModifiers = new Set([this._name]);
    232         let stateModifiers = WI.RecordingAction._stateModifiers[recording.type];
    233         if (stateModifiers) {
    234             let modifiedByAction = stateModifiers[this._name] || [];
    235             for (let item of modifiedByAction)
    236                 this._stateModifiers.add(item);
    237         }
     248        if (this._valid) {
     249            let stateModifiers = WI.RecordingAction._stateModifiers[recording.type];
     250            if (stateModifiers) {
     251                this._stateModifiers.add(this._name);
     252                let modifiedByAction = stateModifiers[this._name] || [];
     253                for (let item of modifiedByAction)
     254                    this._stateModifiers.add(item);
     255            }
     256        }
     257    }
     258
     259    apply(context, options = {})
     260    {
     261        if (!this.valid)
     262            return;
     263
     264        try {
     265            let name = options.nameOverride || this._name;
     266            if (this.isFunction)
     267                context[name](...this._parameters);
     268            else {
     269                if (this.isGetter)
     270                    context[name];
     271                else
     272                    context[name] = this._parameters[0];
     273            }
     274        } catch {
     275            this.markInvalid();
     276
     277            WI.Recording.synthesizeError(WI.UIString("“%s” threw an error.").format(this._name));
     278        }
     279    }
     280
     281    markInvalid()
     282    {
     283        if (!this._valid)
     284            return;
     285
     286        this._valid = false;
     287
     288        this.dispatchEventToListeners(WI.RecordingAction.Event.ValidityChanged);
    238289    }
    239290
     
    277328
    278329        return [];
     330    }
     331
     332    toJSON()
     333    {
     334        let json = [this._payloadName, this._payloadParameters, this._payloadSwizzleTypes, this._payloadTrace];
     335        if (this._payloadSnapshot >= 0)
     336            json.push(this._payloadSnapshot);
     337        return json;
    279338    }
    280339};
     
    311370        "getImageData",
    312371        "getLineDash",
     372        "getPath",
    313373        "isPointInPath",
    314374        "isPointInPath",
     
    334394        "setLineWidth",
    335395        "setMiterLimit",
     396        "setPath",
    336397        "setShadow",
    337398        "setStrokeColor",
  • trunk/Source/WebInspectorUI/UserInterface/Models/RecordingInitialStateAction.js

    r222057 r231218  
    3333
    3434        this._valid = false;
    35         this._swizzledPromise = Promise.resolve();
    3635    }
    3736};
  • trunk/Source/WebInspectorUI/UserInterface/Views/CanvasSidebarPanel.css

    r229377 r231218  
    7272    line-height: 16px;
    7373}
     74
     75.sidebar > .panel.navigation.canvas > .content > .recording-content > .indeterminate-progress-spinner {
     76    margin: 16px auto;
     77}
  • trunk/Source/WebInspectorUI/UserInterface/Views/CanvasSidebarPanel.js

    r229377 r231218  
    5353        this.contentView.addSubview(this._recordingNavigationBar);
    5454
    55         let recordingContent = this.contentView.element.appendChild(document.createElement("div"));
    56         recordingContent.className = "recording-content";
     55        this._recordingContentContainer = this.contentView.element.appendChild(document.createElement("div"));
     56        this._recordingContentContainer.className = "recording-content";
    5757
    5858        this._recordingTreeOutline = this.contentTreeOutline;
    59         recordingContent.appendChild(this._recordingTreeOutline.element);
     59        this._recordingContentContainer.appendChild(this._recordingTreeOutline.element);
    6060
    6161        this._recordingTreeOutline.customIndent = true;
    62         this._recordingTreeOutline.registerScrollVirtualizer(recordingContent, 20);
     62        this._recordingTreeOutline.registerScrollVirtualizer(this._recordingContentContainer, 20);
    6363
    6464        this._canvasTreeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._treeOutlineSelectionDidChange, this);
     
    6767        WI.canvasManager.addEventListener(WI.CanvasManager.Event.RecordingStarted, this._updateRecordNavigationItem, this);
    6868        WI.canvasManager.addEventListener(WI.CanvasManager.Event.RecordingStopped, this._updateRecordNavigationItem, this);
     69
     70        this._recordingProcessPromise = null;
     71        this._recordingProcessSpinner = null;
    6972    }
    7073
     
    109112    set action(action)
    110113    {
    111         if (!this._recording)
     114        if (!this._recording || this._recordingProcessPromise)
    112115            return;
    113116
     
    287290        this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol] = treeElement.representedObject;
    288291
    289         let recordingContentView = this.contentBrowser.showContentViewForRepresentedObject(this._recording);
     292        const onlyExisting = true;
     293        let recordingContentView = this.contentBrowser.contentViewForRepresentedObject(this._recording, onlyExisting);
    290294        if (recordingContentView)
    291295            recordingContentView.updateActionIndex(treeElement.index);
     
    328332            return;
    329333
     334        if (!this._recordingProcessSpinner) {
     335            this._recordingProcessSpinner = new WI.IndeterminateProgressSpinner;
     336            this._recordingContentContainer.appendChild(this._recordingProcessSpinner.element);
     337        }
     338
     339        this.contentBrowser.showContentViewForRepresentedObject(this._recording);
     340
    330341        let recording = this._recording;
    331342
    332         this._recording.actions.then((actions) => {
    333             if (recording !== this._recording)
     343        let promise = this._recording.process().then(() => {
     344            if (recording !== this._recording || promise !== this._recordingProcessPromise)
    334345                return;
    335346
    336             this._recordingTreeOutline.element.dataset.indent = Number.countDigits(actions.length);
    337 
    338             if (actions[0] instanceof WI.RecordingInitialStateAction)
    339                 this._recordingTreeOutline.appendChild(new WI.RecordingActionTreeElement(actions[0], 0, this._recording.type));
     347            if (this._recordingProcessSpinner) {
     348                this._recordingProcessSpinner.element.remove();
     349                this._recordingProcessSpinner = null;
     350            }
     351
     352            this._recordingTreeOutline.element.dataset.indent = Number.countDigits(this._recording.actions.length);
     353
     354            if (this._recording.actions[0] instanceof WI.RecordingInitialStateAction)
     355                this._recordingTreeOutline.appendChild(new WI.RecordingActionTreeElement(this._recording.actions[0], 0, this._recording.type));
    340356
    341357            let cumulativeActionIndex = 1;
     
    367383            }
    368384
    369             this.action = this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol] || actions[0];
     385            this.action = this._recording[WI.CanvasSidebarPanel.SelectedActionSymbol] || this._recording.actions[0];
     386
     387            this._recordingProcessPromise = null;
    370388        });
     389
     390        this._recordingProcessPromise = promise;
    371391    }
    372392
  • trunk/Source/WebInspectorUI/UserInterface/Views/RecordingActionTreeElement.js

    r225884 r231218  
    4040
    4141        this.representedObject.addEventListener(WI.RecordingAction.Event.ValidityChanged, this._handleValidityChanged, this);
    42         this.representedObject.addEventListener(WI.RecordingAction.Event.HasVisibleEffectChanged, this._handleHasVisibleEffectChanged, this);
    4342    }
    4443
     
    400399
    401400        this.element.dataset.index = this._index.toLocaleString();
     401
     402        if (this.representedObject.valid && this.representedObject.isVisual && !this.representedObject.hasVisibleEffect) {
     403            this.addClassName("no-visible-effect");
     404
     405            const title = WI.UIString("This action causes no visual change");
     406            this.status = WI.ImageUtilities.useSVGSymbol("Images/Warning.svg", "warning", title);
     407        }
    402408    }
    403409
     
    431437        this.addClassName("invalid");
    432438    }
    433 
    434     _handleHasVisibleEffectChanged(event)
    435     {
    436         this.addClassName("no-visible-effect");
    437 
    438         this.status = WI.ImageUtilities.useSVGSymbol("Images/Warning.svg", "warning");
    439         this.status.title = WI.UIString("This action causes no visual change");
    440     }
    441439};
    442440
  • trunk/Source/WebInspectorUI/UserInterface/Views/RecordingContentView.css

    r228301 r231218  
    6868    justify-content: center;
    6969    align-items: center;
     70    position: relative;
     71    width: -webkit-fill-available;
     72    height: -webkit-fill-available;
    7073}
    7174
  • trunk/Source/WebInspectorUI/UserInterface/Views/RecordingContentView.js

    r228301 r231218  
    4343        let isCanvasWebGL = this.representedObject.type === WI.Recording.Type.CanvasWebGL;
    4444        if (isCanvas2D || isCanvasWebGL) {
    45             if (isCanvas2D && WI.RecordingContentView.supportsCanvasPathDebugging()) {
     45            if (isCanvas2D && WI.ImageUtilities.supportsCanvasPathDebugging()) {
    4646                this._pathContext = null;
    4747
     
    6262            this._exportButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => { this._exportRecording(); });
    6363        }
     64
     65        this._processing = true;
     66        this._processMessageTextView = null;
    6467    }
    6568
    6669    // Static
    67 
    68     static supportsCanvasPathDebugging()
    69     {
    70         return "currentX" in CanvasRenderingContext2D.prototype && "currentY" in CanvasRenderingContext2D.prototype;
    71     }
    7270
    7371    static _actionModifiesPath(recordingAction)
     
    10098
    10199        let navigationItems = [this._exportButtonNavigationItem, new WI.DividerNavigationItem];
    102         if (isCanvas2D && WI.RecordingContentView.supportsCanvasPathDebugging())
     100        if (isCanvas2D && WI.ImageUtilities.supportsCanvasPathDebugging())
    103101            navigationItems.push(this._showPathButtonNavigationItem);
    104102
     
    120118            return;
    121119
    122         this.representedObject.actions.then((actions) => {
    123             console.assert(index >= 0 && index < actions.length);
    124             if (index < 0 || index >= actions.length)
    125                 return;
    126 
    127             this._index = index;
    128             this._updateSliderValue();
    129 
    130             if (this.representedObject.type === WI.Recording.Type.Canvas2D)
    131                 this._throttler._generateContentCanvas2D(index, actions);
    132             else if (this.representedObject.type === WI.Recording.Type.CanvasWebGL)
    133                 this._throttler._generateContentCanvasWebGL(index, actions);
    134         });
     120        console.assert(index >= 0 && index < this.representedObject.actions.length);
     121        if (index < 0 || index >= this.representedObject.actions.length)
     122            return;
     123
     124        this._index = index;
     125
     126        if (this._processing)
     127            return;
     128
     129        this._updateSliderValue();
     130
     131        if (this.representedObject.type === WI.Recording.Type.Canvas2D)
     132            this._throttler._generateContentCanvas2D(index);
     133        else if (this.representedObject.type === WI.Recording.Type.CanvasWebGL)
     134            this._throttler._generateContentCanvasWebGL(index);
     135
     136        this._action = this.representedObject.actions[this._index];
     137
     138        this.dispatchEventToListeners(WI.ContentView.Event.SupplementalRepresentedObjectsDidChange);
    135139    }
    136140
     
    189193        this._sliderElement.max = 0;
    190194
    191         this.representedObject.actions.then(() => {
     195        this.representedObject.addEventListener(WI.Recording.Event.ProcessedActionSwizzle, this._handleRecordingProcessedActionSwizzle, this);
     196        this.representedObject.addEventListener(WI.Recording.Event.ProcessedActionApply, this._handleRecordingProcessedActionApply, this);
     197
     198        this.representedObject.process().then(() => {
     199            if (this._processMessageTextView)
     200                this._processMessageTextView.remove();
     201
    192202            sliderContainer.classList.remove("hidden");
    193203            this._sliderElement.max = this.representedObject.visualActionIndexes.length;
    194204            this._updateSliderValue();
     205
     206            this._processing = false;
     207
     208            let index = this._index;
     209            if (!isNaN(index)) {
     210                this._index = NaN;
     211                this.updateActionIndex(index);
     212            }
    195213        });
    196214    }
     
    215233    }
    216234
    217     async _generateContentCanvas2D(index, actions)
     235    _generateContentCanvas2D(index)
    218236    {
    219237        let imageLoad = (event) => {
     
    222240                return;
    223241
    224             this._generateContentCanvas2D(index, actions);
     242            this._generateContentCanvas2D(index);
    225243        };
    226244
     
    236254        let snapshot = this._snapshots[snapshotIndex];
    237255
    238         let showCanvasPath = WI.RecordingContentView.supportsCanvasPathDebugging() && WI.settings.showCanvasPath.value;
     256        let showCanvasPath = WI.ImageUtilities.supportsCanvasPathDebugging() && WI.settings.showCanvasPath.value;
    239257        let indexOfLastBeginPathAction = Infinity;
     258
     259        let actions = this.representedObject.actions;
    240260
    241261        let applyActions = (from, to, callback) => {
     
    332352                this._pathContext.canvas.remove();
    333353
    334             let state = {
    335                 currentX: snapshot.context.currentX,
    336                 currentY: snapshot.context.currentY,
    337                 direction: snapshot.context.direction,
    338                 fillStyle: snapshot.context.fillStyle,
    339                 font: snapshot.context.font,
    340                 globalAlpha: snapshot.context.globalAlpha,
    341                 globalCompositeOperation: snapshot.context.globalCompositeOperation,
    342                 imageSmoothingEnabled: snapshot.context.imageSmoothingEnabled,
    343                 imageSmoothingQuality: snapshot.context.imageSmoothingQuality,
    344                 lineCap: snapshot.context.lineCap,
    345                 lineDash: snapshot.context.getLineDash(),
    346                 lineDashOffset: snapshot.context.lineDashOffset,
    347                 lineJoin: snapshot.context.lineJoin,
    348                 lineWidth: snapshot.context.lineWidth,
    349                 miterLimit: snapshot.context.miterLimit,
    350                 shadowBlur: snapshot.context.shadowBlur,
    351                 shadowColor: snapshot.context.shadowColor,
    352                 shadowOffsetX: snapshot.context.shadowOffsetX,
    353                 shadowOffsetY: snapshot.context.shadowOffsetY,
    354                 strokeStyle: snapshot.context.strokeStyle,
    355                 textAlign: snapshot.context.textAlign,
    356                 textBaseline: snapshot.context.textBaseline,
    357                 transform: snapshot.context.getTransform(),
    358                 webkitImageSmoothingEnabled: snapshot.context.webkitImageSmoothingEnabled,
    359                 webkitLineDash: snapshot.context.webkitLineDash,
    360                 webkitLineDashOffset: snapshot.context.webkitLineDashOffset,
    361             };
    362 
    363             if (WI.RecordingContentView.supportsCanvasPathDebugging())
    364                 state.setPath = [snapshot.context.getPath()];
    365 
    366354            snapshot.context.restore();
    367355            while (saveCount-- > 0)
    368356                snapshot.context.restore();
    369 
    370             return state;
    371357        };
    372358
     
    377363                --snapshot.index;
    378364
    379             snapshot.element = document.createElement("canvas");
    380             snapshot.context = snapshot.element.getContext("2d", ...initialState.parameters);
    381             if ("width" in initialState.attributes)
    382                 snapshot.element.width = initialState.attributes.width;
    383             if ("height" in initialState.attributes)
    384                 snapshot.element.height = initialState.attributes.height;
     365            snapshot.context = this.representedObject.createContext();
     366            snapshot.element = snapshot.context.canvas;
    385367
    386368            let lastSnapshotIndex = snapshotIndex;
     
    393375            if (lastSnapshotIndex < 0) {
    394376                snapshot.content = this._initialContent;
    395                 snapshot.state = {};
    396 
    397                 for (let key in initialState.attributes) {
    398                     let value = initialState.attributes[key];
    399 
    400                     switch (key) {
    401                     case "setTransform":
    402                         value = [await this.representedObject.swizzle(value, WI.Recording.Swizzle.DOMMatrix)];
    403                         break;
    404 
    405                     case "fillStyle":
    406                     case "strokeStyle":
    407                         if (Array.isArray(value)) {
    408                             let canvasStyle = await this.representedObject.swizzle(value[0], WI.Recording.Swizzle.String);
    409                             if (canvasStyle.includes("gradient"))
    410                                 value = await this.representedObject.swizzle(value, WI.Recording.Swizzle.CanvasGradient);
    411                             else if (canvasStyle === "pattern")
    412                                 value = await this.representedObject.swizzle(value, WI.Recording.Swizzle.CanvasPattern);
    413                         } else
    414                             value = await this.representedObject.swizzle(value, WI.Recording.Swizzle.String);
    415                         break;
    416 
    417                     case "direction":
    418                     case "font":
    419                     case "globalCompositeOperation":
    420                     case "imageSmoothingEnabled":
    421                     case "imageSmoothingQuality":
    422                     case "lineCap":
    423                     case "lineJoin":
    424                     case "shadowColor":
    425                     case "textAlign":
    426                     case "textBaseline":
    427                         value = await this.representedObject.swizzle(value, WI.Recording.Swizzle.String);
    428                         break;
    429 
    430                     case "setPath":
    431                         value = [await this.representedObject.swizzle(value[0], WI.Recording.Swizzle.Path2D)];
    432                         break;
    433                     }
    434 
    435                     if (value === undefined || (Array.isArray(value) && value.includes(undefined)))
    436                         continue;
    437 
    438                     snapshot.state[key] = value;
    439                 }
     377                snapshot.state = actions[0].state;
    440378            } else {
    441379                snapshot.content = this._snapshots[lastSnapshotIndex].content;
     
    444382            }
    445383
    446             snapshot.state = applyActions(startIndex, snapshot.index - 1);
     384            applyActions(startIndex, snapshot.index - 1);
     385            if (snapshot.index > 0)
     386                snapshot.state = actions[snapshot.index - 1].state;
    447387
    448388            snapshot.content = new Image;
     
    460400        }
    461401
    462         this._action = actions[this._index];
    463 
    464         let state = applyActions(snapshot.index, this._index);
    465         console.assert(!this._action.state || Object.shallowEqual(this._action.state, state));
    466         if (!this._action.state)
    467             this._action.state = state;
    468 
    469         this.dispatchEventToListeners(WI.ContentView.Event.SupplementalRepresentedObjectsDidChange);
     402        applyActions(snapshot.index, this._index);
    470403
    471404        this._previewContainer.appendChild(snapshot.element);
     
    473406    }
    474407
    475     async _generateContentCanvasWebGL(index, actions)
     408    _generateContentCanvasWebGL(index)
    476409    {
    477410        let imageLoad = (event) => {
     
    480413                return;
    481414
    482             this._generateContentCanvasWebGL(index, actions);
     415            this._generateContentCanvasWebGL(index);
    483416        };
    484417
     
    490423            return;
    491424        }
     425
     426        let actions = this.representedObject.actions;
    492427
    493428        let visualIndex = index;
     
    514449            this._updateImageGrid();
    515450        }
    516 
    517         this.dispatchEventToListeners(WI.ContentView.Event.SupplementalRepresentedObjectsDidChange);
    518451    }
    519452
     
    521454    {
    522455        let activated = WI.settings.showCanvasPath.value;
    523         if (this._showPathButtonNavigationItem.activated !== activated) {
    524             this.representedObject.actions.then((actions) => {
    525                 this._generateContentCanvas2D(this._index, actions);
    526             });
    527         }
     456
     457        if (this._showPathButtonNavigationItem.activated !== activated && !this._processing)
     458            this._generateContentCanvas2D(this._index);
    528459
    529460        this._showPathButtonNavigationItem.activated = activated;
     
    556487    }
    557488
     489    _updateProcessProgress(message, index)
     490    {
     491        if (this._processMessageTextView)
     492            this._processMessageTextView.remove();
     493
     494        this._processMessageTextView = WI.createMessageTextView(message);
     495        this.element.appendChild(this._processMessageTextView);
     496
     497        this._processProgressElement = this._processMessageTextView.appendChild(document.createElement("progress"));
     498        this._processProgressElement.value = index / this.representedObject.actions.length;
     499    }
     500
    558501    _showPathButtonClicked(event)
    559502    {
     
    580523        this.updateActionIndex(index);
    581524    }
     525
     526    _handleRecordingProcessedActionSwizzle(event)
     527    {
     528        this._updateProcessProgress(WI.UIString("Loading Recording"), event.data.index);
     529    }
     530
     531    _handleRecordingProcessedActionApply(event)
     532    {
     533        this._updateProcessProgress(WI.UIString("Processing Recording"), event.data.index);
     534    }
    582535};
    583536
Note: See TracChangeset for help on using the changeset viewer.