blob: 420f21b1b207ab4ac37c0b6035fbc4d118a4006c [file] [log] [blame]
/*
* Copyright 2025 Google LLC
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "include/core/SkExecutor.h"
#include "src/base/SkSpinlock.h"
#include "src/core/SkTaskGroup.h"
#include "tests/Test.h"
#include <thread>
namespace {
constexpr int kNumWorkLists = 2;
constexpr int kNumThreads = 4;
constexpr int kHighPriority = 0;
constexpr int kLowPriority = 1;
// This utility just collects a set kMaxCount ints in a thread-safe manner. It has a
// predicate to check if the results match expectations.
class Collector {
public:
static constexpr int kMaxCount = 200;
void insert(int i) SK_EXCLUDES(fSpinLock) {
SkAutoSpinlock lock{fSpinLock};
if (fCount >= kMaxCount) {
return;
}
fData[fCount++] = i;
}
int count() const SK_EXCLUDES(fSpinLock) {
SkAutoSpinlock lock{fSpinLock};
return fCount;
}
#if defined(SK_DEBUG)
void dump() const SK_EXCLUDES(fSpinLock) {
SkAutoSpinlock lock{fSpinLock};
for (int i = 0; i < fCount; ++i) {
SkDebugf("%d ", fData[i]);
}
SkDebugf("\n");
}
#endif
// For this following tests we expect that all the high priority work is completed
// before the low priority work. The length of the two task types is set up so,
// even if low priority work is picked up while the high priority tasks are finishing,
// all the high priority task should still finish before the low priority ones.
bool dataIsInOrder() const SK_EXCLUDES(fSpinLock) {
SkAutoSpinlock lock{fSpinLock};
for (int i = 0; i < fCount; ++i) {
if (i < 100) {
if (fData[i] >= 100) { return false; }
} else {
if (fData[i] < 100) { return false; }
}
}
return true;
}
private:
mutable SkSpinlock fSpinLock;
int fCount SK_GUARDED_BY(fSpinLock) = 0;
int fData[kMaxCount] SK_GUARDED_BY(fSpinLock);
};
// Make sure all high priority work is started before the low priority work is begun
void priority_test(skiatest::Reporter* reporter, std::unique_ptr<SkExecutor> executor) {
SkTaskGroup taskGroup(*executor);
Collector collector;
for (int i = 0; i < 100; ++i) {
taskGroup.add([&collector, i]() {
collector.insert(i);
std::this_thread::sleep_for(std::chrono::microseconds(50));
}, kHighPriority);
}
for (int i = 100; i < 200; ++i) {
taskGroup.add([&collector, i]() {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
collector.insert(i);
}, kLowPriority);
}
taskGroup.wait();
REPORTER_ASSERT(reporter, collector.count() == Collector::kMaxCount);
REPORTER_ASSERT(reporter, collector.dataIsInOrder());
}
// Test out discarding
void discard_test(skiatest::Reporter* reporter, std::unique_ptr<SkExecutor> executor) {
SkTaskGroup taskGroup(*executor);
Collector collector;
for (int i = 0; i < 100; ++i) {
taskGroup.add([&collector, i]() {
collector.insert(i);
std::this_thread::sleep_for(std::chrono::microseconds(50));
}, kHighPriority);
}
for (int i = 100; i < 200; ++i) {
taskGroup.add([&collector, i]() {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
collector.insert(i);
}, kLowPriority);
}
std::this_thread::sleep_for(std::chrono::milliseconds(3));
taskGroup.discardAllPendingWork();
taskGroup.wait();
// We should've shaved off some work
REPORTER_ASSERT(reporter, collector.count() < Collector::kMaxCount);
// But, the work that got done should still be in order
REPORTER_ASSERT(reporter, collector.dataIsInOrder());
}
std::unique_ptr<SkExecutor> make_2x_FIFO() {
return SkExecutor::MakeMultiListFIFOThreadPool(
kNumWorkLists, kNumThreads, /* allowBorrowing= */ false);
}
std::unique_ptr<SkExecutor> make_2x_LIFO() {
return SkExecutor::MakeMultiListLIFOThreadPool(
kNumWorkLists, kNumThreads, /* allowBorrowing= */ false);
}
std::unique_ptr<SkExecutor> make_1x_FIFO() {
return SkExecutor::MakeFIFOThreadPool(kNumThreads, /* allowBorrowing= */ false);
}
} // anonymous namespace
DEF_TEST(ExecutorTest, reporter) {
// 1x_LIFO is incompatible w/ how this test is structured
for (auto makeExecutor : { make_2x_FIFO, make_2x_LIFO, make_1x_FIFO }) {
priority_test(reporter, makeExecutor());
discard_test(reporter, makeExecutor());
}
}