From d0d7f6ac082184ee88bb34224f41537c140d98e5 Mon Sep 17 00:00:00 2001 From: Riley Andrews Date: Thu, 5 Jun 2014 19:34:40 -0700 Subject: [PATCH] Add gtest conformance tests for libsync. Not complete yet, but substantially more comprehensive than the interactive test that was there before. (cherry-picked from internal master, same change-id). Change-Id: I9019b0a8babbc91f78aa850e0e288bbf05f93500 --- libsync/tests/Android.mk | 31 ++ libsync/tests/sync_test.cpp | 615 ++++++++++++++++++++++++++++++++++++ 2 files changed, 646 insertions(+) create mode 100644 libsync/tests/Android.mk create mode 100644 libsync/tests/sync_test.cpp diff --git a/libsync/tests/Android.mk b/libsync/tests/Android.mk new file mode 100644 index 000000000..ad20e50a1 --- /dev/null +++ b/libsync/tests/Android.mk @@ -0,0 +1,31 @@ +# +# Copyright 2014 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) +include external/libcxx/libcxx.mk +LOCAL_CLANG := true +LOCAL_MODULE := sync-unit-tests +LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk +LOCAL_CFLAGS += -g -Wall -Werror -std=gnu++11 -Wno-missing-field-initializers -Wno-sign-compare +LOCAL_SHARED_LIBRARIES += libsync +LOCAL_STATIC_LIBRARIES += libgtest_main +LOCAL_C_INCLUDES += $(LOCAL_PATH)/../include +LOCAL_C_INCLUDES += $(LOCAL_PATH)/.. +LOCAL_SRC_FILES := \ + sync_test.cpp +include $(BUILD_NATIVE_TEST) diff --git a/libsync/tests/sync_test.cpp b/libsync/tests/sync_test.cpp new file mode 100644 index 000000000..55cd68773 --- /dev/null +++ b/libsync/tests/sync_test.cpp @@ -0,0 +1,615 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// TODO: better stress tests? +// Handle more than 64 fd's simultaneously, i.e. fix sync_fence_info's 4k limit. +// Handle wraparound in timelines like nvidia. + +using namespace std; + +namespace { + +// C++ wrapper class for sync timeline. +class SyncTimeline { + int m_fd = -1; + bool m_fdInitialized = false; +public: + SyncTimeline(const SyncTimeline &) = delete; + SyncTimeline& operator=(SyncTimeline&) = delete; + SyncTimeline() noexcept { + int fd = sw_sync_timeline_create(); + if (fd == -1) + return; + m_fdInitialized = true; + m_fd = fd; + } + void destroy() { + if (m_fdInitialized) { + close(m_fd); + m_fd = -1; + m_fdInitialized = false; + } + } + ~SyncTimeline() { + destroy(); + } + bool isValid() const { + if (m_fdInitialized) { + int status = fcntl(m_fd, F_GETFD, 0); + if (status == 0) + return true; + else + return false; + } + else { + return false; + } + } + int getFd() const { + return m_fd; + } + int inc(int val = 1) { + return sw_sync_timeline_inc(m_fd, val); + } +}; + +struct SyncPointInfo { + std::string driverName; + std::string objectName; + uint64_t timeStampNs; + int status; // 1 sig, 0 active, neg is err +}; + +// Wrapper class for sync fence. +class SyncFence { + int m_fd = -1; + bool m_fdInitialized = false; + static int s_fenceCount; + + void setFd(int fd) { + m_fd = fd; + m_fdInitialized = true; + } + void clearFd() { + m_fd = -1; + m_fdInitialized = false; + } +public: + bool isValid() const { + if (m_fdInitialized) { + int status = fcntl(m_fd, F_GETFD, 0); + if (status == 0) + return true; + else + return false; + } + else { + return false; + } + } + SyncFence& operator=(SyncFence &&rhs) noexcept { + destroy(); + if (rhs.isValid()) { + setFd(rhs.getFd()); + rhs.clearFd(); + } + return *this; + } + SyncFence(SyncFence &&fence) noexcept { + if (fence.isValid()) { + setFd(fence.getFd()); + fence.clearFd(); + } + } + SyncFence(const SyncFence &fence) noexcept { + // This is ok, as sync fences are immutable after construction, so a dup + // is basically the same thing as a copy. + if (fence.isValid()) { + int fd = dup(fence.getFd()); + if (fd == -1) + return; + setFd(fd); + } + } + SyncFence(const SyncTimeline &timeline, + int value, + const char *name = nullptr) noexcept { + std::string autoName = "allocFence"; + autoName += s_fenceCount; + s_fenceCount++; + int fd = sw_sync_fence_create(timeline.getFd(), name ? name : autoName.c_str(), value); + if (fd == -1) + return; + setFd(fd); + } + SyncFence(const SyncFence &a, const SyncFence &b, const char *name = nullptr) noexcept { + std::string autoName = "mergeFence"; + autoName += s_fenceCount; + s_fenceCount++; + int fd = sync_merge(name ? name : autoName.c_str(), a.getFd(), b.getFd()); + if (fd == -1) + return; + setFd(fd); + } + SyncFence(const vector &sources) noexcept { + assert(sources.size()); + SyncFence temp(*begin(sources)); + for (auto itr = ++begin(sources); itr != end(sources); ++itr) { + temp = SyncFence(*itr, temp); + } + if (temp.isValid()) { + setFd(temp.getFd()); + temp.clearFd(); + } + } + void destroy() { + if (isValid()) { + close(m_fd); + clearFd(); + } + } + ~SyncFence() { + destroy(); + } + int getFd() const { + return m_fd; + } + int wait(int timeout = -1) { + return sync_wait(m_fd, timeout); + } + vector getInfo() const { + struct sync_pt_info *pointInfo = nullptr; + vector fenceInfo; + sync_fence_info_data *info = sync_fence_info(getFd()); + if (!info) { + return fenceInfo; + } + while ((pointInfo = sync_pt_info(info, pointInfo))) { + fenceInfo.push_back(SyncPointInfo{ + pointInfo->driver_name, + pointInfo->obj_name, + pointInfo->timestamp_ns, + pointInfo->status}); + } + sync_fence_info_free(info); + return fenceInfo; + } + int getSize() const { + return getInfo().size(); + } + int getSignaledCount() const { + return countWithStatus(1); + } + int getActiveCount() const { + return countWithStatus(0); + } + int getErrorCount() const { + return countWithStatus(-1); + } +private: + int countWithStatus(int status) const { + int count = 0; + for (auto &info : getInfo()) { + if (info.status == status) { + count++; + } + } + return count; + } +}; + +int SyncFence::s_fenceCount = 0; + +TEST(AllocTest, Timeline) { + SyncTimeline timeline; + ASSERT_TRUE(timeline.isValid()); +} + +TEST(AllocTest, Fence) { + SyncTimeline timeline; + ASSERT_TRUE(timeline.isValid()); + + SyncFence fence(timeline, 1); + ASSERT_TRUE(fence.isValid()); +} + +TEST(AllocTest, FenceNegative) { + int timeline = sw_sync_timeline_create(); + ASSERT_GT(timeline, 0); + + // bad fd. + ASSERT_LT(sw_sync_fence_create(-1, "fence", 1), 0); + + // No name - segfaults in user space. + // Maybe we should be friendlier here? + /* + ASSERT_LT(sw_sync_fence_create(timeline, nullptr, 1), 0); + */ + close(timeline); +} + +TEST(FenceTest, OneTimelineWait) { + SyncTimeline timeline; + ASSERT_TRUE(timeline.isValid()); + + SyncFence fence(timeline, 5); + ASSERT_TRUE(fence.isValid()); + + // Wait on fence until timeout. + ASSERT_EQ(fence.wait(0), -1); + ASSERT_EQ(errno, ETIME); + + // Advance timeline from 0 -> 1 + ASSERT_EQ(timeline.inc(1), 0); + + // Wait on fence until timeout. + ASSERT_EQ(fence.wait(0), -1); + ASSERT_EQ(errno, ETIME); + + // Signal the fence. + ASSERT_EQ(timeline.inc(4), 0); + + // Wait successfully. + ASSERT_EQ(fence.wait(0), 0); + + // Go even futher, and confirm wait still succeeds. + ASSERT_EQ(timeline.inc(10), 0); + ASSERT_EQ(fence.wait(0), 0); +} + +TEST(FenceTest, OneTimelinePoll) { + SyncTimeline timeline; + ASSERT_TRUE(timeline.isValid()); + + SyncFence fence(timeline, 100); + ASSERT_TRUE(fence.isValid()); + + fd_set set; + FD_ZERO(&set); + FD_SET(fence.getFd(), &set); + + // Poll the fence, and wait till timeout. + timeval time = {0}; + ASSERT_EQ(select(fence.getFd() + 1, &set, nullptr, nullptr, &time), 0); + + // Advance the timeline. + timeline.inc(100); + timeline.inc(100); + + // Select should return that the fd is read for reading. + FD_ZERO(&set); + FD_SET(fence.getFd(), &set); + + ASSERT_EQ(select(fence.getFd() + 1, &set, nullptr, nullptr, &time), 1); + ASSERT_TRUE(FD_ISSET(fence.getFd(), &set)); +} + +TEST(FenceTest, OneTimelineMerge) { + SyncTimeline timeline; + ASSERT_TRUE(timeline.isValid()); + + // create fence a,b,c and then merge them all into fence d. + SyncFence a(timeline, 1), b(timeline, 2), c(timeline, 3); + ASSERT_TRUE(a.isValid()); + ASSERT_TRUE(b.isValid()); + ASSERT_TRUE(c.isValid()); + + SyncFence d({a,b,c}); + ASSERT_TRUE(d.isValid()); + + // confirm all fences have one active point (even d). + ASSERT_EQ(a.getActiveCount(), 1); + ASSERT_EQ(b.getActiveCount(), 1); + ASSERT_EQ(c.getActiveCount(), 1); + ASSERT_EQ(d.getActiveCount(), 1); + + // confirm that d is not signaled until the max of a,b,c + timeline.inc(1); + ASSERT_EQ(a.getSignaledCount(), 1); + ASSERT_EQ(d.getActiveCount(), 1); + + timeline.inc(1); + ASSERT_EQ(b.getSignaledCount(), 1); + ASSERT_EQ(d.getActiveCount(), 1); + + timeline.inc(1); + ASSERT_EQ(c.getSignaledCount(), 1); + ASSERT_EQ(d.getActiveCount(), 0); + ASSERT_EQ(d.getSignaledCount(), 1); +} + +TEST(FenceTest, MergeSameFence) { + SyncTimeline timeline; + ASSERT_TRUE(timeline.isValid()); + + SyncFence fence(timeline, 5); + ASSERT_TRUE(fence.isValid()); + + SyncFence selfMergeFence(fence, fence); + ASSERT_TRUE(selfMergeFence.isValid()); + + ASSERT_EQ(selfMergeFence.getSignaledCount(), 0); + + timeline.inc(5); + ASSERT_EQ(selfMergeFence.getSignaledCount(), 1); +} + +TEST(FenceTest, WaitOnDestroyedTimeline) { + SyncTimeline timeline; + ASSERT_TRUE(timeline.isValid()); + + SyncFence fenceSig(timeline, 100); + SyncFence fenceKill(timeline, 200); + + // Spawn a thread to wait on a fence when the timeline is killed. + thread waitThread{ + [&]() { + ASSERT_EQ(timeline.inc(100), 0); + + ASSERT_EQ(fenceKill.wait(-1), -1); + ASSERT_EQ(errno, ENOENT); + } + }; + + // Wait for the thread to spool up. + fenceSig.wait(); + + // Kill the timeline. + timeline.destroy(); + + // wait for the thread to clean up. + waitThread.join(); +} + +TEST(FenceTest, PollOnDestroyedTimeline) { + SyncTimeline timeline; + ASSERT_TRUE(timeline.isValid()); + + SyncFence fenceSig(timeline, 100); + SyncFence fenceKill(timeline, 200); + + // Spawn a thread to wait on a fence when the timeline is killed. + thread waitThread{ + [&]() { + ASSERT_EQ(timeline.inc(100), 0); + + // Wait on the fd. + struct pollfd fds; + fds.fd = fenceKill.getFd(); + fds.events = POLLIN | POLLERR; + ASSERT_EQ(poll(&fds, 1, -1), 1); + ASSERT_TRUE(fds.revents & POLLERR); + } + }; + + // Wait for the thread to spool up. + fenceSig.wait(); + + // Kill the timeline. + timeline.destroy(); + + // wait for the thread to clean up. + waitThread.join(); +} + +TEST(FenceTest, MultiTimelineWait) { + SyncTimeline timelineA, timelineB, timelineC; + + SyncFence fenceA(timelineA, 5); + SyncFence fenceB(timelineB, 5); + SyncFence fenceC(timelineC, 5); + + // Make a larger fence using 3 other fences from different timelines. + SyncFence mergedFence({fenceA, fenceB, fenceC}); + ASSERT_TRUE(mergedFence.isValid()); + + // Confirm fence isn't signaled + ASSERT_EQ(mergedFence.getActiveCount(), 3); + ASSERT_EQ(mergedFence.wait(0), -1); + ASSERT_EQ(errno, ETIME); + + timelineA.inc(5); + ASSERT_EQ(mergedFence.getActiveCount(), 2); + ASSERT_EQ(mergedFence.getSignaledCount(), 1); + + timelineB.inc(5); + ASSERT_EQ(mergedFence.getActiveCount(), 1); + ASSERT_EQ(mergedFence.getSignaledCount(), 2); + + timelineC.inc(5); + ASSERT_EQ(mergedFence.getActiveCount(), 0); + ASSERT_EQ(mergedFence.getSignaledCount(), 3); + + // confirm you can successfully wait. + ASSERT_EQ(mergedFence.wait(100), 0); +} + +TEST(StressTest, TwoThreadsSharedTimeline) { + const int iterations = 1 << 16; + int counter = 0; + SyncTimeline timeline; + ASSERT_TRUE(timeline.isValid()); + + // Use a single timeline to synchronize two threads + // hammmering on the same counter. + auto threadMain = [&](int threadId) { + for (int i = 0; i < iterations; i++) { + SyncFence fence(timeline, i * 2 + threadId); + ASSERT_TRUE(fence.isValid()); + + // Wait on the prior thread to complete. + ASSERT_EQ(fence.wait(), 0); + + // Confirm the previous thread's writes are visible and then inc. + ASSERT_EQ(counter, i * 2 + threadId); + counter++; + + // Kick off the other thread. + ASSERT_EQ(timeline.inc(), 0); + } + }; + + thread a{threadMain, 0}; + thread b{threadMain, 1}; + a.join(); + b.join(); + + // make sure the threads did not trample on one another. + ASSERT_EQ(counter, iterations * 2); +} + +class ConsumerStressTest : public ::testing::TestWithParam {}; + +TEST_P(ConsumerStressTest, MultiProducerSingleConsumer) { + mutex lock; + int counter = 0; + int iterations = 1 << 12; + + vector producerTimelines(GetParam()); + vector threads; + SyncTimeline consumerTimeline; + + // Producer threads run this lambda. + auto threadMain = [&](int threadId) { + for (int i = 0; i < iterations; i++) { + SyncFence fence(consumerTimeline, i); + ASSERT_TRUE(fence.isValid()); + + // Wait for the consumer to finish. Use alternate + // means of waiting on the fence. + if ((iterations + threadId) % 8 != 0) { + ASSERT_EQ(fence.wait(), 0); + } + else { + while (fence.getSignaledCount() != 1) { + ASSERT_EQ(fence.getErrorCount(), 0); + } + } + + // Every producer increments the counter, the consumer checks + erases it. + lock.lock(); + counter++; + lock.unlock(); + + ASSERT_EQ(producerTimelines[threadId].inc(), 0); + } + }; + + for (int i = 0; i < GetParam(); i++) { + threads.push_back(thread{threadMain, i}); + } + + // Consumer thread runs this loop. + for (int i = 1; i <= iterations; i++) { + // Create a fence representing all producers final timelines. + vector fences; + for (auto& timeline : producerTimelines) { + fences.push_back(SyncFence(timeline, i)); + } + SyncFence mergeFence(fences); + ASSERT_TRUE(mergeFence.isValid()); + + // Make sure we see an increment from every producer thread. Vary + // the means by which we wait. + if (iterations % 8 != 0) { + ASSERT_EQ(mergeFence.wait(), 0); + } + else { + while (mergeFence.getSignaledCount() != mergeFence.getSize()) { + ASSERT_EQ(mergeFence.getErrorCount(), 0); + } + } + ASSERT_EQ(counter, GetParam()*i); + + // Release the producer threads. + ASSERT_EQ(consumerTimeline.inc(), 0); + } + + for_each(begin(threads), end(threads), [](thread& thread) { thread.join(); }); +} +INSTANTIATE_TEST_CASE_P( + ParameterizedStressTest, + ConsumerStressTest, + ::testing::Values(2,4,16)); + +class MergeStressTest : public ::testing::TestWithParam> {}; + +template using dict = unordered_map; + +TEST_P(MergeStressTest, RandomMerge) { + int timelineCount = get<0>(GetParam()); + int mergeCount = get<1>(GetParam()); + + vector timelines(timelineCount); + + default_random_engine generator; + uniform_int_distribution timelineDist(0, timelines.size()-1); + uniform_int_distribution syncPointDist(0, numeric_limits::max()); + + SyncFence fence(timelines[0], 0); + ASSERT_TRUE(fence.isValid()); + + unordered_map fenceMap; + fenceMap.insert(make_tuple(0, 0)); + + // Randomly create syncpoints out of a fixed set of timelines, and merge them together. + for (int i = 0; i < mergeCount; i++) { + + // Generate syncpoint. + int timelineOffset = timelineDist(generator); + const SyncTimeline& timeline = timelines[timelineOffset]; + int syncPoint = syncPointDist(generator); + + // Keep track of the latest syncpoint in each timeline. + auto itr = fenceMap.find(timelineOffset); + if (itr == end(fenceMap)) { + fenceMap.insert(tie(timelineOffset, syncPoint)); + } + else { + int oldSyncPoint = itr->second; + fenceMap.erase(itr); + fenceMap.insert(tie(timelineOffset, max(syncPoint, oldSyncPoint))); + } + + // Merge. + fence = SyncFence(fence, SyncFence(timeline, syncPoint)); + ASSERT_TRUE(fence.isValid()); + } + + // Confirm our map matches the fence. + ASSERT_EQ(fence.getSize(), fenceMap.size()); + + // Trigger the merged fence. + for (auto& item: fenceMap) { + ASSERT_EQ(fence.wait(0), -1); + ASSERT_EQ(errno, ETIME); + + // Increment the timeline to the last syncpoint. + timelines[item.first].inc(item.second); + } + + // Check that the fence is triggered. + ASSERT_EQ(fence.wait(0), 0); +} + +INSTANTIATE_TEST_CASE_P( + ParameterizedMergeStressTest, + MergeStressTest, + ::testing::Combine(::testing::Values(16,32), ::testing::Values(32, 1024, 1024*32))); + +} +