Web Inspector: provide method for recording CanvasRenderingContext2D from JavaScript
authorwebkit@devinrousso.com <webkit@devinrousso.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 4 Dec 2017 21:40:55 +0000 (21:40 +0000)
committerwebkit@devinrousso.com <webkit@devinrousso.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 4 Dec 2017 21:40:55 +0000 (21:40 +0000)
https://bugs.webkit.org/show_bug.cgi?id=175166
<rdar://problem/34040740>

Reviewed by Joseph Pecoraro.

Source/JavaScriptCore:

* inspector/protocol/Recording.json:
Add optional `name` that will be used by the frontend for uniquely identifying the Recording.

* inspector/JSGlobalObjectConsoleClient.h:
* inspector/JSGlobalObjectConsoleClient.cpp:
(Inspector::JSGlobalObjectConsoleClient::record):
(Inspector::JSGlobalObjectConsoleClient::recordEnd):

* runtime/ConsoleClient.h:
* runtime/ConsoleObject.cpp:
(JSC::ConsoleObject::finishCreation):
(JSC::consoleProtoFuncRecord):
(JSC::consoleProtoFuncRecordEnd):

Source/WebCore:

No new tests, updated existing tests.

* inspector/InspectorCanvas.h:
* inspector/InspectorCanvas.cpp:
(WebCore::InspectorCanvas::resetRecordingData):

* inspector/InspectorCanvasAgent.h:
* inspector/InspectorCanvasAgent.cpp:
(WebCore::InspectorCanvasAgent::didFinishRecordingCanvasFrame):
(WebCore::InspectorCanvasAgent::consoleStartRecordingCanvas):

* inspector/InspectorInstrumentation.h:
* inspector/InspectorInstrumentation.cpp:
(WebCore::InspectorInstrumentation::consoleStartRecordingCanvas):
(WebCore::InspectorInstrumentation::consoleStartRecordingCanvasImpl):

* page/PageConsoleClient.h:
* page/PageConsoleClient.cpp:
(WebCore::PageConsoleClient::record):
(WebCore::PageConsoleClient::recordEnd):

* workers/WorkerConsoleClient.h:
* workers/WorkerConsoleClient.cpp:
(WebCore::WorkerConsoleClient::record):
(WebCore::WorkerConsoleClient::recordEnd):

Source/WebInspectorUI:

* UserInterface/Controllers/CanvasManager.js:
(WI.CanvasManager.prototype.recordingFinished):
If a `name` is sent with the payload, use it as the suggested name.

* UserInterface/Models/NativeFunctionParameters.js:
Add `console.record` and `console.recordEnd`.

* UserInterface/Views/CanvasTabContentView.js:
(WI.CanvasTabContentView.prototype.showRepresentedObject):
Drive-by: remove logic that toggled the collapsed state of the navigation sidebar, as this
was not very controllable by the user and often was aggravating.

(WI.CanvasTabContentView.prototype._recordingStopped):
Only show the recording if it was not started from the console. This can determined by
CanvasManager when it recieves a recording if the recording's source is not the same as the
current canvas being recorded.

LayoutTests:

* inspector/canvas/recording-2d-expected.txt:
* inspector/canvas/recording-2d.html:
(performConsoleRecording):
* inspector/canvas/recording-webgl-expected.txt:
* inspector/canvas/recording-webgl.html:
(performConsoleRecording):
* inspector/canvas/resources/recording-utilities.js:
(TestPage.registerInitializer):
* js/console-expected.txt:
* js/console.html:

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

30 files changed:
LayoutTests/ChangeLog
LayoutTests/inspector/canvas/recording-2d-expected.txt
LayoutTests/inspector/canvas/recording-2d.html
LayoutTests/inspector/canvas/recording-webgl-expected.txt
LayoutTests/inspector/canvas/recording-webgl.html
LayoutTests/inspector/canvas/resources/recording-utilities.js
LayoutTests/js/console-expected.txt
LayoutTests/js/console.html
Source/JavaScriptCore/ChangeLog
Source/JavaScriptCore/inspector/JSGlobalObjectConsoleClient.cpp
Source/JavaScriptCore/inspector/JSGlobalObjectConsoleClient.h
Source/JavaScriptCore/inspector/protocol/Recording.json
Source/JavaScriptCore/runtime/ConsoleClient.h
Source/JavaScriptCore/runtime/ConsoleObject.cpp
Source/WebCore/ChangeLog
Source/WebCore/inspector/InspectorCanvas.cpp
Source/WebCore/inspector/InspectorCanvas.h
Source/WebCore/inspector/InspectorInstrumentation.cpp
Source/WebCore/inspector/InspectorInstrumentation.h
Source/WebCore/inspector/agents/InspectorCanvasAgent.cpp
Source/WebCore/inspector/agents/InspectorCanvasAgent.h
Source/WebCore/page/PageConsoleClient.cpp
Source/WebCore/page/PageConsoleClient.h
Source/WebCore/workers/WorkerConsoleClient.cpp
Source/WebCore/workers/WorkerConsoleClient.h
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/UserInterface/Controllers/CanvasManager.js
Source/WebInspectorUI/UserInterface/Models/NativeFunctionParameters.js
Source/WebInspectorUI/UserInterface/Views/CanvasTabContentView.js
Source/WebInspectorUI/UserInterface/Views/RecordingNavigationSidebarPanel.js

index f2890ca..774fa42 100644 (file)
@@ -1,3 +1,22 @@
+2017-12-04  Devin Rousso  <webkit@devinrousso.com>
+
+        Web Inspector: provide method for recording CanvasRenderingContext2D from JavaScript
+        https://bugs.webkit.org/show_bug.cgi?id=175166
+        <rdar://problem/34040740>
+
+        Reviewed by Joseph Pecoraro.
+
+        * inspector/canvas/recording-2d-expected.txt:
+        * inspector/canvas/recording-2d.html:
+        (performConsoleRecording):
+        * inspector/canvas/recording-webgl-expected.txt:
+        * inspector/canvas/recording-webgl.html:
+        (performConsoleRecording):
+        * inspector/canvas/resources/recording-utilities.js:
+        (TestPage.registerInitializer):
+        * js/console-expected.txt:
+        * js/console.html:
+
 2017-12-04  Ms2ger  <Ms2ger@igalia.com>
 
         [GTK] Rebaseline imported/w3c/web-platform-tests/html/semantics/interactive-elements/the-dialog-element/dialog-showModal.html
index fd123cd..01c5bb3 100644 (file)
@@ -1522,6 +1522,11 @@ data:
   29: "_evaluateAndWrap"
   30: [29,10,122,108]
 
+-- Running test case: Canvas.recording2D.Console
+PASS: The recording should have the name "TEST".
+PASS: The recording should have one frame.
+PASS: The first frame should have one action.
+
 -- Running test case: Canvas.recording2D.ActionParameterNaN
 PASS: The recording should have 1 frame.
 PASS: The first frame should have 1 action.
index c252102..ca9bafe 100644 (file)
@@ -395,6 +395,16 @@ function performActions() {
     executeFrameFunction();
 }
 
+function performConsoleActions() {
+    console.record(ctx, {name: "TEST"});
+
+    ctx.fill();
+
+    console.recordEnd(ctx);
+
+    ctx.stroke();
+}
+
 function performNaNActions() {
     ctx.globalAlpha = NaN;
 }
@@ -436,6 +446,14 @@ function test() {
     });
 
     suite.addTestCase({
+        name: "Canvas.recording2D.Console",
+        description: "Check that a recording can be triggered by console.record().",
+        test(resolve, reject) {
+            consoleRecord(resolve, reject);
+        },
+    });
+
+    suite.addTestCase({
         name: "Canvas.recording2D.ActionParameterNaN",
         description: "Check that NaN is converted into the proper value for serialization.",
         test(resolve, reject) {
@@ -476,18 +494,20 @@ function test() {
         description: "Check that a canvas is still able to be recorded after stopping a recording with no actions.",
         test(resolve, reject) {
             let canvas = getCanvas(WI.Canvas.ContextType.Canvas2D);
-            if (!canvas)
-                throw "Missing 2D canvas.";
+            if (!canvas) {
+                reject("Missing 2D canvas.");
+                return;
+            }
 
             let eventCount = 0;
             function handleRecordingStopped(event) {
                 InspectorTest.assert(event.data.canvas === canvas, "We should have stopped recording the selected canvas.");
-                ++eventCount;
+                InspectorTest.assert(!event.data.recording, "The recording payload should be null.");
 
+                ++eventCount;
                 if (eventCount == 1)
                     InspectorTest.pass("A recording should have been started and stopped once.");
-
-                if (eventCount >= 2) {
+                else if (eventCount >= 2) {
                     InspectorTest.pass("A recording should have been started and stopped twice.");
 
                     WI.canvasManager.removeEventListener(handleRecordingStopped);
index a7f30b7..e1e64d3 100644 (file)
@@ -1500,3 +1500,8 @@ data:
   16: "_evaluateAndWrap"
   17: [16,3,122,108]
 
+-- Running test case: Canvas.recordingWebGL.Console
+PASS: The recording should have the name "TEST".
+PASS: The recording should have one frame.
+PASS: The first frame should have one action.
+
index 54c102a..9cd9e36 100644 (file)
@@ -496,6 +496,16 @@ function performActions() {
     executeFrameFunction();
 }
 
+function performConsoleActions() {
+    console.record(context, {name: "TEST"});
+
+    context.createTexture();
+
+    console.recordEnd(context);
+
+    context.createBuffer();
+}
+
 function test() {
     let suite = InspectorTest.createAsyncSuite("Canvas.recordingWebGL");
 
@@ -532,6 +542,14 @@ function test() {
         },
     });
 
+    suite.addTestCase({
+        name: "Canvas.recordingWebGL.Console",
+        description: "Check that a recording can be triggered by console.record().",
+        test(resolve, reject) {
+            consoleRecord(resolve, reject);
+        },
+    });
+
     suite.runTestCasesAndFinish();
 }
 </script>
index cb8a9d6..5c95e06 100644 (file)
@@ -144,4 +144,15 @@ TestPage.registerInitializer(() => {
 
         return canvas;
     };
+
+    window.consoleRecord = function(resolve, reject) {
+        WI.canvasManager.awaitEvent(WI.CanvasManager.Event.RecordingStopped).then((event) => {
+            let recording = event.data.recording;
+            InspectorTest.expectEqual(recording.displayName, "TEST", "The recording should have the name \"TEST\".");
+            InspectorTest.expectEqual(recording.frames.length, 1, "The recording should have one frame.");
+            InspectorTest.expectEqual(recording.frames[0].actions.length, 1, "The first frame should have one action.");
+        }).then(resolve, reject);
+
+        InspectorTest.evaluateInPage(`performConsoleActions()`);
+    };
 });
index eb7f847..073aa92 100644 (file)
@@ -163,7 +163,61 @@ PASS descriptor.configurable is true
 PASS descriptor.writable is true
 PASS descriptor.enumerable is true
 
+console.record
+PASS typeof console.record is "function"
+PASS console.record.length is 0
+PASS descriptor.configurable is true
+PASS descriptor.writable is true
+PASS descriptor.enumerable is true
+
+console.recordEnd
+PASS typeof console.recordEnd is "function"
+PASS console.recordEnd.length is 0
+PASS descriptor.configurable is true
+PASS descriptor.writable is true
+PASS descriptor.enumerable is true
+
 PASS Object.getOwnPropertyNames(console).length is enumerablePropertyCount
+
+fuzzing of target for console.record
+PASS console.record() did not throw exception.
+PASS console.record(undefined) did not throw exception.
+PASS console.record(null) did not throw exception.
+PASS console.record(1) did not throw exception.
+PASS console.record("test") did not throw exception.
+PASS console.record([]) did not throw exception.
+PASS console.record({}) did not throw exception.
+PASS console.record(window) did not throw exception.
+PASS console.record(console) did not throw exception.
+
+fuzzing of options for console.record
+PASS console.record({}, undefined) did not throw exception.
+PASS console.record({}, null) did not throw exception.
+PASS console.record({}, 1) did not throw exception.
+PASS console.record({}, "test") did not throw exception.
+PASS console.record({}, []) did not throw exception.
+PASS console.record({}, {}) did not throw exception.
+PASS console.record({}, window) did not throw exception.
+PASS console.record({}, console) did not throw exception.
+
+fuzzing of target for console.recordEnd
+PASS console.recordEnd() did not throw exception.
+PASS console.recordEnd(undefined) did not throw exception.
+PASS console.recordEnd(null) did not throw exception.
+PASS console.recordEnd(1) did not throw exception.
+PASS console.recordEnd("test") did not throw exception.
+PASS console.recordEnd([]) did not throw exception.
+PASS console.recordEnd({}) did not throw exception.
+PASS console.recordEnd(window) did not throw exception.
+PASS console.recordEnd(console) did not throw exception.
+
+ensure multiple calls don't throw
+PASS console.recordEnd(window.canvas) did not throw exception.
+PASS console.recordEnd(window.canvas) did not throw exception.
+PASS console.recordEnd(window.canvas) did not throw exception.
+PASS console.record(window.canvas) did not throw exception.
+PASS console.record(window.canvas) did not throw exception.
+PASS console.record(window.canvas) did not throw exception.
 PASS successfullyParsed is true
 
 TEST COMPLETE
index 3f79f09..e5637c6 100644 (file)
@@ -46,7 +46,46 @@ for (property in console) {
 
 debug("");
 shouldBe("Object.getOwnPropertyNames(console).length", "enumerablePropertyCount");
+
+const consoleRecordArguments = [
+       `undefined`,
+       `null`,
+       `1`,
+       `"test"`,
+       `[]`,
+       `{}`,
+       `window`,
+       `console`,
+];
+
+debug("");
+debug("fuzzing of target for console.record");
+shouldNotThrow(`console.record()`);
+for (let argument of consoleRecordArguments)
+       shouldNotThrow(`console.record(${argument})`);
+
+debug("");
+debug("fuzzing of options for console.record");
+for (let argument of consoleRecordArguments)
+       shouldNotThrow(`console.record({}, ${argument})`);
+
+debug("");
+debug("fuzzing of target for console.recordEnd");
+shouldNotThrow(`console.recordEnd()`);
+for (let argument of consoleRecordArguments)
+       shouldNotThrow(`console.recordEnd(${argument})`);
+
+debug("");
+debug("ensure multiple calls don't throw");
+shouldNotThrow(`console.recordEnd(window.canvas)`);
+shouldNotThrow(`console.recordEnd(window.canvas)`);
+shouldNotThrow(`console.recordEnd(window.canvas)`);
+shouldNotThrow(`console.record(window.canvas)`);
+shouldNotThrow(`console.record(window.canvas)`);
+shouldNotThrow(`console.record(window.canvas)`);
+
 </script>
 <script src="../resources/js-test-post.js"></script>
+<canvas id="canvas"></canvas>
 </body>
 </html>
index 4564946..3da50a1 100644 (file)
@@ -1,3 +1,25 @@
+2017-12-04  Devin Rousso  <webkit@devinrousso.com>
+
+        Web Inspector: provide method for recording CanvasRenderingContext2D from JavaScript
+        https://bugs.webkit.org/show_bug.cgi?id=175166
+        <rdar://problem/34040740>
+
+        Reviewed by Joseph Pecoraro.
+
+        * inspector/protocol/Recording.json:
+        Add optional `name` that will be used by the frontend for uniquely identifying the Recording.
+
+        * inspector/JSGlobalObjectConsoleClient.h:
+        * inspector/JSGlobalObjectConsoleClient.cpp:
+        (Inspector::JSGlobalObjectConsoleClient::record):
+        (Inspector::JSGlobalObjectConsoleClient::recordEnd):
+
+        * runtime/ConsoleClient.h:
+        * runtime/ConsoleObject.cpp:
+        (JSC::ConsoleObject::finishCreation):
+        (JSC::consoleProtoFuncRecord):
+        (JSC::consoleProtoFuncRecordEnd):
+
 2017-12-03  Yusuke Suzuki  <utatane.tea@gmail.com>
 
         WTF shouldn't have both Thread and ThreadIdentifier
index 1f903ba..7c49dd3 100644 (file)
@@ -165,6 +165,9 @@ void JSGlobalObjectConsoleClient::timeStamp(ExecState*, Ref<ScriptArguments>&&)
     warnUnimplemented(ASCIILiteral("console.timeStamp"));
 }
 
+void JSGlobalObjectConsoleClient::record(ExecState*, Ref<ScriptArguments>&&) { }
+void JSGlobalObjectConsoleClient::recordEnd(ExecState*, Ref<ScriptArguments>&&) { }
+
 void JSGlobalObjectConsoleClient::warnUnimplemented(const String& method)
 {
     String message = method + " is currently ignored in JavaScript context inspection.";
index d9b259c..448917e 100644 (file)
@@ -53,6 +53,8 @@ protected:
     void time(JSC::ExecState*, const String& title) override;
     void timeEnd(JSC::ExecState*, const String& title) override;
     void timeStamp(JSC::ExecState*, Ref<ScriptArguments>&&) override;
+    void record(JSC::ExecState*, Ref<ScriptArguments>&&) override;
+    void recordEnd(JSC::ExecState*, Ref<ScriptArguments>&&) override;
 
 private:
     void warnUnimplemented(const String& method);
index 2912179..d7492d9 100644 (file)
@@ -35,7 +35,8 @@
                 { "name": "version", "type": "integer", "description": "Used for future/backwards compatibility." },
                 { "name": "type", "$ref": "Type" },
                 { "name": "initialState", "$ref": "InitialState", "description": "JSON data of inital state of object before recording." },
-                { "name": "data", "type": "array", "items": { "type": "any" }, "description": "Array of objects that can be referenced by index. Used to avoid duplicating objects." }
+                { "name": "data", "type": "array", "items": { "type": "any" }, "description": "Array of objects that can be referenced by index. Used to avoid duplicating objects." },
+                { "name": "name", "type": "string", "optional": true }
             ]
         }
     ]
index 4951f46..852c193 100644 (file)
@@ -62,6 +62,8 @@ public:
     virtual void time(ExecState*, const String& title) = 0;
     virtual void timeEnd(ExecState*, const String& title) = 0;
     virtual void timeStamp(ExecState*, Ref<Inspector::ScriptArguments>&&) = 0;
+    virtual void record(ExecState*, Ref<Inspector::ScriptArguments>&&) = 0;
+    virtual void recordEnd(ExecState*, Ref<Inspector::ScriptArguments>&&) = 0;
 
 private:
     enum ArgumentRequirement { ArgumentRequired, ArgumentNotRequired };
index 08a7a03..8a029d0 100644 (file)
@@ -57,6 +57,8 @@ static EncodedJSValue JSC_HOST_CALL consoleProtoFuncTimeStamp(ExecState*);
 static EncodedJSValue JSC_HOST_CALL consoleProtoFuncGroup(ExecState*);
 static EncodedJSValue JSC_HOST_CALL consoleProtoFuncGroupCollapsed(ExecState*);
 static EncodedJSValue JSC_HOST_CALL consoleProtoFuncGroupEnd(ExecState*);
+static EncodedJSValue JSC_HOST_CALL consoleProtoFuncRecord(ExecState*);
+static EncodedJSValue JSC_HOST_CALL consoleProtoFuncRecordEnd(ExecState*);
 
 const ClassInfo ConsoleObject::s_info = { "Console", &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(ConsoleObject) };
 
@@ -95,6 +97,8 @@ void ConsoleObject::finishCreation(VM& vm, JSGlobalObject* globalObject)
     JSC_NATIVE_FUNCTION_WITHOUT_TRANSITION("group", consoleProtoFuncGroup, static_cast<unsigned>(PropertyAttribute::None), 0);
     JSC_NATIVE_FUNCTION_WITHOUT_TRANSITION("groupCollapsed", consoleProtoFuncGroupCollapsed, static_cast<unsigned>(PropertyAttribute::None), 0);
     JSC_NATIVE_FUNCTION_WITHOUT_TRANSITION("groupEnd", consoleProtoFuncGroupEnd, static_cast<unsigned>(PropertyAttribute::None), 0);
+    JSC_NATIVE_FUNCTION_WITHOUT_TRANSITION("record", consoleProtoFuncRecord, static_cast<unsigned>(PropertyAttribute::None), 0);
+    JSC_NATIVE_FUNCTION_WITHOUT_TRANSITION("recordEnd", consoleProtoFuncRecordEnd, static_cast<unsigned>(PropertyAttribute::None), 0);
 }
 
 static String valueToStringWithUndefinedOrNullCheck(ExecState* exec, JSValue value)
@@ -367,4 +371,24 @@ static EncodedJSValue JSC_HOST_CALL consoleProtoFuncGroupEnd(ExecState* exec)
     return JSValue::encode(jsUndefined());
 }
 
+static EncodedJSValue JSC_HOST_CALL consoleProtoFuncRecord(ExecState* exec)
+{
+    ConsoleClient* client = exec->lexicalGlobalObject()->consoleClient();
+    if (!client)
+        return JSValue::encode(jsUndefined());
+
+    client->record(exec, Inspector::createScriptArguments(exec, 0));
+    return JSValue::encode(jsUndefined());
+}
+
+static EncodedJSValue JSC_HOST_CALL consoleProtoFuncRecordEnd(ExecState* exec)
+{
+    ConsoleClient* client = exec->lexicalGlobalObject()->consoleClient();
+    if (!client)
+        return JSValue::encode(jsUndefined());
+
+    client->recordEnd(exec, Inspector::createScriptArguments(exec, 0));
+    return JSValue::encode(jsUndefined());
+}
+
 } // namespace JSC
index 6ba94c6..5598c29 100644 (file)
@@ -1,3 +1,37 @@
+2017-12-04  Devin Rousso  <webkit@devinrousso.com>
+
+        Web Inspector: provide method for recording CanvasRenderingContext2D from JavaScript
+        https://bugs.webkit.org/show_bug.cgi?id=175166
+        <rdar://problem/34040740>
+
+        Reviewed by Joseph Pecoraro.
+
+        No new tests, updated existing tests.
+
+        * inspector/InspectorCanvas.h:
+        * inspector/InspectorCanvas.cpp:
+        (WebCore::InspectorCanvas::resetRecordingData):
+
+        * inspector/InspectorCanvasAgent.h:
+        * inspector/InspectorCanvasAgent.cpp:
+        (WebCore::InspectorCanvasAgent::didFinishRecordingCanvasFrame):
+        (WebCore::InspectorCanvasAgent::consoleStartRecordingCanvas):
+
+        * inspector/InspectorInstrumentation.h:
+        * inspector/InspectorInstrumentation.cpp:
+        (WebCore::InspectorInstrumentation::consoleStartRecordingCanvas):
+        (WebCore::InspectorInstrumentation::consoleStartRecordingCanvasImpl):
+
+        * page/PageConsoleClient.h:
+        * page/PageConsoleClient.cpp:
+        (WebCore::PageConsoleClient::record):
+        (WebCore::PageConsoleClient::recordEnd):
+
+        * workers/WorkerConsoleClient.h:
+        * workers/WorkerConsoleClient.cpp:
+        (WebCore::WorkerConsoleClient::record):
+        (WebCore::WorkerConsoleClient::recordEnd):
+
 2017-12-04  Youenn Fablet  <youenn@apple.com>
 
         Call "Terminate Service Worker" on all workers when all their clients are gone
index fed8aca..f97b50e 100644 (file)
@@ -102,6 +102,7 @@ void InspectorCanvas::resetRecordingData()
     m_actionNeedingSnapshot = nullptr;
     m_serializedDuplicateData = nullptr;
     m_indexedDuplicateData.clear();
+    m_recordingName = { };
     m_bufferLimit = 100 * 1024 * 1024;
     m_bufferUsed = 0;
     m_singleFrame = true;
index 2f6c647..cdd5ed3 100644 (file)
@@ -68,6 +68,9 @@ public:
     void finalizeFrame();
     void markCurrentFrameIncomplete();
 
+    const String& recordingName() const { return m_recordingName; }
+    void setRecordingName(const String& name) { m_recordingName = name; }
+
     void setBufferLimit(long);
     bool hasBufferSpace() const;
     long bufferUsed() const { return m_bufferUsed; }
@@ -116,6 +119,8 @@ private:
     RefPtr<JSON::ArrayOf<JSON::Value>> m_actionNeedingSnapshot;
     RefPtr<JSON::ArrayOf<JSON::Value>> m_serializedDuplicateData;
     Vector<DuplicateDataVariant> m_indexedDuplicateData;
+
+    String m_recordingName;
     double m_currentFrameStartTime { NAN };
     size_t m_bufferLimit { 100 * 1024 * 1024 };
     size_t m_bufferUsed { 0 };
index d4b51c7..41e059c 100644 (file)
@@ -882,6 +882,12 @@ void InspectorInstrumentation::stopProfilingImpl(InstrumentingAgents& instrument
         timelineAgent->stopFromConsole(exec, title);
 }
 
+void InspectorInstrumentation::consoleStartRecordingCanvasImpl(InstrumentingAgents& instrumentingAgents, HTMLCanvasElement& canvasElement, JSC::ExecState& exec, JSC::JSObject* options)
+{
+    if (InspectorCanvasAgent* canvasAgent = instrumentingAgents.inspectorCanvasAgent())
+        canvasAgent->consoleStartRecordingCanvas(canvasElement, exec, options);
+}
+
 void InspectorInstrumentation::didOpenDatabaseImpl(InstrumentingAgents& instrumentingAgents, RefPtr<Database>&& database, const String& domain, const String& name, const String& version)
 {
     if (!instrumentingAgents.inspectorEnvironment().developerExtrasEnabled())
index 5d91a3e..ede91af 100644 (file)
@@ -48,6 +48,7 @@
 #include "StorageArea.h"
 #include "WorkerGlobalScope.h"
 #include "WorkerInspectorController.h"
+#include <runtime/JSCInlines.h>
 #include <wtf/MemoryPressureHandler.h>
 #include <wtf/RefPtr.h>
 
@@ -225,6 +226,7 @@ public:
     static void consoleTimeStamp(Frame&, Ref<Inspector::ScriptArguments>&&);
     static void startProfiling(Page&, JSC::ExecState*, const String& title);
     static void stopProfiling(Page&, JSC::ExecState*, const String& title);
+    static void consoleStartRecordingCanvas(HTMLCanvasElement&, JSC::ExecState&, JSC::JSObject* options);
 
     static void didRequestAnimationFrame(Document&, int callbackId);
     static void didCancelAnimationFrame(Document&, int callbackId);
@@ -391,15 +393,15 @@ private:
     static void stopConsoleTimingImpl(InstrumentingAgents&, Frame&, const String& title, Ref<Inspector::ScriptCallStack>&&);
     static void stopConsoleTimingImpl(InstrumentingAgents&, const String& title, Ref<Inspector::ScriptCallStack>&&);
     static void consoleTimeStampImpl(InstrumentingAgents&, Frame&, Ref<Inspector::ScriptArguments>&&);
+    static void startProfilingImpl(InstrumentingAgents&, JSC::ExecState*, const String& title);
+    static void stopProfilingImpl(InstrumentingAgents&, JSC::ExecState*, const String& title);
+    static void consoleStartRecordingCanvasImpl(InstrumentingAgents&, HTMLCanvasElement&, JSC::ExecState&, JSC::JSObject* options);
 
     static void didRequestAnimationFrameImpl(InstrumentingAgents&, int callbackId, Document&);
     static void didCancelAnimationFrameImpl(InstrumentingAgents&, int callbackId, Document&);
     static InspectorInstrumentationCookie willFireAnimationFrameImpl(InstrumentingAgents&, int callbackId, Document&);
     static void didFireAnimationFrameImpl(const InspectorInstrumentationCookie&);
 
-    static void startProfilingImpl(InstrumentingAgents&, JSC::ExecState*, const String& title);
-    static void stopProfilingImpl(InstrumentingAgents&, JSC::ExecState*, const String& title);
-
     static void didOpenDatabaseImpl(InstrumentingAgents&, RefPtr<Database>&&, const String& domain, const String& name, const String& version);
 
     static void didDispatchDOMStorageEventImpl(InstrumentingAgents&, const String& key, const String& oldValue, const String& newValue, StorageType, SecurityOrigin*);
@@ -1389,6 +1391,13 @@ inline void InspectorInstrumentation::stopProfiling(Page& page, JSC::ExecState*
     stopProfilingImpl(instrumentingAgentsForPage(page), exec, title);
 }
 
+inline void InspectorInstrumentation::consoleStartRecordingCanvas(HTMLCanvasElement& canvasElement, JSC::ExecState& exec, JSC::JSObject* options)
+{
+    FAST_RETURN_IF_NO_FRONTENDS(void());
+    if (InstrumentingAgents* instrumentingAgents = instrumentingAgentsForDocument(&canvasElement.document()))
+        consoleStartRecordingCanvasImpl(*instrumentingAgents, canvasElement, exec, options);
+}
+
 inline void InspectorInstrumentation::didRequestAnimationFrame(Document& document, int callbackId)
 {
     if (InstrumentingAgents* instrumentingAgents = instrumentingAgentsForDocument(document))
index 3226c99..c6500c7 100644 (file)
@@ -522,11 +522,38 @@ void InspectorCanvasAgent::didFinishRecordingCanvasFrame(HTMLCanvasElement& canv
         .setData(inspectorCanvas->releaseData())
         .release();
 
+    const String& name = inspectorCanvas->recordingName();
+    if (!name.isEmpty())
+        recording->setName(name);
+
     m_frontendDispatcher->recordingFinished(inspectorCanvas->identifier(), WTFMove(recording));
 
     inspectorCanvas->resetRecordingData();
 }
 
+void InspectorCanvasAgent::consoleStartRecordingCanvas(HTMLCanvasElement& canvasElement, JSC::ExecState& exec, JSC::JSObject* options)
+{
+    auto* inspectorCanvas = findInspectorCanvas(canvasElement);
+    if (!inspectorCanvas)
+        return;
+
+    if (inspectorCanvas->canvas().renderingContext()->callTracingActive())
+        return;
+
+    inspectorCanvas->resetRecordingData();
+
+    if (options) {
+        if (JSC::JSValue optionName = options->get(&exec, JSC::Identifier::fromString(&exec, "name")))
+            inspectorCanvas->setRecordingName(optionName.toWTFString(&exec));
+        if (JSC::JSValue optionSingleFrame = options->get(&exec, JSC::Identifier::fromString(&exec, "singleFrame")))
+            inspectorCanvas->setSingleFrame(optionSingleFrame.toBoolean(&exec));
+        if (JSC::JSValue optionMemoryLimit = options->get(&exec, JSC::Identifier::fromString(&exec, "memoryLimit")))
+            inspectorCanvas->setBufferLimit(optionMemoryLimit.toNumber(&exec));
+    }
+
+    inspectorCanvas->canvas().renderingContext()->setCallTracingActive(true);
+}
+
 #if ENABLE(WEBGL)
 void InspectorCanvasAgent::didEnableExtension(WebGLRenderingContextBase& context, const String& extension)
 {
index dcbb62b..cd302f4 100644 (file)
@@ -87,6 +87,7 @@ public:
     void didChangeCanvasMemory(HTMLCanvasElement&);
     void recordCanvasAction(CanvasRenderingContext&, const String&, Vector<RecordCanvasActionVariant>&& = { });
     void didFinishRecordingCanvasFrame(HTMLCanvasElement&, bool forceDispatch = false);
+    void consoleStartRecordingCanvas(HTMLCanvasElement&, JSC::ExecState&, JSC::JSObject* options);
 #if ENABLE(WEBGL)
     void didEnableExtension(WebGLRenderingContextBase&, const String&);
     void didCreateProgram(WebGLRenderingContextBase&, WebGLProgram&);
index 9175810..960279e 100644 (file)
 #include "config.h"
 #include "PageConsoleClient.h"
 
+#include "CanvasRenderingContext2D.h"
 #include "Chrome.h"
 #include "ChromeClient.h"
 #include "Document.h"
 #include "Frame.h"
+#include "HTMLCanvasElement.h"
 #include "InspectorController.h"
 #include "InspectorInstrumentation.h"
+#include "JSCanvasRenderingContext2D.h"
+#include "JSHTMLCanvasElement.h"
 #include "JSMainThreadExecState.h"
 #include "MainFrame.h"
 #include "Page.h"
 #include "ScriptableDocumentParser.h"
 #include "Settings.h"
+#include <bindings/ScriptValue.h>
 #include <inspector/ConsoleMessage.h>
 #include <inspector/ScriptArguments.h>
 #include <inspector/ScriptCallStack.h>
 #include <inspector/ScriptCallStackFactory.h>
+#include <runtime/JSCInlines.h>
+#include <wtf/text/WTFString.h>
+
+#if ENABLE(WEBGL)
+#include "JSWebGLRenderingContext.h"
+#include "WebGLRenderingContext.h"
+#endif
 
 
 namespace WebCore {
@@ -206,4 +218,52 @@ void PageConsoleClient::timeStamp(JSC::ExecState*, Ref<ScriptArguments>&& argume
     InspectorInstrumentation::consoleTimeStamp(m_page.mainFrame(), WTFMove(arguments));
 }
 
+void PageConsoleClient::record(JSC::ExecState* exec, Ref<ScriptArguments>&& arguments)
+{
+    if (arguments->argumentCount() < 1)
+        return;
+
+    JSC::JSObject* target = arguments->argumentAt(0).jsValue().getObject();
+    if (!target)
+        return;
+
+    JSC::JSObject* options = nullptr;
+    if (arguments->argumentCount() >= 2)
+        options = arguments->argumentAt(1).jsValue().getObject();
+
+    if (HTMLCanvasElement* canvasElement = JSHTMLCanvasElement::toWrapped(*target->vm(), target))
+        InspectorInstrumentation::consoleStartRecordingCanvas(*canvasElement, *exec, options);
+    else if (CanvasRenderingContext2D* context2d = JSCanvasRenderingContext2D::toWrapped(*target->vm(), target))
+        InspectorInstrumentation::consoleStartRecordingCanvas(context2d->canvas(), *exec, options);
+#if ENABLE(WEBGL)
+    else if (WebGLRenderingContext* contextWebGL = JSWebGLRenderingContext::toWrapped(*target->vm(), target)) {
+        auto canvas = contextWebGL->canvas();
+        if (WTF::holds_alternative<RefPtr<HTMLCanvasElement>>(canvas))
+            InspectorInstrumentation::consoleStartRecordingCanvas(*WTF::get<RefPtr<HTMLCanvasElement>>(canvas), *exec, options);
+    }
+#endif
+}
+
+void PageConsoleClient::recordEnd(JSC::ExecState*, Ref<ScriptArguments>&& arguments)
+{
+    if (arguments->argumentCount() < 1)
+        return;
+
+    JSC::JSObject* target = arguments->argumentAt(0).jsValue().getObject();
+    if (!target)
+        return;
+
+    if (HTMLCanvasElement* canvasElement = JSHTMLCanvasElement::toWrapped(*target->vm(), target))
+        InspectorInstrumentation::didFinishRecordingCanvasFrame(*canvasElement, true);
+    else if (CanvasRenderingContext2D* context2d = JSCanvasRenderingContext2D::toWrapped(*target->vm(), target))
+        InspectorInstrumentation::didFinishRecordingCanvasFrame(context2d->canvas(), true);
+#if ENABLE(WEBGL)
+    else if (WebGLRenderingContext* contextWebGL = JSWebGLRenderingContext::toWrapped(*target->vm(), target)) {
+        auto canvas = contextWebGL->canvas();
+        if (WTF::holds_alternative<RefPtr<HTMLCanvasElement>>(canvas))
+            InspectorInstrumentation::didFinishRecordingCanvasFrame(*WTF::get<RefPtr<HTMLCanvasElement>>(canvas), true);
+    }
+#endif
+}
+
 } // namespace WebCore
index 1d44e59..9d6d79e 100644 (file)
@@ -74,6 +74,8 @@ protected:
     void time(JSC::ExecState*, const String& title) override;
     void timeEnd(JSC::ExecState*, const String& title) override;
     void timeStamp(JSC::ExecState*, Ref<Inspector::ScriptArguments>&&) override;
+    void record(JSC::ExecState*, Ref<Inspector::ScriptArguments>&&) override;
+    void recordEnd(JSC::ExecState*, Ref<Inspector::ScriptArguments>&&) override;
 
 private:
     Page& m_page;
index 6e71fc8..1c0fa36 100644 (file)
@@ -74,4 +74,7 @@ void WorkerConsoleClient::profileEnd(JSC::ExecState*, const String&) { }
 void WorkerConsoleClient::takeHeapSnapshot(JSC::ExecState*, const String&) { }
 void WorkerConsoleClient::timeStamp(JSC::ExecState*, Ref<ScriptArguments>&&) { }
 
+void WorkerConsoleClient::record(JSC::ExecState*, Ref<ScriptArguments>&&) { }
+void WorkerConsoleClient::recordEnd(JSC::ExecState*, Ref<ScriptArguments>&&) { }
+
 } // namespace WebCore
index a8d1228..a7e6c00 100644 (file)
@@ -50,6 +50,8 @@ protected:
     void time(JSC::ExecState*, const String& title) override;
     void timeEnd(JSC::ExecState*, const String& title) override;
     void timeStamp(JSC::ExecState*, Ref<Inspector::ScriptArguments>&&) override;
+    void record(JSC::ExecState*, Ref<Inspector::ScriptArguments>&&) override;
+    void recordEnd(JSC::ExecState*, Ref<Inspector::ScriptArguments>&&) override;
 
 private:
     WorkerGlobalScope& m_workerGlobalScope;
index 2d2cb49..5450689 100644 (file)
@@ -1,3 +1,28 @@
+2017-12-04  Devin Rousso  <webkit@devinrousso.com>
+
+        Web Inspector: provide method for recording CanvasRenderingContext2D from JavaScript
+        https://bugs.webkit.org/show_bug.cgi?id=175166
+        <rdar://problem/34040740>
+
+        Reviewed by Joseph Pecoraro.
+
+        * UserInterface/Controllers/CanvasManager.js:
+        (WI.CanvasManager.prototype.recordingFinished):
+        If a `name` is sent with the payload, use it as the suggested name.
+
+        * UserInterface/Models/NativeFunctionParameters.js:
+        Add `console.record` and `console.recordEnd`.
+
+        * UserInterface/Views/CanvasTabContentView.js:
+        (WI.CanvasTabContentView.prototype.showRepresentedObject):
+        Drive-by: remove logic that toggled the collapsed state of the navigation sidebar, as this
+        was not very controllable by the user and often was aggravating.
+
+        (WI.CanvasTabContentView.prototype._recordingStopped):
+        Only show the recording if it was not started from the console. This can determined by
+        CanvasManager when it recieves a recording if the recording's source is not the same as the
+        current canvas being recorded.
+
 2017-12-04  Matt Baker  <mattbaker@apple.com>
 
         Web Inspector: Canvas Tab initial user interface needs some polish
index f73b92f..7c7eb5d 100644 (file)
@@ -168,10 +168,13 @@ WI.CanvasManager = class CanvasManager extends WI.Object
     {
         // Called from WI.CanvasObserver.
 
-        this._recordingCanvas = null;
-
         let canvas = this._canvasIdentifierMap.get(canvasIdentifier);
         console.assert(canvas);
+
+        let fromConsole = canvas !== this._recordingCanvas;
+        if (!fromConsole)
+            this._recordingCanvas = null;
+
         if (!canvas)
             return;
 
@@ -179,12 +182,12 @@ WI.CanvasManager = class CanvasManager extends WI.Object
         let recording = recordingPayload ? WI.Recording.fromPayload(recordingPayload, frames) : null;
         if (recording) {
             recording.source = canvas;
-            recording.createDisplayName();
+            recording.createDisplayName(recordingPayload.name);
 
             canvas.recordingCollection.add(recording);
         }
 
-        this.dispatchEventToListeners(WI.CanvasManager.Event.RecordingStopped, {canvas, recording});
+        this.dispatchEventToListeners(WI.CanvasManager.Event.RecordingStopped, {canvas, recording, fromConsole});
     }
 
     extensionEnabled(canvasIdentifier, extension)
index 51705c6..629e0b7 100644 (file)
@@ -173,6 +173,8 @@ WI.NativeConstructorFunctionParameters = {
         log: "message, [...values]",
         profile: "name",
         profileEnd: "name",
+        record: "object, [options]",
+        recordEnd: "object",
         table: "data, [columns]",
         takeHeapSnapshot: "[label]",
         time: "name = \"default\"",
index da8aac7..29cfa83 100644 (file)
@@ -103,16 +103,12 @@ WI.CanvasTabContentView = class CanvasTabContentView extends WI.ContentBrowserTa
 
         if (representedObject instanceof WI.CanvasCollection || representedObject instanceof WI.ShaderProgram) {
             this._overviewNavigationItem.hidden = true;
-            WI.navigationSidebar.collapsed = true;
             return;
         }
 
         if (representedObject instanceof WI.Recording) {
             this._overviewNavigationItem.hidden = false;
-            representedObject.actions.then((actions) => {
-                this.navigationSidebarPanel.recording = representedObject;
-                WI.navigationSidebar.collapsed = false;
-            });
+            this.navigationSidebarPanel.recording = representedObject;
             return;
         }
 
@@ -245,7 +241,9 @@ WI.CanvasTabContentView = class CanvasTabContentView extends WI.ContentBrowserTa
             return;
         }
 
-        this._recordingAdded(recording);
+        this._recordingAdded(recording, {
+            suppressShowRecording: event.data.fromConsole,
+        });
     }
 
     _navigationSidebarImport(event)
index c718902..86ba06a 100644 (file)
@@ -58,6 +58,9 @@ WI.RecordingNavigationSidebarPanel = class RecordingNavigationSidebarPanel exten
         }
 
         this._recording.actions.then((actions) => {
+            if (recording !== this._recording)
+                return;
+
             this.contentTreeOutline.element.dataset.indent = Number.countDigits(actions.length);
 
             if (actions[0] instanceof WI.RecordingInitialStateAction)