Skip to content

Commit 373b461

Browse files
[llvm][cas] Implement a CAS stress tester
This is a basic tester for CAS ObjectStore that will insert random data into CAS and validate with several configurations randomly generated. It will check: * multi-threaded insertion * multi-process insertion * try randomly kill the subprocesses that are inserting data And make sure it doesn't leave CAS in an invalid state. Suggested usage: ``` LLVM_CAS_LOG=2 llvm-cas-test --cas-path=... --print-config ```
1 parent 43cfa32 commit 373b461

File tree

4 files changed

+269
-1
lines changed

4 files changed

+269
-1
lines changed

llvm/test/CMakeLists.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,9 @@ set(LLVM_TEST_DEPENDS
7979
llvm-bitcode-strip
8080
llvm-c-test
8181
llvm-cas
82-
llvm-cas-object-format
8382
llvm-cas-dump
83+
llvm-cas-object-format
84+
llvm-cas-test
8485
llvm-cat
8586
llvm-cfi-verify
8687
llvm-cgdata

llvm/test/tools/llvm-cas/stress.test

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
REQUIRES: expensive_checks
2+
3+
RUN: rm -rf %t.cas
4+
RUN: llvm-cas-test --cas-path %t.cas --timeout 60
5+
6+
// Clear the CAS if passed.
7+
RUN: rm -rf %t.cas
8+
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
set(LLVM_LINK_COMPONENTS
2+
CAS
3+
Support
4+
)
5+
add_llvm_tool(llvm-cas-test
6+
llvm-cas-test.cpp
7+
)
+252
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
//===- llvm-cas-test.cpp - CAS stress tester ------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
#include "llvm/ADT/BitmaskEnum.h"
9+
#include "llvm/CAS/ActionCache.h"
10+
#include "llvm/CAS/BuiltinUnifiedCASDatabases.h"
11+
#include "llvm/CAS/ObjectStore.h"
12+
#include "llvm/Support/CommandLine.h"
13+
#include "llvm/Support/Path.h"
14+
#include "llvm/Support/Program.h"
15+
#include "llvm/Support/RandomNumberGenerator.h"
16+
#include "llvm/Support/ThreadPool.h"
17+
18+
using namespace llvm;
19+
using namespace llvm::cas;
20+
21+
static cl::opt<std::string>
22+
CASPath("cas-path", cl::desc("CAS path on disk for testing"), cl::Required);
23+
static cl::opt<bool> GenData("gen-data",
24+
cl::desc("generate data into CAS only"));
25+
static cl::opt<bool>
26+
PrintConfig("print-config",
27+
cl::desc("print randomly generated configuration"));
28+
static cl::opt<bool>
29+
ForceKill("force-kill",
30+
cl::desc("force kill subprocess to test termination"));
31+
static cl::opt<unsigned>
32+
OptNumShards("num-shards", cl::desc("number of shards"), cl::init(0));
33+
static cl::opt<unsigned> OptTreeDepth("tree-depth", cl::desc("tree depth"),
34+
cl::init(0));
35+
static cl::opt<unsigned> OptNumChildren("num-children",
36+
cl::desc("number of child nodes"),
37+
cl::init(0));
38+
static cl::opt<unsigned> OptDataLength("data-length", cl::desc("data length"),
39+
cl::init(0));
40+
static cl::opt<unsigned> OptPrecentFile(
41+
"precent-file",
42+
cl::desc("percentage of nodes that is long enough to be file based"),
43+
cl::init(0));
44+
45+
// Default size to be 100MB.
46+
static cl::opt<uint64_t>
47+
SizeLimit("size-limit", cl::desc("CAS size limit (in MB)"), cl::init(100));
48+
// Default timeout 180s.
49+
static cl::opt<uint64_t>
50+
Timeout("timeout", cl::desc("test timeout (in seconds)"), cl::init(180));
51+
52+
enum CASFuzzingSettings : uint8_t {
53+
DEFAULT = 0,
54+
FORK = 1, // CAS Data filling happens in subprocesses.
55+
CHECK_TERMINATION = 1 << 1, // Try kill the subprocess when it fills the data.
56+
57+
LAST = UINT8_MAX, // Enum is randomly generated, use MAX to cover all inputs.
58+
LLVM_MARK_AS_BITMASK_ENUM(LAST)
59+
};
60+
61+
struct Config {
62+
CASFuzzingSettings Settings = DEFAULT;
63+
uint8_t NumShards;
64+
uint8_t NumChildren;
65+
uint8_t TreeDepth;
66+
uint16_t DataLength;
67+
uint16_t PrecentFile;
68+
69+
static constexpr unsigned MaxShards = 20;
70+
static constexpr unsigned MaxChildren = 32;
71+
static constexpr unsigned MaxDepth = 8;
72+
static constexpr unsigned MaxDataLength = 1024 * 4;
73+
74+
void constraint() {
75+
// reduce the size of parameter if they are too big.
76+
NumShards = NumShards % MaxShards;
77+
NumChildren = NumChildren % MaxChildren;
78+
TreeDepth = TreeDepth % MaxDepth;
79+
DataLength = DataLength % MaxDataLength;
80+
PrecentFile = PrecentFile % 100;
81+
82+
if (ForceKill) {
83+
Settings |= FORK;
84+
Settings |= CHECK_TERMINATION;
85+
}
86+
}
87+
88+
bool extendToFile(uint8_t Seed) const {
89+
return ((float)Seed / (float)UINT8_MAX) > ((float)PrecentFile / 100.0f);
90+
}
91+
92+
void init() {
93+
NumShards = OptNumShards ? OptNumShards : MaxShards;
94+
NumChildren = OptNumChildren ? OptNumChildren : MaxChildren;
95+
TreeDepth = OptTreeDepth ? OptTreeDepth : MaxDepth;
96+
DataLength = OptDataLength ? OptDataLength : MaxDataLength;
97+
PrecentFile = OptPrecentFile;
98+
}
99+
100+
void appendCommandLineOpts(std::vector<std::string> &Cmd) {
101+
Cmd.push_back("--num-shards=" + utostr(NumShards));
102+
Cmd.push_back("--num-children=" + utostr(NumChildren));
103+
Cmd.push_back("--tree-depth=" + utostr(TreeDepth));
104+
Cmd.push_back("--data-length=" + utostr(DataLength));
105+
Cmd.push_back("--precent-file=" + utostr(PrecentFile));
106+
}
107+
108+
void dump() {
109+
llvm::errs() << "## Configuration:"
110+
<< " Fork: " << (bool)(Settings & FORK)
111+
<< " Kill: " << (bool)(Settings & CHECK_TERMINATION)
112+
<< " NumShards: " << (unsigned)NumShards
113+
<< " TreeDepth: " << (unsigned)TreeDepth
114+
<< " NumChildren: " << (unsigned)NumChildren
115+
<< " DataLength: " << (unsigned)DataLength
116+
<< " PrecentFile: " << (unsigned)PrecentFile << "\n";
117+
}
118+
};
119+
120+
// fill the CAS with random data of specified tree depth and children numbers.
121+
static void fillData(ObjectStore &CAS, const Config &Conf) {
122+
ExitOnError ExitOnErr("llvm-cas-fuzzer fill data: ");
123+
DefaultThreadPool ThreadPool(hardware_concurrency());
124+
std::atomic<uint64_t> NumCreated = 0;
125+
for (size_t I = 0; I != Conf.NumShards; ++I) {
126+
ThreadPool.async([&] {
127+
std::vector<ObjectRef> Refs;
128+
for (unsigned Depth = 0; Depth < Conf.TreeDepth; ++Depth) {
129+
unsigned NumNodes = (Conf.TreeDepth - Depth + 1) * Conf.NumChildren + 1;
130+
std::vector<ObjectRef> Created;
131+
Created.reserve(NumNodes);
132+
ArrayRef<ObjectRef> PreviouslyCreated(Refs);
133+
for (unsigned I = 0; I < NumNodes; ++I) {
134+
std::vector<char> Data(Conf.DataLength);
135+
getRandomBytes(Data.data(), Data.size());
136+
// Use the first byte that generated to decide if we should make it
137+
// 64KB bigger and force that into a file based storage.
138+
if (Conf.extendToFile(Data[0]))
139+
Data.resize(64LL * 1024LL + Conf.DataLength);
140+
141+
if (Depth == 0) {
142+
auto Ref = ExitOnErr(CAS.store({}, Data));
143+
Created.push_back(Ref);
144+
} else {
145+
auto Parent = PreviouslyCreated.slice(I, Conf.NumChildren);
146+
auto Ref = ExitOnErr(CAS.store(Parent, Data));
147+
Created.push_back(Ref);
148+
}
149+
++NumCreated;
150+
}
151+
Refs.swap(Created);
152+
}
153+
});
154+
}
155+
ThreadPool.wait();
156+
}
157+
158+
static int genData() {
159+
ExitOnError ExitOnErr("llvm-cas-test --gen-data: ");
160+
161+
Config Conf;
162+
Conf.init();
163+
164+
auto DB = ExitOnErr(cas::createOnDiskUnifiedCASDatabases(CASPath));
165+
fillData(*DB.first, Conf);
166+
167+
return 0;
168+
}
169+
170+
static int runOneTest(const char *argv0) {
171+
ExitOnError ExitOnErr("llvm-cas-test: ");
172+
173+
Config Conf;
174+
getRandomBytes(&Conf, sizeof(Conf));
175+
Conf.constraint();
176+
177+
if (PrintConfig)
178+
Conf.dump();
179+
180+
// Start with fresh log.
181+
static constexpr StringLiteral LogFile = "v1.log";
182+
SmallString<256> LogPath(CASPath);
183+
llvm::sys::path::append(LogPath, LogFile);
184+
llvm::sys::fs::remove(LogPath);
185+
186+
auto DB = ExitOnErr(cas::createOnDiskUnifiedCASDatabases(CASPath));
187+
auto &CAS = DB.first;
188+
189+
// Size limit in MB.
190+
ExitOnErr(CAS->setSizeLimit(SizeLimit * 1024 * 1024));
191+
if (Conf.Settings & FORK) {
192+
// fill data using sub processes.
193+
std::string MainExe = sys::fs::getMainExecutable(argv0, &CASPath);
194+
std::vector<std::string> Args = {MainExe, "--gen-data",
195+
"--cas-path=" + CASPath};
196+
Conf.appendCommandLineOpts(Args);
197+
198+
std::vector<StringRef> Cmd;
199+
for_each(Args, [&Cmd](const std::string &Arg) { Cmd.push_back(Arg); });
200+
201+
std::vector<sys::ProcessInfo> Subprocesses;
202+
for (int I = 0; I < Conf.NumShards; ++I) {
203+
auto SP = sys::ExecuteNoWait(MainExe, Cmd, std::nullopt);
204+
if (SP.Pid != 0)
205+
Subprocesses.push_back(SP);
206+
}
207+
208+
if (Conf.Settings & CHECK_TERMINATION) {
209+
for_each(Subprocesses, [](auto &P) {
210+
// Wait 1 second and killed the process.
211+
auto WP = sys::Wait(P, 1);
212+
if (WP.ReturnCode)
213+
llvm::errs() << "subprocess killed successfully\n";
214+
});
215+
} else {
216+
for_each(Subprocesses, [](auto &P) { sys::Wait(P, std::nullopt); });
217+
}
218+
219+
} else {
220+
// in-process fill data.
221+
fillData(*CAS, Conf);
222+
}
223+
224+
// validate and prune in the end.
225+
ExitOnErr(CAS->validate(true));
226+
ExitOnErr(CAS->pruneStorageData());
227+
228+
return 0;
229+
}
230+
231+
int main(int argc, char **argv) {
232+
cl::ParseCommandLineOptions(argc, argv, "llvm-cas CAS tool\n");
233+
234+
if (GenData) {
235+
genData();
236+
return 0;
237+
}
238+
239+
auto Start = std::chrono::steady_clock::now();
240+
std::chrono::seconds Duration(Timeout);
241+
242+
while (std::chrono::steady_clock::now() - Start < Duration) {
243+
if (int Res = runOneTest(argv[0]))
244+
return Res;
245+
}
246+
247+
std::chrono::duration_cast<std::chrono::seconds>(
248+
std::chrono::steady_clock::now() - Start)
249+
.count();
250+
251+
return 0;
252+
}

0 commit comments

Comments
 (0)