WindowEventLoop should be shared among similar origin documents
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 8 Nov 2019 01:13:19 +0000 (01:13 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 8 Nov 2019 01:13:19 +0000 (01:13 +0000)
https://bugs.webkit.org/show_bug.cgi?id=203882

Reviewed by Wenson Hsieh.

Source/WebCore:

Made WindowEventLoop shared across similar origin documents.

Also added internals.queueTask to directly test the event loop behavior
since implicitly testing it via other features has been very cumbersome.

This will help test other features that use the HTML5 event loop as well.

Tests: http/tests/eventloop/queue-task-across-cross-site-frames.html
       http/tests/eventloop/queue-task-across-frames.html

* dom/Document.cpp:
(WebCore::Document::eventLoop): Use WindowEventLoop::ensureForRegistrableDomain.
* dom/WindowEventLoop.cpp:
(WebCore::WindowEventLoop::ensureForRegistrableDomain): Added. Replaces create,
and returns an existing WindowEventLoop if the RegistrableDomain matches.
(WebCore::WindowEventLoop::WindowEventLoop): Added.
(WebCore::WindowEventLoop::~WindowEventLoop): Added. Removes itself from the map.
* dom/WindowEventLoop.h:
* testing/Internals.cpp:
(WebCore::Internals::queueTask): Added.
* testing/Internals.h:
* testing/Internals.idl:

LayoutTests:

Added some tests to make sure the event loop is shared among similar origin documents.

* http/tests/eventloop: Added.
* http/tests/eventloop/queue-task-across-cross-site-frames-expected.txt: Added.
* http/tests/eventloop/queue-task-across-cross-site-frames.html: Added.
* http/tests/eventloop/queue-task-across-frames-expected.txt: Added.
* http/tests/eventloop/queue-task-across-frames.html: Added.
* http/tests/eventloop/resources: Added.
* http/tests/eventloop/resources/eventloop-helper.html: Added.

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

13 files changed:
LayoutTests/ChangeLog
LayoutTests/http/tests/eventloop/queue-task-across-cross-site-frames-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/eventloop/queue-task-across-cross-site-frames.html [new file with mode: 0644]
LayoutTests/http/tests/eventloop/queue-task-across-frames-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/eventloop/queue-task-across-frames.html [new file with mode: 0644]
LayoutTests/http/tests/eventloop/resources/eventloop-helper.html [new file with mode: 0644]
Source/WebCore/ChangeLog
Source/WebCore/dom/Document.cpp
Source/WebCore/dom/WindowEventLoop.cpp
Source/WebCore/dom/WindowEventLoop.h
Source/WebCore/testing/Internals.cpp
Source/WebCore/testing/Internals.h
Source/WebCore/testing/Internals.idl

index dec3e51..351133e 100644 (file)
@@ -1,3 +1,20 @@
+2019-11-07  Ryosuke Niwa  <rniwa@webkit.org>
+
+        WindowEventLoop should be shared among similar origin documents
+        https://bugs.webkit.org/show_bug.cgi?id=203882
+
+        Reviewed by Wenson Hsieh.
+
+        Added some tests to make sure the event loop is shared among similar origin documents.
+
+        * http/tests/eventloop: Added.
+        * http/tests/eventloop/queue-task-across-cross-site-frames-expected.txt: Added.
+        * http/tests/eventloop/queue-task-across-cross-site-frames.html: Added.
+        * http/tests/eventloop/queue-task-across-frames-expected.txt: Added.
+        * http/tests/eventloop/queue-task-across-frames.html: Added.
+        * http/tests/eventloop/resources: Added.
+        * http/tests/eventloop/resources/eventloop-helper.html: Added.
+
 2019-11-07  youenn fablet  <youenn@apple.com>
 
         Layout Test http/tests/appcache/remove-cache.html is a flaky failure
diff --git a/LayoutTests/http/tests/eventloop/queue-task-across-cross-site-frames-expected.txt b/LayoutTests/http/tests/eventloop/queue-task-across-cross-site-frames-expected.txt
new file mode 100644 (file)
index 0000000..6ccec2f
--- /dev/null
@@ -0,0 +1,11 @@
+This tests the order by which tasks are scheduled across documents that are not similar origins.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS logs.join(", ") is "1, 2, 3, 4, 5"
+PASS crossOriginLogs.join(", ") is "10, 11, 12"
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/eventloop/queue-task-across-cross-site-frames.html b/LayoutTests/http/tests/eventloop/queue-task-across-cross-site-frames.html
new file mode 100644 (file)
index 0000000..2a5ad27
--- /dev/null
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script src="../resources/js-test-pre.js"></script>
+<script>
+
+description('This tests the order by which tasks are scheduled across documents that are not similar origins.');
+
+if (!window.internals)
+    testFailed('This test relies on window.internals');
+else {
+    jsTestIsAsync = true;
+    window.onload = startTest;
+}
+
+logs = [];
+crossOriginLogs = [];
+
+async function startTest()
+{
+    const frame1 = document.createElement('iframe');
+    document.body.appendChild(frame1);
+
+    const frame2 = document.createElement('iframe');
+    frame2.name = 'frame2';
+    frame2.src = 'http://localhost:8000/eventloop/resources/eventloop-helper.html';
+    document.body.appendChild(frame2);
+
+    const frame3 = document.createElement('iframe');
+    frame3.src = 'resources/eventloop-helper.html';
+    document.body.appendChild(frame3);
+
+    const frame4 = document.createElement('iframe');
+    frame4.name = 'frame4';
+    frame4.src = 'http://localhost:8000/eventloop/resources/eventloop-helper.html';
+
+    await waitForLoad(frame3);
+    frame3.contentDocument.body.appendChild(frame4);
+
+    await waitForLoad(frame2);
+    await waitForLoad(frame4);
+
+    frame3.contentWindow.internals.queueTask("DOMManipulation", () => logs.push('1'));
+    frame3.contentWindow.internals.queueTask("DOMManipulation", () => logs.push('2'));
+    internals.queueTask("DOMManipulation", () => logs.push('3'));
+    frame1.contentWindow.internals.queueTask("DOMManipulation", () => logs.push('4'));
+    internals.queueTask("DOMManipulation", () => logs.push('5'));
+
+    frame2.contentWindow.postMessage({
+        type: 'run',
+        order: ['frame2', 'frame2', 'self'],
+        startingNumber: 10,
+    }, '*');
+
+    setTimeout(() => {
+        shouldBeEqualToString('logs.join(", ")', '1, 2, 3, 4, 5');
+        shouldBeEqualToString('crossOriginLogs.join(", ")', '10, 11, 12');
+        finishJSTest();
+    }, 100);
+}
+
+const loadedFrames = new Map;
+onmessage = (event) => {
+    if (event.data.type == 'load') {
+        const resolve = loadedFrames.get(event.source);
+        if (resolve)
+            resolve();
+        else
+            loadedFrames.set(event.source, null);
+    } else if (event.data.type == 'logs')
+        crossOriginLogs = event.data.logs;
+}
+
+function waitForLoad(frame)
+{
+    if (loadedFrames.has(frame.contentWindow))
+        return Promise.resolve();
+    return new Promise((resolve) => loadedFrames.set(frame.contentWindow, resolve));
+}
+
+successfullyParsed = true;
+
+</script>
+<script src="../resources/js-test-post.js"></script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/eventloop/queue-task-across-frames-expected.txt b/LayoutTests/http/tests/eventloop/queue-task-across-frames-expected.txt
new file mode 100644 (file)
index 0000000..0c0465d
--- /dev/null
@@ -0,0 +1,10 @@
+This tests the order by which tasks are scheduled across documents of similar origins.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS logs.join(", ") is "1, 2, 3, 4, 5, 6"
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/eventloop/queue-task-across-frames.html b/LayoutTests/http/tests/eventloop/queue-task-across-frames.html
new file mode 100644 (file)
index 0000000..9485ca1
--- /dev/null
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script src="../resources/js-test-pre.js"></script>
+<script>
+
+description('This tests the order by which tasks are scheduled across documents of similar origins.');
+
+if (!window.internals)
+    testFailed('This test relies on window.internals');
+else {
+    jsTestIsAsync = true;
+    logs = [];
+
+    frame1 = document.createElement('iframe');
+    document.body.appendChild(frame1);
+
+    frame2 = document.createElement('iframe');
+    frame2.src = 'resources/eventloop-helper.html';
+    frame2.addEventListener('load', runTest);
+    document.body.appendChild(frame2);
+}
+
+function runTest() {
+    internals.queueTask("DOMManipulation", () => logs.push('1'));
+    frame1.contentWindow.internals.queueTask("DOMManipulation", () => logs.push('2'));
+    internals.queueTask("DOMManipulation", () => logs.push('3'));
+    frame2.contentWindow.internals.queueTask("DOMManipulation", () => logs.push('4'));
+    frame1.contentWindow.internals.queueTask("DOMManipulation", () => logs.push('5'));
+    internals.queueTask("DOMManipulation", () => logs.push('6'));
+
+    setTimeout(() => {
+        shouldBeEqualToString('logs.join(", ")', '1, 2, 3, 4, 5, 6');
+        finishJSTest();
+    }, 100);
+}
+
+successfullyParsed = true;
+
+</script>
+<script src="../resources/js-test-post.js"></script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/eventloop/resources/eventloop-helper.html b/LayoutTests/http/tests/eventloop/resources/eventloop-helper.html
new file mode 100644 (file)
index 0000000..e26197b
--- /dev/null
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script>
+
+onload = top.postMessage({'type': 'load'}, '*');
+onmessage = (event) => {
+    if (event.data.type != 'run')
+        return;
+
+    let number = event.data.startingNumber;
+    let logs = [];
+    for (const frameName of event.data.order) {
+        const windowProxy = frameName == 'self' ? self : top[frameName];
+        windowProxy.internals.queueTask("DOMManipulation", () => logs.push(number++));
+    }
+
+    setTimeout(() => {
+        top.postMessage({'type': 'logs', logs}, '*');
+    }, 0);
+}
+
+</script>
+</body>
+</html>
index b71fe17..037416c 100644 (file)
@@ -1,3 +1,33 @@
+2019-11-07  Ryosuke Niwa  <rniwa@webkit.org>
+
+        WindowEventLoop should be shared among similar origin documents
+        https://bugs.webkit.org/show_bug.cgi?id=203882
+
+        Reviewed by Wenson Hsieh.
+
+        Made WindowEventLoop shared across similar origin documents.
+
+        Also added internals.queueTask to directly test the event loop behavior
+        since implicitly testing it via other features has been very cumbersome.
+
+        This will help test other features that use the HTML5 event loop as well.
+
+        Tests: http/tests/eventloop/queue-task-across-cross-site-frames.html
+               http/tests/eventloop/queue-task-across-frames.html
+
+        * dom/Document.cpp:
+        (WebCore::Document::eventLoop): Use WindowEventLoop::ensureForRegistrableDomain.
+        * dom/WindowEventLoop.cpp:
+        (WebCore::WindowEventLoop::ensureForRegistrableDomain): Added. Replaces create,
+        and returns an existing WindowEventLoop if the RegistrableDomain matches.
+        (WebCore::WindowEventLoop::WindowEventLoop): Added.
+        (WebCore::WindowEventLoop::~WindowEventLoop): Added. Removes itself from the map.
+        * dom/WindowEventLoop.h:
+        * testing/Internals.cpp:
+        (WebCore::Internals::queueTask): Added.
+        * testing/Internals.h:
+        * testing/Internals.idl:
+
 2019-11-07  Chris Dumez  <cdumez@apple.com>
 
         Drop GenericEventQueue class now that it is unused
index a4032ac..d5370f9 100644 (file)
@@ -6224,14 +6224,9 @@ void Document::pendingTasksTimerFired()
 AbstractEventLoop& Document::eventLoop()
 {
     ASSERT(isMainThread());
-    if (!m_eventLoop) {
-        if (m_contextDocument)
-            m_eventLoop = m_contextDocument->m_eventLoop;
-        else // FIXME: Documents of similar origin should share the same event loop.
-            m_eventLoop = WindowEventLoop::create();
-    }
+    if (UNLIKELY(!m_eventLoop))
+        m_eventLoop = WindowEventLoop::ensureForRegistrableDomain(RegistrableDomain { securityOrigin().data() });
     return *m_eventLoop;
-
 }
 
 void Document::suspendScheduledTasks(ReasonForSuspension reason)
index a582b11..d685f61 100644 (file)
 
 namespace WebCore {
 
-Ref<WindowEventLoop> WindowEventLoop::create()
+static HashMap<RegistrableDomain, WindowEventLoop*>& windowEventLoopMap()
 {
-    return adoptRef(*new WindowEventLoop);
+    RELEASE_ASSERT(isMainThread());
+    static NeverDestroyed<HashMap<RegistrableDomain, WindowEventLoop*>> map;
+    return map.get();
+}
+
+Ref<WindowEventLoop> WindowEventLoop::ensureForRegistrableDomain(const RegistrableDomain& domain)
+{
+    auto addResult = windowEventLoopMap().add(domain, nullptr);
+    if (UNLIKELY(addResult.isNewEntry)) {
+        auto newEventLoop = adoptRef(*new WindowEventLoop(domain));
+        addResult.iterator->value = newEventLoop.ptr();
+        return newEventLoop;
+    }
+    return *addResult.iterator->value;
+}
+
+inline WindowEventLoop::WindowEventLoop(const RegistrableDomain& domain)
+    : m_domain(domain)
+{
+}
+
+WindowEventLoop::~WindowEventLoop()
+{
+    auto didRemove = windowEventLoopMap().remove(m_domain);
+    RELEASE_ASSERT(didRemove);
 }
 
 void WindowEventLoop::queueTask(TaskSource source, ScriptExecutionContext& context, TaskFunction&& task)
index e73c2eb..43032d3 100644 (file)
@@ -27,6 +27,7 @@
 
 #include "AbstractEventLoop.h"
 #include "DocumentIdentifier.h"
+#include "RegistrableDomain.h"
 #include <wtf/HashSet.h>
 
 namespace WebCore {
@@ -36,7 +37,9 @@ class Document;
 // https://html.spec.whatwg.org/multipage/webappapis.html#window-event-loop
 class WindowEventLoop final : public AbstractEventLoop {
 public:
-    static Ref<WindowEventLoop> create();
+    static Ref<WindowEventLoop> ensureForRegistrableDomain(const RegistrableDomain&);
+
+    ~WindowEventLoop();
 
     void queueTask(TaskSource, ScriptExecutionContext&, TaskFunction&&) override;
 
@@ -45,7 +48,7 @@ public:
     void stop(Document&);
 
 private:
-    WindowEventLoop() = default;
+    WindowEventLoop(const RegistrableDomain&);
 
     void scheduleToRunIfNeeded();
     void run();
@@ -58,8 +61,9 @@ private:
 
     // Use a global queue instead of multiple task queues since HTML5 spec allows UA to pick arbitrary queue.
     Vector<Task> m_tasks;
-    bool m_isScheduledToRun { false };
     HashSet<DocumentIdentifier> m_documentIdentifiersForSuspendedTasks;
+    RegistrableDomain m_domain;
+    bool m_isScheduledToRun { false };
 };
 
 } // namespace WebCore
index a74b4fc..a182d90 100644 (file)
@@ -28,6 +28,7 @@
 #include "Internals.h"
 
 #include "AXObjectCache.h"
+#include "AbstractEventLoop.h"
 #include "ActiveDOMCallbackMicrotask.h"
 #include "ActivityState.h"
 #include "AnimationTimeline.h"
@@ -4675,6 +4676,21 @@ void Internals::postTask(RefPtr<VoidCallback>&& callback)
     });
 }
 
+ExceptionOr<void> Internals::queueTask(ScriptExecutionContext& context, const String& taskSourceName, RefPtr<VoidCallback>&& callback)
+{
+    TaskSource source;
+    if (taskSourceName == "DOMManipulation")
+        source = TaskSource::DOMManipulation;
+    else
+        return Exception { NotSupportedError };
+
+    context.eventLoop().queueTask(source, context, [callback = WTFMove(callback)]() {
+        callback->handleEvent();
+    });
+
+    return { };
+}
+
 Vector<String> Internals::accessKeyModifiers() const
 {
     Vector<String> accessKeyModifierStrings;
index 78627d6..de4e64d 100644 (file)
@@ -776,6 +776,7 @@ public:
     bool isSystemPreviewImage(Element&) const;
 
     void postTask(RefPtr<VoidCallback>&&);
+    ExceptionOr<void> queueTask(ScriptExecutionContext&, const String& source, RefPtr<VoidCallback>&&);
     void markContextAsInsecure();
 
     bool usingAppleInternalSDK() const;
index 33f4865..91b56fa 100644 (file)
@@ -765,6 +765,7 @@ enum CompositingPolicy {
     boolean usingAppleInternalSDK();
 
     void postTask(VoidCallback callback);
+    [CallWith=ScriptExecutionContext, MayThrowException] void queueTask(DOMString source, VoidCallback callback);
     void markContextAsInsecure();
 
     void setMaxCanvasPixelMemory(unsigned long size);