Commit e5629306 authored by Hiroki Nakagawa's avatar Hiroki Nakagawa Committed by Commit Bot

Worklet: Implement the "module responses map" concept

This CL implements the "module responses map" concept defined in the Worklet
spec. This map caches module scripts to ensure that WorkletGlobalScopes created
at different times contain the same set of script source text and have the same
behaviour.

The map is not used yet in this CL. Following CLs will wire it up with
ModuleScriptLoader via WorkletModuleResponsesMapProxy (see the design doc for
details).

Worklet spec: https://drafts.css-houdini.org/worklets/#module-responses-map
Design doc: https://docs.google.com/document/d/1cgLcrua7H_7x_o5GlzYrAi2qt-TqTzgtOeixFAugR6g/edit?usp=sharing

Bug: 726576
Change-Id: Ib2af974e4b48eaf23ed2a38c74da5f2a503568d9
Reviewed-on: https://chromium-review.googlesource.com/549601Reviewed-by: default avatarKouhei Ueno <kouhei@chromium.org>
Commit-Queue: Hiroki Nakagawa <nhiroki@chromium.org>
Cr-Commit-Position: refs/heads/master@{#485859}
parent e2dce251
......@@ -1554,6 +1554,7 @@ source_set("unit_tests") {
"workers/ThreadedWorkletTest.cpp",
"workers/WorkerThreadTest.cpp",
"workers/WorkerThreadTestHelper.h",
"workers/WorkletModuleResponsesMapTest.cpp",
"xml/XPathFunctionsTest.cpp",
"xml/parser/SharedBufferReaderTest.cpp",
]
......
......@@ -77,6 +77,7 @@ blink_core_sources("loader") {
"appcache/ApplicationCache.h",
"appcache/ApplicationCacheHost.cpp",
"appcache/ApplicationCacheHost.h",
"modulescript/ModuleScriptCreationParams.h",
"modulescript/ModuleScriptFetchRequest.h",
"modulescript/ModuleScriptLoader.cpp",
"modulescript/ModuleScriptLoader.h",
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef ModuleScriptCreationParams_h
#define ModuleScriptCreationParams_h
#include "platform/CrossThreadCopier.h"
#include "platform/loader/fetch/AccessControlStatus.h"
#include "platform/weborigin/KURL.h"
#include "platform/wtf/Optional.h"
#include "platform/wtf/text/WTFString.h"
#include "public/platform/WebURLRequest.h"
namespace blink {
// A ModuleScriptCreationParams carries parameters for creating ModuleScript.
class ModuleScriptCreationParams {
public:
ModuleScriptCreationParams(
const KURL& response_url,
const String& source_text,
WebURLRequest::FetchCredentialsMode fetch_credentials_mode,
AccessControlStatus access_control_status)
: response_url_(response_url),
source_text_(source_text),
fetch_credentials_mode_(fetch_credentials_mode),
access_control_status_(access_control_status) {}
~ModuleScriptCreationParams() = default;
const KURL& GetResponseUrl() const { return response_url_; };
const String& GetSourceText() const { return source_text_; }
WebURLRequest::FetchCredentialsMode GetFetchCredentialsMode() const {
return fetch_credentials_mode_;
}
AccessControlStatus GetAccessControlStatus() const {
return access_control_status_;
}
private:
const KURL response_url_;
const String source_text_;
const WebURLRequest::FetchCredentialsMode fetch_credentials_mode_;
const AccessControlStatus access_control_status_;
};
} // namespace blink
#endif // ModuleScriptCreationParams_h
......@@ -81,6 +81,8 @@ blink_core_sources("workers") {
"WorkletGlobalScope.cpp",
"WorkletGlobalScope.h",
"WorkletGlobalScopeProxy.h",
"WorkletModuleResponsesMap.cpp",
"WorkletModuleResponsesMap.h",
"WorkletModuleTreeClient.cpp",
"WorkletModuleTreeClient.h",
"WorkletPendingTasks.cpp",
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "core/workers/WorkletModuleResponsesMap.h"
#include "platform/wtf/Optional.h"
namespace blink {
class WorkletModuleResponsesMap::Entry
: public GarbageCollectedFinalized<Entry> {
public:
enum class State { kFetching, kFetched, kFailed };
State GetState() const { return state_; }
ModuleScriptCreationParams GetParams() const { return *params_; }
void AddClient(Client* client) {
// Clients can be added only while a module script is being fetched.
DCHECK_EQ(State::kFetching, state_);
clients_.push_back(client);
}
void NotifyUpdate(const ModuleScriptCreationParams& params) {
AdvanceState(State::kFetched);
params_.emplace(params);
for (Client* client : clients_)
client->OnRead(params);
clients_.clear();
}
void NotifyFailure() {
AdvanceState(State::kFailed);
for (Client* client : clients_)
client->OnFailed();
clients_.clear();
}
DEFINE_INLINE_TRACE() { visitor->Trace(clients_); }
private:
void AdvanceState(State new_state) {
switch (state_) {
case State::kFetching:
DCHECK(new_state == State::kFetched || new_state == State::kFailed);
break;
case State::kFetched:
case State::kFailed:
NOTREACHED();
break;
}
state_ = new_state;
}
State state_ = State::kFetching;
WTF::Optional<ModuleScriptCreationParams> params_;
HeapVector<Member<Client>> clients_;
};
// Implementation of the first half of the custom fetch defined in the
// "fetch a worklet script" algorithm:
// https://drafts.css-houdini.org/worklets/#fetch-a-worklet-script
//
// "To perform the fetch given request, perform the following steps:"
// Step 1: "Let cache be the moduleResponsesMap."
// Step 2: "Let url be request's url."
void WorkletModuleResponsesMap::ReadOrCreateEntry(const KURL& url,
Client* client) {
DCHECK(IsMainThread());
auto it = entries_.find(url);
if (it != entries_.end()) {
Entry* entry = it->value;
switch (entry->GetState()) {
case Entry::State::kFetching:
// Step 3: "If cache contains an entry with key url whose value is
// "fetching", wait until that entry's value changes, then proceed to
// the next step."
entry->AddClient(client);
return;
case Entry::State::kFetched:
// Step 4: "If cache contains an entry with key url, asynchronously
// complete this algorithm with that entry's value, and abort these
// steps."
client->OnRead(entry->GetParams());
return;
case Entry::State::kFailed:
// Module fetching failed before. Abort following steps.
client->OnFailed();
return;
}
}
// Step 5: "Create an entry in cache with key url and value "fetching"."
entries_.insert(url, new Entry);
// Step 6: "Fetch request."
// Running the callback with an empty params will make the fetcher to fallback
// to regular module loading and Write() will be called once the fetch is
// complete.
client->OnFetchNeeded();
}
// Implementation of the second half of the custom fetch defined in the
// "fetch a worklet script" algorithm:
// https://drafts.css-houdini.org/worklets/#fetch-a-worklet-script
void WorkletModuleResponsesMap::UpdateEntry(
const KURL& url,
const ModuleScriptCreationParams& params) {
DCHECK(IsMainThread());
DCHECK(entries_.Contains(url));
Entry* entry = entries_.find(url)->value;
// Step 7: "Let response be the result of fetch when it asynchronously
// completes."
// Step 8: "Set the value of the entry in cache whose key is url to response,
// and asynchronously complete this algorithm with response."
entry->NotifyUpdate(params);
}
void WorkletModuleResponsesMap::InvalidateEntry(const KURL& url) {
DCHECK(IsMainThread());
DCHECK(entries_.Contains(url));
Entry* entry = entries_.find(url)->value;
entry->NotifyFailure();
}
DEFINE_TRACE(WorkletModuleResponsesMap) {
visitor->Trace(entries_);
}
} // namespace blink
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef WorkletModuleResponsesMap_h
#define WorkletModuleResponsesMap_h
#include "core/CoreExport.h"
#include "core/loader/modulescript/ModuleScriptCreationParams.h"
#include "platform/heap/Heap.h"
#include "platform/heap/HeapAllocator.h"
#include "platform/weborigin/KURL.h"
#include "platform/weborigin/KURLHash.h"
namespace blink {
// WorkletModuleResponsesMap implements the module responses map concept and the
// "fetch a worklet script" algorithm:
// https://drafts.css-houdini.org/worklets/#module-responses-map
// https://drafts.css-houdini.org/worklets/#fetch-a-worklet-script
class CORE_EXPORT WorkletModuleResponsesMap
: public GarbageCollectedFinalized<WorkletModuleResponsesMap> {
public:
// Used for notifying results of ReadOrCreateEntry(). See comments on the
// function for details.
class Client : public GarbageCollectedMixin {
public:
virtual ~Client() {}
virtual void OnRead(const ModuleScriptCreationParams&) = 0;
virtual void OnFetchNeeded() = 0;
virtual void OnFailed() = 0;
};
WorkletModuleResponsesMap() = default;
// Reads an entry for a given URL, or creates a placeholder entry:
// 1) If an entry is already fetched, synchronously calls Client::OnRead().
// 2) If an entry is now being fetched, pushes a given client into the entry's
// waiting queue and asynchronously calls Client::OnRead() on the
// completion of the fetch.
// 3) If an entry doesn't exist, creates a placeholder entry and synchronously
// calls Client::OnFetchNeeded. A caller is required to fetch a module
// script and update the entry via UpdateEntry().
void ReadOrCreateEntry(const KURL&, Client*);
// Updates an entry in 'fetching' state to 'fetched'.
void UpdateEntry(const KURL&, const ModuleScriptCreationParams&);
// Marks an entry as "failed" state and calls OnFailed() for waiting clients.
void InvalidateEntry(const KURL&);
DECLARE_TRACE();
private:
class Entry;
// TODO(nhiroki): Keep the insertion order of top-level modules to replay
// addModule() calls for a newly created global scope.
// See https://drafts.css-houdini.org/worklets/#creating-a-workletglobalscope
HeapHashMap<KURL, Member<Entry>> entries_;
};
} // namespace blink
#endif // WorkletModuleResponsesMap_h
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "core/loader/modulescript/ModuleScriptCreationParams.h"
#include "core/workers/WorkletModuleResponsesMap.h"
#include "platform/weborigin/KURL.h"
#include "platform/wtf/Functional.h"
#include "platform/wtf/Optional.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace blink {
namespace {
class ClientImpl final : public GarbageCollectedFinalized<ClientImpl>,
public WorkletModuleResponsesMap::Client {
USING_GARBAGE_COLLECTED_MIXIN(ClientImpl);
public:
enum class Result { kInitial, kOK, kNeedsFetching, kFailed };
void OnRead(const ModuleScriptCreationParams& params) override {
ASSERT_EQ(Result::kInitial, result_);
result_ = Result::kOK;
params_.emplace(params);
}
void OnFetchNeeded() override {
ASSERT_EQ(Result::kInitial, result_);
result_ = Result::kNeedsFetching;
}
void OnFailed() override {
ASSERT_EQ(Result::kInitial, result_);
result_ = Result::kFailed;
}
Result GetResult() const { return result_; }
WTF::Optional<ModuleScriptCreationParams> GetParams() const {
return params_;
}
private:
Result result_ = Result::kInitial;
WTF::Optional<ModuleScriptCreationParams> params_;
};
} // namespace
TEST(WorkletModuleResponsesMapTest, Basic) {
WorkletModuleResponsesMap* map = new WorkletModuleResponsesMap;
const KURL kUrl(kParsedURLString, "https://example.com/foo.js");
// An initial read call creates a placeholder entry and asks the client to
// fetch a module script.
ClientImpl* client1 = new ClientImpl;
map->ReadOrCreateEntry(kUrl, client1);
EXPECT_EQ(ClientImpl::Result::kNeedsFetching, client1->GetResult());
EXPECT_FALSE(client1->GetParams().has_value());
// The entry is now being fetched. Following read calls should wait for the
// completion.
ClientImpl* client2 = new ClientImpl;
map->ReadOrCreateEntry(kUrl, client2);
EXPECT_EQ(ClientImpl::Result::kInitial, client2->GetResult());
ClientImpl* client3 = new ClientImpl;
map->ReadOrCreateEntry(kUrl, client3);
EXPECT_EQ(ClientImpl::Result::kInitial, client3->GetResult());
// An update call should notify the waiting clients.
ModuleScriptCreationParams params(kUrl, "// dummy script",
WebURLRequest::kFetchCredentialsModeOmit,
kNotSharableCrossOrigin);
map->UpdateEntry(kUrl, params);
EXPECT_EQ(ClientImpl::Result::kOK, client2->GetResult());
EXPECT_TRUE(client2->GetParams().has_value());
EXPECT_EQ(ClientImpl::Result::kOK, client3->GetResult());
EXPECT_TRUE(client3->GetParams().has_value());
}
TEST(WorkletModuleResponsesMapTest, Failure) {
WorkletModuleResponsesMap* map = new WorkletModuleResponsesMap;
const KURL kUrl(kParsedURLString, "https://example.com/foo.js");
// An initial read call creates a placeholder entry and asks the client to
// fetch a module script.
ClientImpl* client1 = new ClientImpl;
map->ReadOrCreateEntry(kUrl, client1);
EXPECT_EQ(ClientImpl::Result::kNeedsFetching, client1->GetResult());
EXPECT_FALSE(client1->GetParams().has_value());
// The entry is now being fetched. Following read calls should wait for the
// completion.
ClientImpl* client2 = new ClientImpl;
map->ReadOrCreateEntry(kUrl, client2);
EXPECT_EQ(ClientImpl::Result::kInitial, client2->GetResult());
ClientImpl* client3 = new ClientImpl;
map->ReadOrCreateEntry(kUrl, client3);
EXPECT_EQ(ClientImpl::Result::kInitial, client3->GetResult());
// An invalidation call should notify the waiting clients.
map->InvalidateEntry(kUrl);
EXPECT_EQ(ClientImpl::Result::kFailed, client2->GetResult());
EXPECT_FALSE(client2->GetParams().has_value());
EXPECT_EQ(ClientImpl::Result::kFailed, client3->GetResult());
EXPECT_FALSE(client3->GetParams().has_value());
}
TEST(WorkletModuleResponsesMapTest, Isolation) {
WorkletModuleResponsesMap* map = new WorkletModuleResponsesMap;
const KURL kUrl1(kParsedURLString, "https://example.com/foo.js");
const KURL kUrl2(kParsedURLString, "https://example.com/bar.js");
// An initial read call for |kUrl1| creates a placeholder entry and asks the
// client to fetch a module script.
ClientImpl* client1 = new ClientImpl;
map->ReadOrCreateEntry(kUrl1, client1);
EXPECT_EQ(ClientImpl::Result::kNeedsFetching, client1->GetResult());
EXPECT_FALSE(client1->GetParams().has_value());
// The entry is now being fetched. Following read calls for |kUrl1| should
// wait for the completion.
ClientImpl* client2 = new ClientImpl;
map->ReadOrCreateEntry(kUrl1, client2);
EXPECT_EQ(ClientImpl::Result::kInitial, client2->GetResult());
// An initial read call for |kUrl2| also creates a placeholder entry and asks
// the client to fetch a module script.
ClientImpl* client3 = new ClientImpl;
map->ReadOrCreateEntry(kUrl2, client3);
EXPECT_EQ(ClientImpl::Result::kNeedsFetching, client3->GetResult());
EXPECT_FALSE(client3->GetParams().has_value());
// The entry is now being fetched. Following read calls for |kUrl2| should
// wait for the completion.
ClientImpl* client4 = new ClientImpl;
map->ReadOrCreateEntry(kUrl2, client4);
EXPECT_EQ(ClientImpl::Result::kInitial, client4->GetResult());
// The read call for |kUrl2| should not affect the other entry for |kUrl1|.
EXPECT_EQ(ClientImpl::Result::kInitial, client2->GetResult());
// An update call for |kUrl2| should notify the waiting clients for |kUrl2|.
ModuleScriptCreationParams params(kUrl2, "// dummy script",
WebURLRequest::kFetchCredentialsModeOmit,
kNotSharableCrossOrigin);
map->UpdateEntry(kUrl2, params);
EXPECT_EQ(ClientImpl::Result::kOK, client4->GetResult());
EXPECT_TRUE(client4->GetParams().has_value());
// ... but should not notify the waiting clients for |kUrl1|.
EXPECT_EQ(ClientImpl::Result::kInitial, client2->GetResult());
}
} // namespace blink
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment