yum-mirror/slang

Making it easier to work with shaders

git clone https://git.yummers.dev/yum-mirror/slang

Ellie HermaszewskaCorrect include dir for libslang (#5539)7b570feed

master
20.9 KiB643 linesraw
1// unit-test-persistent-cache.cpp
2#include "../../source/core/slang-file-system.h"
3#include "../../source/core/slang-io.h"
4#include "../../source/core/slang-persistent-cache.h"
5#include "../../source/core/slang-process.h"
6#include "../../source/core/slang-random-generator.h"
7#include "unit-test/slang-unit-test.h"
8
9#include <atomic>
10#include <chrono>
11#include <condition_variable>
12#include <functional>
13#include <mutex>
14#include <thread>
15
16using namespace Slang;
17
18static DefaultRandomGenerator rng(0xdeadbeef);
19
20inline ComPtr<ISlangBlob> createRandomBlob(size_t size)
21{
22    ScopedAllocation alloc;
23    alloc.allocate(size);
24    rng.nextData(alloc.getData(), size);
25    return RawBlob::moveCreate(alloc);
26}
27
28inline bool isBlobEqual(ISlangBlob* a, ISlangBlob* b)
29{
30    return a->getBufferSize() == b->getBufferSize() &&
31           ::memcmp(a->getBufferPointer(), b->getBufferPointer(), a->getBufferSize()) == 0;
32}
33
34class Barrier
35{
36public:
37    Barrier(size_t threadCount, std::function<void()> completionFunc = nullptr)
38        : m_threadCount(threadCount), m_waitCount(threadCount), m_completionFunc(completionFunc)
39    {
40    }
41
42    Barrier(const Barrier& barrier) = delete;
43    Barrier& operator=(const Barrier& barrier) = delete;
44
45    void wait()
46    {
47        std::unique_lock<std::mutex> lock(m_mutex);
48
49        auto generation = m_generation;
50
51        if (--m_waitCount == 0)
52        {
53            if (m_completionFunc)
54                m_completionFunc();
55            ++m_generation;
56            m_waitCount = m_threadCount;
57            m_condition.notify_all();
58        }
59        else
60        {
61            m_condition.wait(lock, [this, generation]() { return generation != m_generation; });
62        }
63    }
64
65private:
66    size_t m_threadCount;
67    size_t m_waitCount;
68    size_t m_generation = 0;
69    std::function<void()> m_completionFunc;
70    std::mutex m_mutex;
71    std::condition_variable m_condition;
72};
73
74namespace Slang
75{
76
77/// Helper class for performing tests on the persistent cache.
78/// This class is a friend class of PersistentCache and can access its internals.
79struct PersistentCacheTest
80{
81    ISlangMutableFileSystem* osFileSystem;
82    String cacheDirectory;
83    RefPtr<PersistentCache> cache;
84
85    PersistentCacheTest(Count maxEntryCount = 0)
86    {
87        osFileSystem = OSFileSystem::getMutableSingleton();
88        cacheDirectory = Path::simplify(
89            Path::getParentDirectory(Path::getExecutablePath()) + "/persistent-cache-test" +
90            String(Process::getId()));
91
92        removeCacheFiles();
93
94        PersistentCache::Desc desc;
95        desc.directory = cacheDirectory.getBuffer();
96        desc.maxEntryCount = maxEntryCount;
97        cache = new PersistentCache(desc);
98    }
99
100    virtual ~PersistentCacheTest()
101    {
102        cache = nullptr;
103
104        removeCacheFiles();
105    }
106
107    void removeCacheFiles()
108    {
109        // Remove all files the cache created.
110        osFileSystem->enumeratePathContents(
111            cacheDirectory.getBuffer(),
112            [](SlangPathType pathType, const char* fileName, void* userData)
113            {
114                PersistentCacheTest* self = static_cast<PersistentCacheTest*>(userData);
115                String path = self->cacheDirectory + "/" + fileName;
116                self->osFileSystem->remove(path.getBuffer());
117            },
118            this);
119
120        // Also remove the cache directory.
121        osFileSystem->remove(cacheDirectory.getBuffer());
122    }
123
124    // Entry (key, data) for testing.
125    struct Entry
126    {
127        PersistentCache::Key key;
128        ComPtr<ISlangBlob> data;
129    };
130
131    // Helper to write an entry to the cache.
132    void writeEntry(const Entry& entry)
133    {
134        SLANG_CHECK(cache->writeEntry(entry.key, entry.data) == SLANG_OK);
135    }
136
137    // Helper to read an entry from the cache and discard the data.
138    // Returns true if the entry was found, false otherwise.
139    bool readEntry(const Entry& entry)
140    {
141        ComPtr<ISlangBlob> data;
142        SlangResult result = cache->readEntry(entry.key, data.writeRef());
143        SLANG_CHECK(result == SLANG_OK || result == SLANG_E_NOT_FOUND);
144        if (result == SLANG_OK)
145        {
146            SLANG_CHECK(isBlobEqual(data, entry.data));
147        }
148        if (result == SLANG_E_NOT_FOUND)
149        {
150            SLANG_CHECK(data == nullptr);
151        }
152        return result == SLANG_OK;
153    }
154
155    // Get the absolute filename for a cache entry file.
156    String getEntryFileName(const Entry& entry) { return cache->getEntryFileName(entry.key); }
157
158    // Get the absolute filename of the cache index file.
159    String getIndexFilename() { return cache->m_indexFileName; }
160};
161
162} // namespace Slang
163
164// Performs basic tests on the cache.
165// - write/read entries
166// - check for correct cache stats
167// - clearing the cache
168// - resetting stats
169struct BasicTest : public PersistentCacheTest
170{
171    BasicTest()
172        : PersistentCacheTest()
173    {
174    }
175
176    void run()
177    {
178        // Check that cache is empty.
179        SLANG_CHECK(cache->getStats().entryCount == 0);
180        SLANG_CHECK(cache->getStats().hitCount == 0);
181        SLANG_CHECK(cache->getStats().missCount == 0);
182
183        // Setup a list of entries to store in the cache.
184        List<Entry> entries;
185        for (size_t i = 0; i < 10; ++i)
186        {
187            auto data = createRandomBlob(i * 1024);
188            auto key = SHA1::compute(data->getBufferPointer(), data->getBufferSize());
189            entries.add(Entry{key, data});
190        }
191
192        for (size_t i = 0; i < 10; ++i)
193        {
194            const auto& entry = entries[i];
195            ComPtr<ISlangBlob> data;
196
197            // Try to read an entry. Check that its not found and counts as a miss.
198            SLANG_CHECK(cache->readEntry(entry.key, data.writeRef()) == SLANG_E_NOT_FOUND);
199            SLANG_CHECK(cache->getStats().missCount == i + 1);
200
201            // Write the entry. Check that it gets added.
202            SLANG_CHECK(cache->writeEntry(entry.key, entry.data) == SLANG_OK);
203            SLANG_CHECK(cache->getStats().entryCount == i + 1);
204        }
205
206        SLANG_CHECK(cache->getStats().entryCount == 10);
207        SLANG_CHECK(cache->getStats().hitCount == 0);
208        SLANG_CHECK(cache->getStats().missCount == 10);
209
210        for (size_t i = 0; i < 10; ++i)
211        {
212            const auto& entry = entries[i];
213            ComPtr<ISlangBlob> data;
214
215            // Read entries. Check that these are cache hits and return the correct data.
216            SLANG_CHECK(cache->readEntry(entry.key, data.writeRef()) == SLANG_OK);
217            SLANG_CHECK(cache->getStats().hitCount == i + 1);
218            SLANG_CHECK(isBlobEqual(data, entry.data));
219        }
220
221        SLANG_CHECK(cache->getStats().entryCount == 10);
222        SLANG_CHECK(cache->getStats().hitCount == 10);
223        SLANG_CHECK(cache->getStats().missCount == 10);
224
225        // Clear the cache. Check that entry count is reset.
226        SLANG_CHECK(cache->clear() == SLANG_OK);
227        SLANG_CHECK(cache->getStats().entryCount == 0);
228        SLANG_CHECK(cache->getStats().hitCount == 10);
229        SLANG_CHECK(cache->getStats().missCount == 10);
230
231        // Reset stats.
232        cache->resetStats();
233        SLANG_CHECK(cache->getStats().entryCount == 0);
234        SLANG_CHECK(cache->getStats().hitCount == 0);
235        SLANG_CHECK(cache->getStats().missCount == 0);
236
237        // Check that cache is empty.
238        for (size_t i = 0; i < 10; ++i)
239        {
240            const auto& entry = entries[i];
241            ComPtr<ISlangBlob> data;
242            SLANG_CHECK(cache->readEntry(entry.key, data.writeRef()) == SLANG_E_NOT_FOUND);
243        }
244        SLANG_CHECK(cache->getStats().missCount == 10);
245    }
246};
247
248// Tests the least-recently-used cache eviction policy.
249struct EvictionTest : public PersistentCacheTest
250{
251    EvictionTest()
252        : PersistentCacheTest(3)
253    {
254    }
255
256    void run()
257    {
258        // Setup a list of entries to store in the cache.
259        List<Entry> entries;
260        for (size_t i = 0; i < 10; ++i)
261        {
262            auto data = createRandomBlob(4096);
263            auto key = SHA1::compute(data->getBufferPointer(), data->getBufferSize());
264            entries.add(Entry{key, data});
265        }
266
267        writeEntry(entries[0]);
268        writeEntry(entries[1]);
269        writeEntry(entries[2]);
270
271        SLANG_CHECK(readEntry(entries[0]) == true);
272        SLANG_CHECK(readEntry(entries[1]) == true);
273        SLANG_CHECK(readEntry(entries[2]) == true);
274
275        // Evict LRU entry 0.
276        writeEntry(entries[3]);
277        SLANG_CHECK(readEntry(entries[0]) == false);
278        SLANG_CHECK(readEntry(entries[1]) == true);
279        SLANG_CHECK(readEntry(entries[2]) == true);
280        SLANG_CHECK(readEntry(entries[3]) == true);
281
282        // Evict LRU entry 1.
283        writeEntry(entries[4]);
284        SLANG_CHECK(readEntry(entries[1]) == false);
285        SLANG_CHECK(readEntry(entries[2]) == true);
286        SLANG_CHECK(readEntry(entries[3]) == true);
287        SLANG_CHECK(readEntry(entries[4]) == true);
288
289        // Evict LRU entry 2.
290        writeEntry(entries[5]);
291        SLANG_CHECK(readEntry(entries[2]) == false);
292        SLANG_CHECK(readEntry(entries[3]) == true);
293        SLANG_CHECK(readEntry(entries[4]) == true);
294        SLANG_CHECK(readEntry(entries[5]) == true);
295
296        // Evict LRU entry 4.
297        SLANG_CHECK(readEntry(entries[3]) == true);
298        writeEntry(entries[6]);
299        SLANG_CHECK(readEntry(entries[3]) == true);
300        SLANG_CHECK(readEntry(entries[4]) == false);
301        SLANG_CHECK(readEntry(entries[5]) == true);
302        SLANG_CHECK(readEntry(entries[6]) == true);
303    }
304};
305
306
307// Tests the cache to be robust against various corruptions.
308// These can happen if the cache files are manipulated externally.
309// The cache might also be corrupted if the application is terminated while writing.
310struct CorruptionTest : public PersistentCacheTest
311{
312    List<Entry> entries;
313
314    template<typename Func>
315    void testIndexCorruption(Func func, SlangResult expectedReadResult)
316    {
317        writeEntry(entries[0]);
318        SLANG_CHECK(readEntry(entries[0]) == true);
319        func();
320        // We expect a SLANG_E_NOT_FOUND because the cache has an empty index now.
321        ComPtr<ISlangBlob> data;
322        SLANG_CHECK(cache->readEntry(entries[0].key, data.writeRef()) == expectedReadResult);
323
324        writeEntry(entries[0]);
325        SLANG_CHECK(readEntry(entries[0]) == true);
326        func();
327        writeEntry(entries[0]);
328        SLANG_CHECK(readEntry(entries[0]) == true);
329    }
330
331    void run()
332    {
333        // Setup a list of entries to store in the cache.
334        for (size_t i = 0; i < 10; ++i)
335        {
336            auto data = createRandomBlob(4096);
337            auto key = SHA1::compute(data->getBufferPointer(), data->getBufferSize());
338            entries.add(Entry{key, data});
339        }
340
341        // Test behavior when a cached entry file is removed externally before reading.
342        writeEntry(entries[0]);
343        SLANG_CHECK(readEntry(entries[0]) == true);
344        osFileSystem->remove(getEntryFileName(entries[0]).getBuffer());
345        ComPtr<ISlangBlob> data;
346        // First time we read the entry, we expect a SLANG_E_CANNOT_OPEN because the file is gone.
347        SLANG_CHECK(cache->readEntry(entries[0].key, data.writeRef()) == SLANG_E_CANNOT_OPEN);
348        // The next time we read the entry, we expect a SLANG_E_NOT_FOUND because the entry has
349        // been removed from the cache index.
350        SLANG_CHECK(cache->readEntry(entries[0].key, data.writeRef()) == SLANG_E_NOT_FOUND);
351
352        // Test behavior when a cached entry file is removed externally before writing.
353        writeEntry(entries[0]);
354        SLANG_CHECK(readEntry(entries[0]) == true);
355        osFileSystem->remove(getEntryFileName(entries[0]).getBuffer());
356        writeEntry(entries[0]);
357        SLANG_CHECK(readEntry(entries[0]) == true);
358
359        // Test behavior when the index file is removed before reading.
360        writeEntry(entries[0]);
361        SLANG_CHECK(readEntry(entries[0]) == true);
362        osFileSystem->remove(getIndexFilename().getBuffer());
363        // We expect a SLANG_E_NOT_FOUND because the cache has an empty index now.
364        SLANG_CHECK(cache->readEntry(entries[0].key, data.writeRef()) == SLANG_E_NOT_FOUND);
365
366        // Test behavior when the index file is removed before writing.
367        writeEntry(entries[0]);
368        SLANG_CHECK(readEntry(entries[0]) == true);
369        osFileSystem->remove(getIndexFilename().getBuffer());
370        writeEntry(entries[1]);
371        SLANG_CHECK(readEntry(entries[1]) == true);
372
373        // Test different corruptions of the index file.
374        testIndexCorruption(
375            [this]() { osFileSystem->remove(getIndexFilename().getBuffer()); },
376            SLANG_E_NOT_FOUND);
377
378        testIndexCorruption(
379            [this]()
380            {
381                FileStream fs;
382                fs.init(
383                    getIndexFilename(),
384                    FileMode::Open,
385                    FileAccess::ReadWrite,
386                    FileShare::ReadWrite);
387                fs.write("x", 1);
388            },
389            SLANG_E_INTERNAL_FAIL);
390
391        testIndexCorruption(
392            [this]()
393            {
394                FileStream fs;
395                fs.init(
396                    getIndexFilename(),
397                    FileMode::Open,
398                    FileAccess::ReadWrite,
399                    FileShare::ReadWrite);
400                fs.seek(SeekOrigin::Start, 4);
401                uint32_t version = 0xffffffff;
402                fs.write(&version, sizeof(version));
403            },
404            SLANG_E_INTERNAL_FAIL);
405
406        testIndexCorruption(
407            [this]()
408            {
409                FileStream fs;
410                fs.init(
411                    getIndexFilename(),
412                    FileMode::Open,
413                    FileAccess::ReadWrite,
414                    FileShare::ReadWrite);
415                fs.seek(SeekOrigin::Start, 8);
416                uint32_t count = 0x7fffffff;
417                fs.write(&count, sizeof(count));
418            },
419            SLANG_E_INTERNAL_FAIL);
420
421        testIndexCorruption(
422            [this]()
423            {
424                FileStream fs;
425                fs.init(
426                    getIndexFilename(),
427                    FileMode::Open,
428                    FileAccess::ReadWrite,
429                    FileShare::ReadWrite);
430                fs.seek(SeekOrigin::Start, 8);
431                uint32_t count = 0;
432                fs.write(&count, sizeof(count));
433            },
434            SLANG_E_INTERNAL_FAIL);
435
436        testIndexCorruption(
437            [this]()
438            {
439                FileStream fs;
440                fs.init(
441                    getIndexFilename(),
442                    FileMode::Open,
443                    FileAccess::ReadWrite,
444                    FileShare::ReadWrite);
445                fs.seek(SeekOrigin::End, 0);
446                fs.write("x", 1);
447            },
448            SLANG_E_INTERNAL_FAIL);
449    }
450};
451
452#undef ENABLE_LOGGING
453#undef ENABLE_WRITE_TEST
454
455#ifdef ENABLE_LOGGING
456#define LOG(fmt, ...)           \
457    printf(fmt, ##__VA_ARGS__); \
458    fflush(stdout);
459#else
460#define LOG(fmt, ...)
461#endif
462
463// Stress testing.
464// This test spawns a number of threads to do concurrent access to the cache.
465// For now this is fairly simple:
466// - spawn a number of threads
467// - write random entries to the cache concurrenctly (slightly oversubscribe)
468// - synchronize
469// - read entries from the cache concurretly (test that we get the expected number of hits/misses)
470// - synchronize
471// - repeat for a number of iterations
472struct StressTest : public PersistentCacheTest
473{
474    // Number of entries to write/read per iteration.
475    static const uint32_t kEntryCount = 100;
476    // Number of entries the cache is short for storing one iteration.
477    static const uint32_t kEntryShortageCount = 10;
478    // Number of parallel threads to write/read.
479    static const uint32_t kThreadCount = 4;
480    // Number of entries to write/read per thread per iteration.
481    static const uint32_t kBatchCount = kEntryCount / kThreadCount;
482    // Total number of iterations.
483    static const uint32_t kIterationCount = 4;
484
485    static_assert(kEntryCount % kThreadCount == 0, "kEntryCount must be divisible by kThreadCount");
486
487    List<Entry> entries;
488
489    std::atomic<uint32_t> iteration{0};
490    std::atomic<uint32_t> entriesWritten{0};
491    std::atomic<uint32_t> bytesWritten{0};
492    std::atomic<uint32_t> entriesRead{0};
493    std::atomic<uint32_t> bytesRead{0};
494    std::atomic<uint32_t> readSuccess{0};
495    std::thread threads[kThreadCount];
496
497    Barrier* read_barrier;
498    Barrier* write_barrier;
499
500    StressTest()
501        : PersistentCacheTest(kEntryCount - kEntryShortageCount)
502    {
503    }
504
505    void run()
506    {
507        // Setup a list of entries to store in the cache.
508        for (size_t i = 0; i < kEntryCount * 2; ++i)
509        {
510            size_t size = rng.nextInt32InRange(256, 64 * 1024);
511            auto data = createRandomBlob(size);
512            auto key = SHA1::compute(data->getBufferPointer(), data->getBufferSize());
513            entries.add(Entry{key, data});
514        }
515
516        auto startTime = std::chrono::high_resolution_clock::now();
517
518        Barrier read_barrier_(kThreadCount, []() { LOG("Read synchronized\n"); });
519        Barrier write_barrier_(
520            kThreadCount,
521            [this]()
522            {
523                LOG("Write synchronized\n");
524#ifndef ENABLE_WRITE_TEST
525                SLANG_CHECK(readSuccess == kEntryCount - kEntryShortageCount);
526                readSuccess.store(0);
527#endif
528                iteration += 1;
529            });
530
531        read_barrier = &read_barrier_;
532        write_barrier = &write_barrier_;
533
534        for (uint32_t threadIndex = 0; threadIndex < kThreadCount; ++threadIndex)
535        {
536            threads[threadIndex] = std::thread(
537                [this, threadIndex]()
538                {
539                    LOG("Thread %u: starting\n", threadIndex);
540
541                    while (true)
542                    {
543                        // Write to cache.
544                        size_t startIndex =
545                            (iteration * kEntryCount + (threadIndex * kBatchCount)) %
546                            (kEntryCount * 2);
547                        for (size_t i = 0; i < kBatchCount; ++i)
548                        {
549                            const Entry& entry = entries[startIndex + i];
550#ifdef ENABLE_WRITE_TEST
551                            osFileSystem->saveFileBlob(
552                                getEntryFileName(entry).getBuffer(),
553                                entry.data);
554#else
555                            writeEntry(entry);
556#endif
557                            entriesWritten.fetch_add(1);
558                            bytesWritten.fetch_add((uint32_t)entry.data->getBufferSize());
559                        }
560
561                        LOG("Thread %u: ended writing (iteration=%u)\n",
562                            threadIndex,
563                            iteration.load());
564
565                        // Synchronize.
566                        read_barrier->wait();
567
568                        // Read from cache.
569                        for (size_t i = 0; i < kBatchCount; ++i)
570                        {
571                            const Entry& entry = entries[startIndex + i];
572#ifndef ENABLE_WRITE_TEST
573                            if (readEntry(entry))
574                            {
575                                readSuccess.fetch_add(1);
576                                bytesRead.fetch_add((uint32_t)entry.data->getBufferSize());
577                            }
578#endif
579                            entriesRead.fetch_add(1);
580                        }
581
582                        LOG("Thread %u: ended reading (iteration=%u)\n",
583                            threadIndex,
584                            iteration.load());
585
586                        // Synchronize.
587                        write_barrier->wait();
588
589                        // Terminate.
590                        if (iteration >= kIterationCount)
591                        {
592                            LOG("Thread %u: terminates\n", threadIndex);
593                            return;
594                        }
595                    }
596                });
597        }
598
599        for (auto& thread : threads)
600        {
601            thread.join();
602        }
603
604        auto endTime = std::chrono::high_resolution_clock::now();
605        auto duration = endTime - startTime;
606        auto seconds =
607            std::chrono::duration_cast<std::chrono::milliseconds>(duration).count() / 1000.0;
608
609        LOG("Total time: %.3fs\n", seconds);
610        LOG("Total bytes written: %d\n", bytesWritten.load());
611        LOG("Write througput: %.3fMB/s\n", (bytesWritten.load() / (1024.0 * 1024.0)) / seconds);
612        LOG("Total bytes read: %d\n", bytesRead.load());
613    }
614};
615
616SLANG_UNIT_TEST(persistentCacheBasic)
617{
618    BasicTest test;
619    test.run();
620}
621
622SLANG_UNIT_TEST(persistentCacheEviction)
623{
624    EvictionTest test;
625    test.run();
626}
627
628SLANG_UNIT_TEST(persistentCacheCorruption)
629{
630    CorruptionTest test;
631    test.run();
632}
633
634SLANG_UNIT_TEST(persistentCacheStress)
635{
636    // aarch64 builds currently fail to run multi-threaded tests within the test-server.
637    // Tests work fine without the test-server, which is puzzling. For now we disable them.
638#if SLANG_PROCESSOR_ARM_64 || SLANG_LINUX_FAMILY
639    SLANG_IGNORE_TEST
640#endif
641    StressTest test;
642    test.run();
643}