def testClearPostcondition(self): cache = clcache.Cache() # Compile a random file to populate cache cmd = CLCACHE_CMD + ["/nologo", "/EHsc", "/c", os.path.join(ASSETS_DIR, "fibonacci.cpp")] subprocess.check_call(cmd) # Now there should be something in the cache with cache.statistics as stats: self.assertTrue(stats.currentCacheSize() > 0) self.assertTrue(stats.numCacheEntries() > 0) # Now, clear the cache: the stats should remain unchanged except for # the cache size and number of cache entries. oldStats = copy.copy(cache.statistics) self._clearCache() with cache.statistics as stats: self.assertEqual(stats.currentCacheSize(), 0) self.assertEqual(stats.numCacheEntries(), 0) self.assertEqual(stats.numCallsWithoutSourceFile(), oldStats.numCallsWithoutSourceFile()) self.assertEqual(stats.numCallsWithMultipleSourceFiles(), oldStats.numCallsWithMultipleSourceFiles()) self.assertEqual(stats.numCallsWithPch(), oldStats.numCallsWithPch()) self.assertEqual(stats.numCallsForLinking(), oldStats.numCallsForLinking()) self.assertEqual(stats.numCallsForPreprocessing(), oldStats.numCallsForPreprocessing()) self.assertEqual(stats.numCallsForExternalDebugInfo(), oldStats.numCallsForExternalDebugInfo()) self.assertEqual(stats.numEvictedMisses(), oldStats.numEvictedMisses()) self.assertEqual(stats.numHeaderChangedMisses(), oldStats.numHeaderChangedMisses()) self.assertEqual(stats.numSourceChangedMisses(), oldStats.numSourceChangedMisses()) self.assertEqual(stats.numCacheHits(), oldStats.numCacheHits()) self.assertEqual(stats.numCacheMisses(), oldStats.numCacheMisses())
def testHitsViaMpConcurrent(self): with cd(os.path.join(ASSETS_DIR, "parallel")), tempfile.TemporaryDirectory() as tempDir: cache = clcache.Cache(tempDir) customEnv = self._createEnv(tempDir) with cache.statistics as stats: self.assertEqual(stats.numCacheHits(), 0) self.assertEqual(stats.numCacheMisses(), 0) self.assertEqual(stats.numCacheEntries(), 0) cmd = CLCACHE_CMD + ["/nologo", "/EHsc", "/c"] # Compile two random files subprocess.check_call(cmd + ["fibonacci01.cpp"], env=customEnv) subprocess.check_call(cmd + ["fibonacci02.cpp"], env=customEnv) with cache.statistics as stats: self.assertEqual(stats.numCacheHits(), 0) self.assertEqual(stats.numCacheMisses(), 2) self.assertEqual(stats.numCacheEntries(), 2) # Compile same two files concurrently, this should hit twice. subprocess.check_call(cmd + ["/MP2", "fibonacci01.cpp", "fibonacci02.cpp"], env=customEnv) with cache.statistics as stats: self.assertEqual(stats.numCacheHits(), 2) self.assertEqual(stats.numCacheMisses(), 2) self.assertEqual(stats.numCacheEntries(), 2)
def testHitViaMpSequential(self): with cd(os.path.join(ASSETS_DIR, "parallel")), tempfile.TemporaryDirectory() as tempDir: cache = clcache.Cache(tempDir) customEnv = self._createEnv(tempDir) with cache.statistics as stats: self.assertEqual(stats.numCacheHits(), 0) self.assertEqual(stats.numCacheMisses(), 0) self.assertEqual(stats.numCacheEntries(), 0) cmd = CLCACHE_CMD + ["/nologo", "/EHsc", "/c"] # Compile random file, filling cache subprocess.check_call(cmd + ["fibonacci01.cpp"], env=customEnv) with cache.statistics as stats: self.assertEqual(stats.numCacheHits(), 0) self.assertEqual(stats.numCacheMisses(), 1) self.assertEqual(stats.numCacheEntries(), 1) # Compile same files with specifying /MP, this should hit subprocess.check_call(cmd + ["/MP", "fibonacci01.cpp"], env=customEnv) with cache.statistics as stats: self.assertEqual(stats.numCacheHits(), 1) self.assertEqual(stats.numCacheMisses(), 1) self.assertEqual(stats.numCacheEntries(), 1)
def testAlternatingIncludeOrder(self): with cd(os.path.join(ASSETS_DIR, "hits-and-misses")), tempfile.TemporaryDirectory() as tempDir: cache = clcache.Cache(tempDir) customEnv = dict(os.environ, CLCACHE_DIR=tempDir) baseCmd = CLCACHE_CMD + ["/nologo", "/EHsc", "/c"] with open('A.h', 'w') as header: header.write('#define A 1\n') with open('B.h', 'w') as header: header.write('#define B 1\n') with cache.statistics as stats: self.assertEqual(stats.numCacheHits(), 0) self.assertEqual(stats.numCacheMisses(), 0) self.assertEqual(stats.numCacheEntries(), 0) # VERSION 1 with open('stable-source-with-alternating-header.h', 'w') as f: f.write('#include "A.h"\n') f.write('#include "B.h"\n') subprocess.check_call(baseCmd + ["stable-source-with-alternating-header.cpp"], env=customEnv) with cache.statistics as stats: self.assertEqual(stats.numCacheHits(), 0) self.assertEqual(stats.numCacheMisses(), 1) self.assertEqual(stats.numCacheEntries(), 1) # VERSION 2 with open('stable-source-with-alternating-header.h', 'w') as f: f.write('#include "B.h"\n') f.write('#include "A.h"\n') subprocess.check_call(baseCmd + ["stable-source-with-alternating-header.cpp"], env=customEnv) with cache.statistics as stats: self.assertEqual(stats.numCacheHits(), 0) self.assertEqual(stats.numCacheMisses(), 2) self.assertEqual(stats.numCacheEntries(), 2) # VERSION 1 again with open('stable-source-with-alternating-header.h', 'w') as f: f.write('#include "A.h"\n') f.write('#include "B.h"\n') subprocess.check_call(baseCmd + ["stable-source-with-alternating-header.cpp"], env=customEnv) with cache.statistics as stats: self.assertEqual(stats.numCacheHits(), 1) self.assertEqual(stats.numCacheMisses(), 2) self.assertEqual(stats.numCacheEntries(), 2) # VERSION 2 again with open('stable-source-with-alternating-header.h', 'w') as f: f.write('#include "B.h"\n') f.write('#include "A.h"\n') subprocess.check_call(baseCmd + ["stable-source-with-alternating-header.cpp"], env=customEnv) with cache.statistics as stats: self.assertEqual(stats.numCacheHits(), 2) self.assertEqual(stats.numCacheMisses(), 2) self.assertEqual(stats.numCacheEntries(), 2)
def testPreprocessorFailure(self): cache = clcache.Cache() oldStats = copy.copy(cache.statistics) cmd = CLCACHE_CMD + ["/nologo", "/c", "doesnotexist.cpp"] self.assertNotEqual(subprocess.call(cmd, env=self.env), 0) self.assertEqual(cache.statistics, oldStats)
def testOutput(self): with cd(os.path.join(ASSETS_DIR, "parallel")), tempfile.TemporaryDirectory() as tempDir: sources = glob.glob("*.cpp") clcache.Cache(tempDir) customEnv = self._createEnv(tempDir) cmd = CLCACHE_CMD + ["/nologo", "/EHsc", "/c"] mpFlag = "/MP" + str(len(sources)) out = subprocess.check_output(cmd + [mpFlag] + sources, env=customEnv).decode("ascii") for s in sources: self.assertEqual(out.count(s), 1)
def testParallel(self): with cd(os.path.join(ASSETS_DIR, "parallel")): self._zeroStats() # Compile first time self._buildAll() cache = clcache.Cache() with cache.statistics as stats: hits = stats.numCacheHits() misses = stats.numCacheMisses() self.assertEqual(hits + misses, 10) # Compile second time self._buildAll() cache = clcache.Cache() with cache.statistics as stats: hits = stats.numCacheHits() misses = stats.numCacheMisses() self.assertEqual(hits + misses, 20)
def testHitsSimple(self): with cd(os.path.join(ASSETS_DIR, "hits-and-misses")): cmd = CLCACHE_CMD + ["/nologo", "/EHsc", "/c", 'hit.cpp'] subprocess.check_call(cmd) # Ensure it has been compiled before cache = clcache.Cache() with cache.statistics as stats: oldHits = stats.numCacheHits() subprocess.check_call(cmd) # This must hit now with cache.statistics as stats: newHits = stats.numCacheHits() self.assertEqual(newHits, oldHits + 1)
def testEvictedManifest(self): with cd(os.path.join(ASSETS_DIR, "hits-and-misses")), tempfile.TemporaryDirectory() as tempDir: customEnv = dict(os.environ, CLCACHE_DIR=tempDir) cmd = CLCACHE_CMD + ["/nologo", "/EHsc", "/c", 'hit.cpp'] # Compile once to insert the object in the cache subprocess.check_call(cmd, env=customEnv) # Remove manifest cache = clcache.Cache(tempDir) clcache.clearCache(cache) self.assertEqual(subprocess.call(cmd, env=customEnv), 0)
def setUp(self): self.projectDir = os.path.join(ASSETS_DIR, "basedir") self.tempDir = tempfile.TemporaryDirectory() self.clcacheDir = os.path.join(self.tempDir.name, "clcache") self.savedCwd = os.getcwd() os.chdir(self.tempDir.name) # First, create two separate build directories with the same sources for buildDir in ["builddir_a", "builddir_b"]: shutil.copytree(self.projectDir, buildDir) self.cache = clcache.Cache(self.clcacheDir)
def testHit(self): with cd(os.path.join(ASSETS_DIR, "hits-and-misses")): cmd = CLCACHE_CMD + ["/nologo", "/EHsc", "/c", "hit.cpp"] self.assertEqual(subprocess.call(cmd, env=self.env), 0) cache = clcache.Cache() with cache.statistics as stats: oldHits = stats.numCacheHits() self.assertEqual(subprocess.call(cmd, env=self.env), 0) # This should hit now with cache.statistics as stats: self.assertEqual(stats.numCacheHits(), oldHits + 1)
def testClearIdempotency(self): cache = clcache.Cache() self._clearCache() with cache.statistics as stats: self.assertEqual(stats.currentCacheSize(), 0) self.assertEqual(stats.numCacheEntries(), 0) # Clearing should be idempotent self._clearCache() with cache.statistics as stats: self.assertEqual(stats.currentCacheSize(), 0) self.assertEqual(stats.numCacheEntries(), 0)
def testRemovedTransitiveHeader(self): with cd(os.path.join(ASSETS_DIR, "hits-and-misses")), tempfile.TemporaryDirectory() as tempDir: cache = clcache.Cache(tempDir) customEnv = dict(os.environ, CLCACHE_DIR=tempDir) baseCmd = CLCACHE_CMD + ["/nologo", "/EHsc", "/c"] with cache.statistics as stats: self.assertEqual(stats.numCacheHits(), 0) self.assertEqual(stats.numCacheMisses(), 0) self.assertEqual(stats.numCacheEntries(), 0) # VERSION 1 with open('alternating-header.h', 'w') as f: f.write("#define VERSION 1\n") subprocess.check_call(baseCmd + ["stable-source-transitive-header.cpp"], env=customEnv) with cache.statistics as stats: self.assertEqual(stats.numCacheHits(), 0) self.assertEqual(stats.numCacheMisses(), 1) self.assertEqual(stats.numCacheEntries(), 1) # Remove header, trigger the compiler which should fail os.remove('alternating-header.h') with self.assertRaises(subprocess.CalledProcessError): subprocess.check_call(baseCmd + ["stable-source-transitive-header.cpp"], env=customEnv) with cache.statistics as stats: self.assertEqual(stats.numCacheHits(), 0) self.assertEqual(stats.numCacheMisses(), 2) self.assertEqual(stats.numCacheEntries(), 1) # VERSION 1 again with open('alternating-header.h', 'w') as f: f.write("#define VERSION 1\n") subprocess.check_call(baseCmd + ["stable-source-transitive-header.cpp"], env=customEnv) with cache.statistics as stats: self.assertEqual(stats.numCacheHits(), 1) self.assertEqual(stats.numCacheMisses(), 2) self.assertEqual(stats.numCacheEntries(), 1) # Remove header again, trigger the compiler which should fail os.remove('alternating-header.h') with self.assertRaises(subprocess.CalledProcessError): subprocess.check_call(baseCmd + ["stable-source-transitive-header.cpp"], env=customEnv) with cache.statistics as stats: self.assertEqual(stats.numCacheHits(), 1) self.assertEqual(stats.numCacheMisses(), 3) self.assertEqual(stats.numCacheEntries(), 1)
def testObsoleteHeaderDisappears(self): # A includes B with cd(os.path.join(ASSETS_DIR, "header-miss-obsolete")), tempfile.TemporaryDirectory() as tempDir: customEnv = dict(os.environ, CLCACHE_DIR=tempDir) compileCmd = CLCACHE_CMD + ["/I.", "/nologo", "/EHsc", "/c", "main.cpp"] cache = clcache.Cache(tempDir) with open("A.h", "w") as header: header.write('#define INFO 1337\n') header.write('#include "B.h"\n') with open("B.h", "w") as header: header.write('#define SOMETHING 1\n') subprocess.check_call(compileCmd, env=customEnv) with cache.statistics as stats: headerChangedMisses1 = stats.numHeaderChangedMisses() hits1 = stats.numCacheHits() misses1 = stats.numCacheMisses() # Make include B.h obsolete with open("A.h", "w") as header: header.write('#define INFO 1337\n') header.write('\n') os.remove("B.h") subprocess.check_call(compileCmd, env=customEnv) with cache.statistics as stats: headerChangedMisses2 = stats.numHeaderChangedMisses() hits2 = stats.numCacheHits() misses2 = stats.numCacheMisses() self.assertEqual(headerChangedMisses2, headerChangedMisses1+1) self.assertEqual(misses2, misses1+1) self.assertEqual(hits2, hits1) # Ensure the new manifest was stored subprocess.check_call(compileCmd, env=customEnv) with cache.statistics as stats: headerChangedMisses3 = stats.numHeaderChangedMisses() hits3 = stats.numCacheHits() misses3 = stats.numCacheMisses() self.assertEqual(headerChangedMisses3, headerChangedMisses2) self.assertEqual(misses3, misses2) self.assertEqual(hits3, hits2+1)
def testHitsSimple(self): invocations = [ ["/nologo", "/E"], ["/nologo", "/EP", "/c"], ["/nologo", "/P", "/c"], ["/nologo", "/E", "/EP"], ] cache = clcache.Cache() with cache.statistics as stats: oldPreprocessorCalls = stats.numCallsForPreprocessing() for i, invocation in enumerate(invocations, 1): cmd = CLCACHE_CMD + invocation + [os.path.join(ASSETS_DIR, "minimal.cpp")] subprocess.check_call(cmd) with cache.statistics as stats: newPreprocessorCalls = stats.numCallsForPreprocessing() self.assertEqual(newPreprocessorCalls, oldPreprocessorCalls + i, str(cmd))
def testFive(self): with cd(os.path.join(ASSETS_DIR, "mutiple-sources")), tempfile.TemporaryDirectory() as tempDir: cache = clcache.Cache(tempDir) customEnv = dict(os.environ, CLCACHE_DIR=tempDir) baseCmd = CLCACHE_CMD + ["/nologo", "/EHsc", "/c"] with cache.statistics as stats: self.assertEqual(stats.numCacheHits(), 0) self.assertEqual(stats.numCacheMisses(), 0) self.assertEqual(stats.numCacheEntries(), 0) subprocess.check_call(baseCmd + [ "fibonacci01.cpp", "fibonacci02.cpp", "fibonacci03.cpp", "fibonacci04.cpp", "fibonacci05.cpp", ], env=customEnv) with cache.statistics as stats: self.assertEqual(stats.numCacheHits(), 0) self.assertEqual(stats.numCacheMisses(), 5) self.assertEqual(stats.numCacheEntries(), 5) subprocess.check_call(baseCmd + [ "fibonacci01.cpp", "fibonacci02.cpp", "fibonacci03.cpp", "fibonacci04.cpp", "fibonacci05.cpp", ], env=customEnv) with cache.statistics as stats: self.assertEqual(stats.numCacheHits(), 5) self.assertEqual(stats.numCacheMisses(), 5) self.assertEqual(stats.numCacheEntries(), 5)
def testConcurrentHitsScaling(self): with tempfile.TemporaryDirectory() as tempDir: customEnv = dict(os.environ, CLCACHE_DIR=tempDir) cache = clcache.Cache(tempDir) with cache.statistics as stats: self.assertEqual(stats.numCacheHits(), 0) self.assertEqual(stats.numCacheMisses(), 0) self.assertEqual(stats.numCacheEntries(), 0) # Populate cache cmd = CLCACHE_CMD + ['/nologo', '/EHsc', '/c' ] + TestConcurrency.sources coldCacheSequential = takeTime( lambda: subprocess.check_call(cmd, env=customEnv)) with cache.statistics as stats: self.assertEqual(stats.numCacheHits(), 0) self.assertEqual(stats.numCacheMisses(), len(TestConcurrency.sources)) self.assertEqual(stats.numCacheEntries(), len(TestConcurrency.sources)) # Compile one-by-one, measuring the time. cmd = CLCACHE_CMD + ['/nologo', '/EHsc', '/c' ] + TestConcurrency.sources hotCacheSequential = takeTime( lambda: subprocess.check_call(cmd, env=customEnv)) with cache.statistics as stats: self.assertEqual(stats.numCacheHits(), len(TestConcurrency.sources)) self.assertEqual(stats.numCacheMisses(), len(TestConcurrency.sources)) self.assertEqual(stats.numCacheEntries(), len(TestConcurrency.sources)) # Recompile with many concurrent processes, measuring time cmd = CLCACHE_CMD + [ '/nologo', '/EHsc', '/c', '/MP{}'.format(cpu_count()) ] + TestConcurrency.sources hotCacheConcurrent = takeTime( lambda: subprocess.check_call(cmd, env=customEnv)) with cache.statistics as stats: self.assertEqual(stats.numCacheHits(), len(TestConcurrency.sources) * 2) self.assertEqual(stats.numCacheMisses(), len(TestConcurrency.sources)) self.assertEqual(stats.numCacheEntries(), len(TestConcurrency.sources)) print( "Compiling {} source files sequentially, cold cache: {} seconds" .format(len(TestConcurrency.sources), coldCacheSequential)) print( "Compiling {} source files sequentially, hot cache: {} seconds" .format(len(TestConcurrency.sources), hotCacheSequential)) print( "Compiling {} source files concurrently via /MP{}, hot cache: {} seconds" .format(len(TestConcurrency.sources), cpu_count(), hotCacheConcurrent))