diff --git a/llvm/test/CMakeLists.txt b/llvm/test/CMakeLists.txt index 07acb9d83c8dd..5b08d349f478d 100644 --- a/llvm/test/CMakeLists.txt +++ b/llvm/test/CMakeLists.txt @@ -79,8 +79,9 @@ set(LLVM_TEST_DEPENDS llvm-bitcode-strip llvm-c-test llvm-cas - llvm-cas-object-format llvm-cas-dump + llvm-cas-object-format + llvm-cas-test llvm-cat llvm-cfi-verify llvm-cgdata diff --git a/llvm/test/check-lock-files.ll b/llvm/test/tools/llvm-cas-test/check-lock-files.ll similarity index 60% rename from llvm/test/check-lock-files.ll rename to llvm/test/tools/llvm-cas-test/check-lock-files.ll index f8740a9aa4006..01ff56b86bd50 100644 --- a/llvm/test/check-lock-files.ll +++ b/llvm/test/tools/llvm-cas-test/check-lock-files.ll @@ -1,6 +1,4 @@ -# REQUIRES: ondisk_cas - # Multi-threaded test that CAS lock files protecting the shared data are working. # RUN: rm -rf %t/cas -# RUN: llvm-cas -cas %t/cas -check-lock-files \ No newline at end of file +# RUN: llvm-cas-test -cas %t/cas -check-lock-files diff --git a/llvm/test/tools/llvm-cas-test/lit.local.cfg b/llvm/test/tools/llvm-cas-test/lit.local.cfg new file mode 100644 index 0000000000000..379945b68925d --- /dev/null +++ b/llvm/test/tools/llvm-cas-test/lit.local.cfg @@ -0,0 +1,2 @@ +if not config.have_ondisk_cas: + config.unsupported = True diff --git a/llvm/test/tools/llvm-cas-test/stress.test b/llvm/test/tools/llvm-cas-test/stress.test new file mode 100644 index 0000000000000..3200b4d79e3e2 --- /dev/null +++ b/llvm/test/tools/llvm-cas-test/stress.test @@ -0,0 +1,5 @@ +REQUIRES: expensive_checks + +RUN: rm -rf %t.cas +RUN: llvm-cas-test --cas %t.cas --timeout 60 + diff --git a/llvm/tools/llvm-cas-test/CMakeLists.txt b/llvm/tools/llvm-cas-test/CMakeLists.txt new file mode 100644 index 0000000000000..ba11456d69685 --- /dev/null +++ b/llvm/tools/llvm-cas-test/CMakeLists.txt @@ -0,0 +1,7 @@ +set(LLVM_LINK_COMPONENTS + CAS + Support + ) +add_llvm_tool(llvm-cas-test + llvm-cas-test.cpp + ) diff --git a/llvm/tools/llvm-cas-test/llvm-cas-test.cpp b/llvm/tools/llvm-cas-test/llvm-cas-test.cpp new file mode 100644 index 0000000000000..2ea001fd536b4 --- /dev/null +++ b/llvm/tools/llvm-cas-test/llvm-cas-test.cpp @@ -0,0 +1,310 @@ +//===- llvm-cas-test.cpp - CAS stress tester ------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +#include "llvm/ADT/BitmaskEnum.h" +#include "llvm/CAS/ActionCache.h" +#include "llvm/CAS/BuiltinUnifiedCASDatabases.h" +#include "llvm/CAS/ObjectStore.h" +#include "llvm/Support/CommandLine.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/Program.h" +#include "llvm/Support/RandomNumberGenerator.h" +#include "llvm/Support/ThreadPool.h" + +using namespace llvm; +using namespace llvm::cas; + +enum CommandKind { + StressTest, + GenerateData, + CheckLockFiles +}; + +static cl::opt + Command(cl::desc("choose command action:"), + cl::values(clEnumValN(StressTest, "stress-test", "stress test CAS"), + clEnumValN(GenerateData, "gen-data", + "fill CAS with random data"), + clEnumValN(CheckLockFiles, "check-lock-files", + "check lock files")), + cl::init(CommandKind::StressTest)); + +// CAS configuration. +static cl::opt + CASPath("cas", cl::desc("CAS path on disk for testing"), cl::Required); +static cl::opt + PrintConfig("print-config", + cl::desc("print randomly generated configuration")); +static cl::opt + ForceKill("force-kill", + cl::desc("force kill subprocess to test termination")); +static cl::opt KeepLog("keep-log", + cl::desc("keep log and do not rotate the log")); + +// CAS stress test parameters. +static cl::opt + OptNumShards("num-shards", cl::desc("number of shards"), cl::init(0)); +static cl::opt OptTreeDepth("tree-depth", cl::desc("tree depth"), + cl::init(0)); +static cl::opt OptNumChildren("num-children", + cl::desc("number of child nodes"), + cl::init(0)); +static cl::opt OptDataLength("data-length", cl::desc("data length"), + cl::init(0)); +static cl::opt OptPrecentFile( + "precent-file", + cl::desc("percentage of nodes that is long enough to be file based"), + cl::init(0)); +// Default size to be 100MB. +static cl::opt + SizeLimit("size-limit", cl::desc("CAS size limit (in MB)"), cl::init(100)); +// Default timeout 180s. +static cl::opt + Timeout("timeout", cl::desc("test timeout (in seconds)"), cl::init(180)); + +enum CASFuzzingSettings : uint8_t { + Default = 0, + Fork = 1, // CAS Data filling happens in subprocesses. + CheckTermination = 1 << 1, // Try kill the subprocess when it fills the data. + + Last = UINT8_MAX, // Enum is randomly generated, use MAX to cover all inputs. + LLVM_MARK_AS_BITMASK_ENUM(Last) +}; + +struct Config { + CASFuzzingSettings Settings = Default; + uint8_t NumShards; + uint8_t NumChildren; + uint8_t TreeDepth; + uint16_t DataLength; + uint16_t PrecentFile; + + static constexpr unsigned MaxShards = 20; + static constexpr unsigned MaxChildren = 32; + static constexpr unsigned MaxDepth = 8; + static constexpr unsigned MaxDataLength = 1024 * 4; + + void constrainParameters() { + // reduce the size of parameter if they are too big. + NumShards = NumShards % MaxShards; + NumChildren = NumChildren % MaxChildren; + TreeDepth = TreeDepth % MaxDepth; + DataLength = DataLength % MaxDataLength; + PrecentFile = PrecentFile % 100; + + if (ForceKill) { + Settings |= Fork; + Settings |= CheckTermination; + } + } + + bool extendToFile(uint8_t Seed) const { + return ((float)Seed / (float)UINT8_MAX) > ((float)PrecentFile / 100.0f); + } + + void init() { + NumShards = OptNumShards ? OptNumShards : MaxShards; + NumChildren = OptNumChildren ? OptNumChildren : MaxChildren; + TreeDepth = OptTreeDepth ? OptTreeDepth : MaxDepth; + DataLength = OptDataLength ? OptDataLength : MaxDataLength; + PrecentFile = OptPrecentFile; + } + + void appendCommandLineOpts(std::vector &Cmd) { + Cmd.push_back("--num-shards=" + utostr(NumShards)); + Cmd.push_back("--num-children=" + utostr(NumChildren)); + Cmd.push_back("--tree-depth=" + utostr(TreeDepth)); + Cmd.push_back("--data-length=" + utostr(DataLength)); + Cmd.push_back("--precent-file=" + utostr(PrecentFile)); + } + + void dump() { + llvm::errs() << "## Configuration:" + << " Fork: " << (bool)(Settings & Fork) + << " Kill: " << (bool)(Settings & CheckTermination) + << " NumShards: " << (unsigned)NumShards + << " TreeDepth: " << (unsigned)TreeDepth + << " NumChildren: " << (unsigned)NumChildren + << " DataLength: " << (unsigned)DataLength + << " PrecentFile: " << (unsigned)PrecentFile << "\n"; + } +}; + +// fill the CAS with random data of specified tree depth and children numbers. +static void fillData(ObjectStore &CAS, const Config &Conf) { + ExitOnError ExitOnErr("llvm-cas-test fill data: "); + DefaultThreadPool ThreadPool(hardware_concurrency()); + std::atomic NumCreated = 0; + for (size_t I = 0; I != Conf.NumShards; ++I) { + ThreadPool.async([&] { + std::vector Refs; + for (unsigned Depth = 0; Depth < Conf.TreeDepth; ++Depth) { + unsigned NumNodes = (Conf.TreeDepth - Depth + 1) * Conf.NumChildren + 1; + std::vector Created; + Created.reserve(NumNodes); + ArrayRef PreviouslyCreated(Refs); + for (unsigned I = 0; I < NumNodes; ++I) { + std::vector Data(Conf.DataLength); + getRandomBytes(Data.data(), Data.size()); + // Use the first byte that generated to decide if we should make it + // 64KB bigger and force that into a file based storage. + if (Conf.extendToFile(Data[0])) + Data.resize(64LL * 1024LL + Conf.DataLength); + + if (Depth == 0) { + auto Ref = ExitOnErr(CAS.store({}, Data)); + Created.push_back(Ref); + } else { + auto Parent = PreviouslyCreated.slice(I, Conf.NumChildren); + auto Ref = ExitOnErr(CAS.store(Parent, Data)); + Created.push_back(Ref); + } + ++NumCreated; + } + Refs.swap(Created); + } + }); + } + ThreadPool.wait(); +} + +static int genData() { + ExitOnError ExitOnErr("llvm-cas-test --gen-data: "); + + Config Conf; + Conf.init(); + + auto DB = ExitOnErr(cas::createOnDiskUnifiedCASDatabases(CASPath)); + fillData(*DB.first, Conf); + + return 0; +} + +static int runOneTest(const char *Argv0) { + ExitOnError ExitOnErr("llvm-cas-test: "); + + Config Conf; + getRandomBytes(&Conf, sizeof(Conf)); + Conf.constrainParameters(); + + if (PrintConfig) + Conf.dump(); + + // Start with fresh log if --keep-log is not used. + if (!KeepLog) { + static constexpr StringLiteral LogFile = "v1.log"; + SmallString<256> LogPath(CASPath); + llvm::sys::path::append(LogPath, LogFile); + llvm::sys::fs::remove(LogPath); + } + + auto DB = ExitOnErr(cas::createOnDiskUnifiedCASDatabases(CASPath)); + auto &CAS = DB.first; + + // Size limit in MB. + ExitOnErr(CAS->setSizeLimit(SizeLimit * 1024 * 1024)); + if (Conf.Settings & Fork) { + // fill data using sub processes. + std::string MainExe = sys::fs::getMainExecutable(Argv0, &CASPath); + std::vector Args = {MainExe, "--gen-data", "--cas=" + CASPath}; + Conf.appendCommandLineOpts(Args); + + std::vector Cmd; + for_each(Args, [&Cmd](const std::string &Arg) { Cmd.push_back(Arg); }); + + std::vector Subprocesses; + for (int I = 0; I < Conf.NumShards; ++I) { + auto SP = sys::ExecuteNoWait(MainExe, Cmd, std::nullopt); + if (SP.Pid != 0) + Subprocesses.push_back(SP); + } + + if (Conf.Settings & CheckTermination) { + for_each(Subprocesses, [](auto &P) { + // Wait 1 second and killed the process. + auto WP = sys::Wait(P, 1); + if (WP.ReturnCode) + llvm::errs() << "subprocess killed successfully\n"; + }); + } else { + for_each(Subprocesses, [](auto &P) { sys::Wait(P, std::nullopt); }); + } + + } else { + // in-process fill data. + fillData(*CAS, Conf); + } + + // validate and prune in the end. + ExitOnErr(CAS->validate(true)); + ExitOnErr(CAS->pruneStorageData()); + + return 0; +} + +static int stressTest(const char *Argv0) { + auto Start = std::chrono::steady_clock::now(); + std::chrono::seconds Duration(Timeout); + + while (std::chrono::steady_clock::now() - Start < Duration) { + if (int Res = runOneTest(Argv0)) + return Res; + } + + return 0; +} + +static int checkLockFiles() { + ExitOnError ExitOnErr("llvm-cas-test: check-lock-files: "); + + SmallString<128> DataPoolPath(CASPath); + sys::path::append(DataPoolPath, "v1.1/v8.data"); + + auto OpenCASAndGetDataPoolSize = [&]() -> Expected { + auto Result = createOnDiskUnifiedCASDatabases(CASPath); + if (!Result) + return Result.takeError(); + + sys::fs::file_status DataStat; + if (std::error_code EC = sys::fs::status(DataPoolPath, DataStat)) + ExitOnErr(createFileError(DataPoolPath, EC)); + return DataStat.getSize(); + }; + + // Get the normal size of an open CAS data pool to compare against later. + uint64_t OpenSize = ExitOnErr(OpenCASAndGetDataPoolSize()); + + DefaultThreadPool Pool; + for (int I = 0; I < 1000; ++I) { + Pool.async([&, I] { + uint64_t DataPoolSize = ExitOnErr(OpenCASAndGetDataPoolSize()); + if (DataPoolSize < OpenSize) + ExitOnErr(createStringError( + inconvertibleErrorCode(), + StringRef("CAS data file size (" + std::to_string(DataPoolSize) + + ") is smaller than expected (" + + std::to_string(OpenSize) + ") in iteration " + + std::to_string(I)))); + }); + } + + Pool.wait(); + return 0; +} + +int main(int argc, char **argv) { + cl::ParseCommandLineOptions(argc, argv, "llvm-cas-test CAS testing tool\n"); + + switch (Command) { + case GenerateData: + return genData(); + case StressTest: + return stressTest(argv[0]); + case CheckLockFiles: + return checkLockFiles(); + } +} diff --git a/llvm/tools/llvm-cas/llvm-cas.cpp b/llvm/tools/llvm-cas/llvm-cas.cpp index 40ccf5c1ec57a..5759088efdf9a 100644 --- a/llvm/tools/llvm-cas/llvm-cas.cpp +++ b/llvm/tools/llvm-cas/llvm-cas.cpp @@ -23,8 +23,6 @@ #include "llvm/Support/MemoryBuffer.h" #include "llvm/Support/Path.h" #include "llvm/Support/PrefixMapper.h" -#include "llvm/Support/StringSaver.h" -#include "llvm/Support/ThreadPool.h" #include "llvm/Support/raw_ostream.h" #include #include @@ -71,7 +69,6 @@ static int validateIfNeeded(StringRef Path, StringRef PluginPath, bool Force, bool AllowRecovery, bool InProcess, const char *Argv0); static int ingestCasIDFile(cas::ObjectStore &CAS, ArrayRef CASIDs); -static int checkLockFiles(StringRef CASPath); static int prune(cas::ObjectStore &CAS); int main(int Argc, char **Argv) { @@ -117,7 +114,6 @@ int main(int Argc, char **Argv) { Import, PutCacheKey, GetCacheResult, - CheckLockFiles, Validate, ValidateObject, ValidateIfNeeded, @@ -146,8 +142,6 @@ int main(int Argc, char **Argv) { "set a value for a cache key"), clEnumValN(GetCacheResult, "get-cache-result", "get the result value from a cache key"), - clEnumValN(CheckLockFiles, "check-lock-files", - "Test file locking behaviour of on-disk CAS"), clEnumValN(Validate, "validate", "validate ObjectStore"), clEnumValN(ValidateObject, "validate-object", "validate the object for CASID"), @@ -168,9 +162,6 @@ int main(int Argc, char **Argv) { ExitOnErr( createStringError(inconvertibleErrorCode(), "missing --cas=")); - if (Command == CheckLockFiles) - return checkLockFiles(CASPath); - if (Command == ValidateIfNeeded) return validateIfNeeded(CASPath, CASPluginPath, CASPluginOpts, CheckHash, Force, AllowRecovery, InProcess, Argv[0]); @@ -699,45 +690,6 @@ static int getCacheResult(ObjectStore &CAS, ActionCache &AC, const CASID &ID) { return 0; } -static int checkLockFiles(StringRef CASPath) { - ExitOnError ExitOnErr("llvm-cas: check-lock-files: "); - - SmallString<128> DataPoolPath(CASPath); - sys::path::append(DataPoolPath, "v1.1/v8.data"); - - auto OpenCASAndGetDataPoolSize = [&]() -> Expected { - auto Result = createOnDiskUnifiedCASDatabases(CASPath); - if (!Result) - return Result.takeError(); - - sys::fs::file_status DataStat; - if (std::error_code EC = sys::fs::status(DataPoolPath, DataStat)) - ExitOnErr(createFileError(DataPoolPath, EC)); - return DataStat.getSize(); - }; - - // Get the normal size of an open CAS data pool to compare against later. - uint64_t OpenSize = ExitOnErr(OpenCASAndGetDataPoolSize()); - - DefaultThreadPool Pool; - for (int i = 0; i < 1000; ++i) { - Pool.async([&, i] { - uint64_t DataPoolSize = ExitOnErr(OpenCASAndGetDataPoolSize()); - if (DataPoolSize < OpenSize) - ExitOnErr(createStringError( - inconvertibleErrorCode(), - StringRef("CAS data file size (" + std::to_string(DataPoolSize) + - ") is smaller than expected (" + - std::to_string(OpenSize) + ") in iteration " + - std::to_string(i)))); - }); - } - - Pool.wait(); - - return 0; -} - int validateObject(ObjectStore &CAS, const CASID &ID) { ExitOnError ExitOnErr("llvm-cas: validate-object: "); ExitOnErr(CAS.validateObject(ID)); @@ -790,4 +742,4 @@ static int prune(cas::ObjectStore &CAS) { ExitOnError ExitOnErr("llvm-cas: prune: "); ExitOnErr(CAS.pruneStorageData()); return 0; -} \ No newline at end of file +}