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