summaryrefslogtreecommitdiff
path: root/chromium/third_party/blink/renderer/modules/csspaint/paint_worklet_proxy_client_test.cc
blob: c1a9d85ca803e27a0b1f577b56b79d836ea4fe45 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
// Copyright 2019 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 "third_party/blink/renderer/modules/csspaint/paint_worklet_proxy_client.h"

#include <memory>
#include <utility>

#include "base/synchronization/waitable_event.h"
#include "base/test/test_simple_task_runner.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/bindings/core/v8/script_source_code.h"
#include "third_party/blink/renderer/core/css/cssom/cross_thread_style_value.h"
#include "third_party/blink/renderer/core/css/cssom/css_paint_worklet_input.h"
#include "third_party/blink/renderer/core/script/classic_script.h"
#include "third_party/blink/renderer/core/testing/core_unit_test_helper.h"
#include "third_party/blink/renderer/core/workers/worker_reporting_proxy.h"
#include "third_party/blink/renderer/modules/csspaint/paint_worklet.h"
#include "third_party/blink/renderer/modules/csspaint/paint_worklet_global_scope.h"
#include "third_party/blink/renderer/modules/worklet/worklet_thread_test_common.h"
#include "third_party/blink/renderer/platform/graphics/paint_worklet_paint_dispatcher.h"

namespace blink {

// We inject a fake task runner in multiple tests, to avoid actually posting
// tasks cross-thread whilst still being able to know if they have been posted.
class FakeTaskRunner : public base::SingleThreadTaskRunner {
 public:
  FakeTaskRunner() : task_posted_(false) {}

  bool PostNonNestableDelayedTask(const base::Location& from_here,
                                  base::OnceClosure task,
                                  base::TimeDelta delay) override {
    task_posted_ = true;
    return true;
  }
  bool PostDelayedTask(const base::Location& from_here,
                       base::OnceClosure task,
                       base::TimeDelta delay) override {
    task_posted_ = true;
    return true;
  }
  bool RunsTasksInCurrentSequence() const override { return true; }

  bool task_posted_;

 protected:
  ~FakeTaskRunner() override {}
};

class PaintWorkletProxyClientTest : public RenderingTest {
 public:
  PaintWorkletProxyClientTest() = default;

  void SetUp() override {
    RenderingTest::SetUp();
    paint_worklet_ =
        MakeGarbageCollected<PaintWorklet>(*GetFrame().DomWindow());
    dispatcher_ = std::make_unique<PaintWorkletPaintDispatcher>();
    fake_compositor_thread_runner_ = base::MakeRefCounted<FakeTaskRunner>();
    proxy_client_ = MakeGarbageCollected<PaintWorkletProxyClient>(
        1, paint_worklet_, dispatcher_->GetWeakPtr(),
        fake_compositor_thread_runner_);
    reporting_proxy_ = std::make_unique<WorkerReportingProxy>();
  }

  void AddGlobalScopeOnWorkletThread(WorkerThread* worker_thread,
                                     PaintWorkletProxyClient* proxy_client,
                                     base::WaitableEvent* waitable_event) {
    // The natural flow for PaintWorkletGlobalScope is to be registered with the
    // proxy client during its first registerPaint call. Rather than circumvent
    // this with a specialised AddGlobalScopeForTesting method, we just use the
    // standard flow.
    ClassicScript::CreateUnspecifiedScript(
        ScriptSourceCode(
            "registerPaint('add_global_scope', class { paint() { } });"))
        ->RunScriptOnWorkerOrWorklet(*worker_thread->GlobalScope());
    waitable_event->Signal();
  }

  using TestCallback = void (*)(WorkerThread*,
                                PaintWorkletProxyClient*,
                                base::WaitableEvent*);

  void RunMultipleGlobalScopeTestsOnWorklet(TestCallback callback) {
    // PaintWorklet is stateless, and this is enforced via having multiple
    // global scopes (which are switched between). To mimic the real world,
    // create multiple WorkerThread for this. Note that the underlying thread
    // may be shared even though they are unique WorkerThread instances!
    Vector<std::unique_ptr<WorkerThread>> worklet_threads;
    for (wtf_size_t i = 0; i < PaintWorklet::kNumGlobalScopesPerThread; i++) {
      worklet_threads.push_back(CreateThreadAndProvidePaintWorkletProxyClient(
          &GetDocument(), reporting_proxy_.get(), proxy_client_));
    }

    // Add the global scopes. This must happen on the worklet thread.
    for (wtf_size_t i = 0; i < PaintWorklet::kNumGlobalScopesPerThread; i++) {
      base::WaitableEvent waitable_event;
      PostCrossThreadTask(
          *worklet_threads[i]->GetTaskRunner(TaskType::kInternalTest),
          FROM_HERE,
          CrossThreadBindOnce(
              &PaintWorkletProxyClientTest::AddGlobalScopeOnWorkletThread,
              CrossThreadUnretained(this),
              CrossThreadUnretained(worklet_threads[i].get()),
              CrossThreadPersistent<PaintWorkletProxyClient>(proxy_client_),
              CrossThreadUnretained(&waitable_event)));
      waitable_event.Wait();
    }

    // Now let the test actually run. We only run the test on the first worklet
    // thread currently; this suffices since they share the proxy.
    base::WaitableEvent waitable_event;
    PostCrossThreadTask(
        *worklet_threads[0]->GetTaskRunner(TaskType::kInternalTest), FROM_HERE,
        CrossThreadBindOnce(
            callback, CrossThreadUnretained(worklet_threads[0].get()),
            CrossThreadPersistent<PaintWorkletProxyClient>(proxy_client_),
            CrossThreadUnretained(&waitable_event)));
    waitable_event.Wait();

    // And finally clean up.
    for (wtf_size_t i = 0; i < PaintWorklet::kNumGlobalScopesPerThread; i++) {
      worklet_threads[i]->Terminate();
      worklet_threads[i]->WaitForShutdownForTesting();
    }
  }

  std::unique_ptr<PaintWorkletPaintDispatcher> dispatcher_;
  Persistent<PaintWorklet> paint_worklet_;
  scoped_refptr<FakeTaskRunner> fake_compositor_thread_runner_;
  Persistent<PaintWorkletProxyClient> proxy_client_;
  std::unique_ptr<WorkerReportingProxy> reporting_proxy_;
};

TEST_F(PaintWorkletProxyClientTest, PaintWorkletProxyClientConstruction) {
  PaintWorkletProxyClient* proxy_client =
      MakeGarbageCollected<PaintWorkletProxyClient>(1, nullptr, nullptr,
                                                    nullptr);
  EXPECT_EQ(proxy_client->worklet_id_, 1);
  EXPECT_EQ(proxy_client->paint_dispatcher_, nullptr);

  auto dispatcher = std::make_unique<PaintWorkletPaintDispatcher>();

  proxy_client = MakeGarbageCollected<PaintWorkletProxyClient>(
      1, nullptr, dispatcher->GetWeakPtr(), nullptr);
  EXPECT_EQ(proxy_client->worklet_id_, 1);
  EXPECT_NE(proxy_client->paint_dispatcher_, nullptr);
}

void RunAddGlobalScopesTestOnWorklet(
    WorkerThread* thread,
    PaintWorkletProxyClient* proxy_client,
    scoped_refptr<FakeTaskRunner> compositor_task_runner,
    base::WaitableEvent* waitable_event) {
  // For this test, we cheat and reuse the same global scope object from a
  // single WorkerThread. In real code these would be different global scopes.

  // First, add all but one of the global scopes. The proxy client should not
  // yet register itself.
  for (size_t i = 0; i < PaintWorklet::kNumGlobalScopesPerThread - 1; i++) {
    proxy_client->AddGlobalScope(To<WorkletGlobalScope>(thread->GlobalScope()));
  }

  EXPECT_EQ(proxy_client->GetGlobalScopesForTesting().size(),
            PaintWorklet::kNumGlobalScopesPerThread - 1);
  EXPECT_FALSE(compositor_task_runner->task_posted_);

  // Now add the final global scope. This should trigger the registration.
  proxy_client->AddGlobalScope(To<WorkletGlobalScope>(thread->GlobalScope()));
  EXPECT_EQ(proxy_client->GetGlobalScopesForTesting().size(),
            PaintWorklet::kNumGlobalScopesPerThread);
  EXPECT_TRUE(compositor_task_runner->task_posted_);

  waitable_event->Signal();
}

TEST_F(PaintWorkletProxyClientTest, AddGlobalScopes) {
  ScopedOffMainThreadCSSPaintForTest off_main_thread_css_paint(true);
  // Global scopes must be created on worker threads.
  std::unique_ptr<WorkerThread> worklet_thread =
      CreateThreadAndProvidePaintWorkletProxyClient(
          &GetDocument(), reporting_proxy_.get(), proxy_client_);

  EXPECT_TRUE(proxy_client_->GetGlobalScopesForTesting().IsEmpty());

  base::WaitableEvent waitable_event;
  PostCrossThreadTask(
      *worklet_thread->GetTaskRunner(TaskType::kInternalTest), FROM_HERE,
      CrossThreadBindOnce(
          &RunAddGlobalScopesTestOnWorklet,
          CrossThreadUnretained(worklet_thread.get()),
          CrossThreadPersistent<PaintWorkletProxyClient>(proxy_client_),
          fake_compositor_thread_runner_,
          CrossThreadUnretained(&waitable_event)));
  waitable_event.Wait();

  worklet_thread->Terminate();
  worklet_thread->WaitForShutdownForTesting();
}

void RunPaintTestOnWorklet(WorkerThread* thread,
                           PaintWorkletProxyClient* proxy_client,
                           base::WaitableEvent* waitable_event) {
  // Assert that all global scopes have been registered. Note that we don't
  // use ASSERT_EQ here as that would crash the worklet thread and the test
  // would timeout rather than fail.
  EXPECT_EQ(proxy_client->GetGlobalScopesForTesting().size(),
            PaintWorklet::kNumGlobalScopesPerThread);

  // Register the painter on all global scopes.
  for (const auto& global_scope : proxy_client->GetGlobalScopesForTesting()) {
    ClassicScript::CreateUnspecifiedScript(
        ScriptSourceCode("registerPaint('foo', class { paint() { } });"))
        ->RunScriptOnWorkerOrWorklet(*global_scope);
  }

  PaintWorkletStylePropertyMap::CrossThreadData data;
  Vector<std::unique_ptr<CrossThreadStyleValue>> input_arguments;
  std::vector<cc::PaintWorkletInput::PropertyKey> property_keys;
  scoped_refptr<CSSPaintWorkletInput> input =
      base::MakeRefCounted<CSSPaintWorkletInput>(
          "foo", FloatSize(100, 100), 1.0f, 1.0f, 1, std::move(data),
          std::move(input_arguments), std::move(property_keys));
  sk_sp<PaintRecord> record = proxy_client->Paint(input.get(), {});
  EXPECT_NE(record, nullptr);

  waitable_event->Signal();
}

TEST_F(PaintWorkletProxyClientTest, Paint) {
  ScopedOffMainThreadCSSPaintForTest off_main_thread_css_paint(true);
  RunMultipleGlobalScopeTestsOnWorklet(&RunPaintTestOnWorklet);
}

void RunDefinitionsMustBeCompatibleTestOnWorklet(
    WorkerThread* thread,
    PaintWorkletProxyClient* proxy_client,
    base::WaitableEvent* waitable_event) {
  // Assert that all global scopes have been registered. Note that we don't
  // use ASSERT_EQ here as that would crash the worklet thread and the test
  // would timeout rather than fail.
  EXPECT_EQ(proxy_client->GetGlobalScopesForTesting().size(),
            PaintWorklet::kNumGlobalScopesPerThread);

  // This test doesn't make sense if there's only one global scope!
  EXPECT_GT(PaintWorklet::kNumGlobalScopesPerThread, 1u);

  const Vector<CrossThreadPersistent<PaintWorkletGlobalScope>>& global_scopes =
      proxy_client->GetGlobalScopesForTesting();

  // Things that can be different: alpha different, native properties
  // different, custom properties different, input type args different.
  const HashMap<String, std::unique_ptr<DocumentPaintDefinition>>&
      document_definition_map = proxy_client->DocumentDefinitionMapForTesting();

  // Differing native properties.
  ClassicScript::CreateUnspecifiedScript(
      ScriptSourceCode(R"JS(registerPaint('test1', class {
        static get inputProperties() { return ['border-image', 'color']; }
        paint() { }
      });)JS"))
      ->RunScriptOnWorkerOrWorklet(*global_scopes[0]);
  EXPECT_NE(document_definition_map.at("test1"), nullptr);
  ClassicScript::CreateUnspecifiedScript(
      ScriptSourceCode(R"JS(registerPaint('test1', class {
        static get inputProperties() { return ['left']; }
        paint() { }
      });)JS"))
      ->RunScriptOnWorkerOrWorklet(*global_scopes[1]);
  EXPECT_EQ(document_definition_map.at("test1"), nullptr);

  // Differing custom properties.
  ClassicScript::CreateUnspecifiedScript(
      ScriptSourceCode(R"JS(registerPaint('test2', class {
        static get inputProperties() { return ['--foo', '--bar']; }
        paint() { }
      });)JS"))
      ->RunScriptOnWorkerOrWorklet(*global_scopes[0]);
  EXPECT_NE(document_definition_map.at("test2"), nullptr);
  ClassicScript::CreateUnspecifiedScript(
      ScriptSourceCode(R"JS(registerPaint('test2', class {
        static get inputProperties() { return ['--zoinks']; }
        paint() { }
      });)JS"))
      ->RunScriptOnWorkerOrWorklet(*global_scopes[1]);
  EXPECT_EQ(document_definition_map.at("test2"), nullptr);

  // Differing alpha values. The default is 'true'.
  ClassicScript::CreateUnspecifiedScript(
      ScriptSourceCode("registerPaint('test3', class { paint() { } });"))
      ->RunScriptOnWorkerOrWorklet(*global_scopes[0]);
  EXPECT_NE(document_definition_map.at("test3"), nullptr);
  ClassicScript::CreateUnspecifiedScript(
      ScriptSourceCode(R"JS(registerPaint('test3', class {
        static get contextOptions() { return {alpha: false}; }
        paint() { }
      });)JS"))
      ->RunScriptOnWorkerOrWorklet(*global_scopes[1]);
  EXPECT_EQ(document_definition_map.at("test3"), nullptr);

  waitable_event->Signal();
}

TEST_F(PaintWorkletProxyClientTest, DefinitionsMustBeCompatible) {
  ScopedOffMainThreadCSSPaintForTest off_main_thread_css_paint(true);
  RunMultipleGlobalScopeTestsOnWorklet(
      &RunDefinitionsMustBeCompatibleTestOnWorklet);
}

namespace {
// Calling registerPaint can cause the PaintWorkletProxyClient to post back from
// the worklet thread to the main thread. This is safe in the general case,
// since the task will just queue up to run after the test has finished, but
// the following tests want to know whether or not the task has posted; this
// class provides that information.
class ScopedFakeMainThreadTaskRunner {
 public:
  ScopedFakeMainThreadTaskRunner(PaintWorkletProxyClient* proxy_client)
      : proxy_client_(proxy_client), fake_task_runner_(new FakeTaskRunner) {
    original_task_runner_ = proxy_client->MainThreadTaskRunnerForTesting();
    proxy_client_->SetMainThreadTaskRunnerForTesting(fake_task_runner_);
  }

  ~ScopedFakeMainThreadTaskRunner() {
    proxy_client_->SetMainThreadTaskRunnerForTesting(original_task_runner_);
  }

  void ResetTaskHasBeenPosted() { fake_task_runner_->task_posted_ = false; }
  bool TaskHasBeenPosted() const { return fake_task_runner_->task_posted_; }

 private:
  // The PaintWorkletProxyClient is held on the main test thread, but we are
  // constructed on the worklet thread so we have to hold the client reference
  // in a CrossThreadPersistent.
  CrossThreadPersistent<PaintWorkletProxyClient> proxy_client_;
  scoped_refptr<FakeTaskRunner> fake_task_runner_;
  scoped_refptr<base::SingleThreadTaskRunner> original_task_runner_;
};
}  // namespace

void RunAllDefinitionsMustBeRegisteredBeforePostingTestOnWorklet(
    WorkerThread* thread,
    PaintWorkletProxyClient* proxy_client,
    base::WaitableEvent* waitable_event) {
  ScopedFakeMainThreadTaskRunner fake_runner(proxy_client);

  // Assert that all global scopes have been registered. Note that we don't
  // use ASSERT_EQ here as that would crash the worklet thread and the test
  // would timeout rather than fail.
  EXPECT_EQ(proxy_client->GetGlobalScopesForTesting().size(),
            PaintWorklet::kNumGlobalScopesPerThread);

  // Register a new paint function on all but one global scope. They should not
  // end up posting a task to the PaintWorklet.
  const Vector<CrossThreadPersistent<PaintWorkletGlobalScope>>& global_scopes =
      proxy_client->GetGlobalScopesForTesting();
  for (wtf_size_t i = 0; i < global_scopes.size() - 1; i++) {
    ClassicScript::CreateUnspecifiedScript(
        ScriptSourceCode("registerPaint('foo', class { paint() { } });"))
        ->RunScriptOnWorkerOrWorklet(*global_scopes[i]);
    EXPECT_FALSE(fake_runner.TaskHasBeenPosted());
  }

  // Now register the final one; the task should then be posted.
  ClassicScript::CreateUnspecifiedScript(
      ScriptSourceCode("registerPaint('foo', class { paint() { } });"))
      ->RunScriptOnWorkerOrWorklet(*global_scopes.back());
  EXPECT_TRUE(fake_runner.TaskHasBeenPosted());

  waitable_event->Signal();
}

TEST_F(PaintWorkletProxyClientTest,
       AllDefinitionsMustBeRegisteredBeforePosting) {
  ScopedOffMainThreadCSSPaintForTest off_main_thread_css_paint(true);
  RunMultipleGlobalScopeTestsOnWorklet(
      &RunAllDefinitionsMustBeRegisteredBeforePostingTestOnWorklet);
}

}  // namespace blink