1 //===- Delta.cpp - Delta Debugging Algorithm Implementation ---------------===// 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 // 9 // This file contains the implementation for the Delta Debugging Algorithm: 10 // it splits a given set of Targets (i.e. Functions, Instructions, BBs, etc.) 11 // into chunks and tries to reduce the number chunks that are interesting. 12 // 13 //===----------------------------------------------------------------------===// 14 15 #include "Delta.h" 16 #include "ReducerWorkItem.h" 17 #include "llvm/ADT/STLExtras.h" 18 #include "llvm/Bitcode/BitcodeReader.h" 19 #include "llvm/Bitcode/BitcodeWriter.h" 20 #include "llvm/IR/Verifier.h" 21 #include "llvm/Support/CommandLine.h" 22 #include "llvm/Support/ThreadPool.h" 23 #include "llvm/Support/ToolOutputFile.h" 24 #include <fstream> 25 #include <set> 26 27 using namespace llvm; 28 29 extern cl::OptionCategory LLVMReduceOptions; 30 31 static cl::opt<bool> AbortOnInvalidReduction( 32 "abort-on-invalid-reduction", 33 cl::desc("Abort if any reduction results in invalid IR"), 34 cl::cat(LLVMReduceOptions)); 35 36 static cl::opt<unsigned int> StartingGranularityLevel( 37 "starting-granularity-level", 38 cl::desc("Number of times to divide chunks prior to first test"), 39 cl::cat(LLVMReduceOptions)); 40 41 static cl::opt<bool> TmpFilesAsBitcode( 42 "write-tmp-files-as-bitcode", 43 cl::desc("Write temporary files as bitcode, instead of textual IR"), 44 cl::init(false), cl::cat(LLVMReduceOptions)); 45 46 #ifdef LLVM_ENABLE_THREADS 47 static cl::opt<unsigned> NumJobs( 48 "j", 49 cl::desc("Maximum number of threads to use to process chunks. Set to 1 to " 50 "disables parallelism."), 51 cl::init(1), cl::cat(LLVMReduceOptions)); 52 #else 53 unsigned NumJobs = 1; 54 #endif 55 56 void writeOutput(ReducerWorkItem &M, llvm::StringRef Message); 57 58 bool isReduced(ReducerWorkItem &M, TestRunner &Test, 59 SmallString<128> &CurrentFilepath) { 60 // Write ReducerWorkItem to tmp file 61 int FD; 62 std::error_code EC = sys::fs::createTemporaryFile( 63 "llvm-reduce", M.isMIR() ? "mir" : (TmpFilesAsBitcode ? "bc" : "ll"), FD, 64 CurrentFilepath); 65 if (EC) { 66 errs() << "Error making unique filename: " << EC.message() << "!\n"; 67 exit(1); 68 } 69 70 if (TmpFilesAsBitcode) { 71 llvm::raw_fd_ostream OutStream(FD, true); 72 WriteBitcodeToFile(M, OutStream); 73 OutStream.close(); 74 if (OutStream.has_error()) { 75 errs() << "Error emitting bitcode to file '" << CurrentFilepath << "'!\n"; 76 sys::fs::remove(CurrentFilepath); 77 exit(1); 78 } 79 bool Res = Test.run(CurrentFilepath); 80 sys::fs::remove(CurrentFilepath); 81 return Res; 82 } 83 ToolOutputFile Out(CurrentFilepath, FD); 84 M.print(Out.os(), /*AnnotationWriter=*/nullptr); 85 Out.os().close(); 86 if (Out.os().has_error()) { 87 errs() << "Error emitting bitcode to file '" << CurrentFilepath << "'!\n"; 88 exit(1); 89 } 90 91 // Current Chunks aren't interesting 92 return Test.run(CurrentFilepath); 93 } 94 95 /// Counts the amount of lines for a given file 96 static int getLines(StringRef Filepath) { 97 int Lines = 0; 98 std::string CurrLine; 99 std::ifstream FileStream{std::string(Filepath)}; 100 101 while (std::getline(FileStream, CurrLine)) 102 ++Lines; 103 104 return Lines; 105 } 106 107 /// Splits Chunks in half and prints them. 108 /// If unable to split (when chunk size is 1) returns false. 109 static bool increaseGranularity(std::vector<Chunk> &Chunks) { 110 errs() << "Increasing granularity..."; 111 std::vector<Chunk> NewChunks; 112 bool SplitOne = false; 113 114 for (auto &C : Chunks) { 115 if (C.End - C.Begin == 0) 116 NewChunks.push_back(C); 117 else { 118 int Half = (C.Begin + C.End) / 2; 119 NewChunks.push_back({C.Begin, Half}); 120 NewChunks.push_back({Half + 1, C.End}); 121 SplitOne = true; 122 } 123 } 124 if (SplitOne) { 125 Chunks = NewChunks; 126 errs() << "Success! New Chunks:\n"; 127 for (auto C : Chunks) { 128 errs() << '\t'; 129 C.print(); 130 errs() << '\n'; 131 } 132 } 133 return SplitOne; 134 } 135 // Check if \p ChunkToCheckForUninterestingness is interesting. Returns the 136 // modified module if the chunk resulted in a reduction. 137 template <typename T> 138 static std::unique_ptr<ReducerWorkItem> 139 CheckChunk(Chunk &ChunkToCheckForUninterestingness, 140 std::unique_ptr<ReducerWorkItem> Clone, TestRunner &Test, 141 function_ref<void(Oracle &, T &)> ExtractChunksFromModule, 142 std::set<Chunk> &UninterestingChunks, 143 std::vector<Chunk> &ChunksStillConsideredInteresting) { 144 // Take all of ChunksStillConsideredInteresting chunks, except those we've 145 // already deemed uninteresting (UninterestingChunks) but didn't remove 146 // from ChunksStillConsideredInteresting yet, and additionally ignore 147 // ChunkToCheckForUninterestingness chunk. 148 std::vector<Chunk> CurrentChunks; 149 CurrentChunks.reserve(ChunksStillConsideredInteresting.size() - 150 UninterestingChunks.size() - 1); 151 copy_if(ChunksStillConsideredInteresting, std::back_inserter(CurrentChunks), 152 [&](const Chunk &C) { 153 return !UninterestingChunks.count(C) && 154 C != ChunkToCheckForUninterestingness; 155 }); 156 157 // Generate Module with only Targets inside Current Chunks 158 Oracle O(CurrentChunks); 159 ExtractChunksFromModule(O, *Clone); 160 161 // Some reductions may result in invalid IR. Skip such reductions. 162 if (verifyReducerWorkItem(*Clone, &errs())) { 163 if (AbortOnInvalidReduction) { 164 errs() << "Invalid reduction\n"; 165 exit(1); 166 } 167 errs() << " **** WARNING | reduction resulted in invalid module, " 168 "skipping\n"; 169 return nullptr; 170 } 171 172 errs() << "Ignoring: "; 173 ChunkToCheckForUninterestingness.print(); 174 for (const Chunk &C : UninterestingChunks) 175 C.print(); 176 177 SmallString<128> CurrentFilepath; 178 if (!isReduced(*Clone, Test, CurrentFilepath)) { 179 // Program became non-reduced, so this chunk appears to be interesting. 180 errs() << "\n"; 181 return nullptr; 182 } 183 return Clone; 184 } 185 186 template <typename T> 187 SmallString<0> ProcessChunkFromSerializedBitcode( 188 Chunk &ChunkToCheckForUninterestingness, TestRunner &Test, 189 function_ref<void(Oracle &, T &)> ExtractChunksFromModule, 190 std::set<Chunk> &UninterestingChunks, 191 std::vector<Chunk> &ChunksStillConsideredInteresting, 192 SmallString<0> &OriginalBC, std::atomic<bool> &AnyReduced) { 193 LLVMContext Ctx; 194 Expected<std::unique_ptr<Module>> MOrErr = parseBitcodeFile( 195 MemoryBufferRef(StringRef(OriginalBC.data(), OriginalBC.size()), 196 "<llvm-reduce tmp module>"), 197 Ctx); 198 if (!MOrErr) 199 report_fatal_error("Failed to read bitcode"); 200 auto CloneMMM = std::make_unique<ReducerWorkItem>(); 201 CloneMMM->M = std::move(MOrErr.get()); 202 203 SmallString<0> Result; 204 if (std::unique_ptr<ReducerWorkItem> ChunkResult = 205 CheckChunk(ChunkToCheckForUninterestingness, std::move(CloneMMM), 206 Test, ExtractChunksFromModule, UninterestingChunks, 207 ChunksStillConsideredInteresting)) { 208 raw_svector_ostream BCOS(Result); 209 WriteBitcodeToFile(*ChunkResult->M, BCOS); 210 // Communicate that the task reduced a chunk. 211 AnyReduced = true; 212 } 213 return Result; 214 } 215 216 /// Runs the Delta Debugging algorithm, splits the code into chunks and 217 /// reduces the amount of chunks that are considered interesting by the 218 /// given test. The number of chunks is determined by a preliminary run of the 219 /// reduction pass where no change must be made to the module. 220 template <typename T> 221 void runDeltaPassInt( 222 TestRunner &Test, 223 function_ref<void(Oracle &, T &)> ExtractChunksFromModule) { 224 assert(!verifyReducerWorkItem(Test.getProgram(), &errs()) && 225 "input module is broken before making changes"); 226 227 SmallString<128> CurrentFilepath; 228 if (!isReduced(Test.getProgram(), Test, CurrentFilepath)) { 229 errs() << "\nInput isn't interesting! Verify interesting-ness test\n"; 230 exit(1); 231 } 232 233 int Targets; 234 { 235 // Count the number of chunks by counting the number of calls to 236 // Oracle::shouldKeep() but always returning true so no changes are 237 // made. 238 std::vector<Chunk> AllChunks = {{0, INT_MAX}}; 239 Oracle Counter(AllChunks); 240 ExtractChunksFromModule(Counter, Test.getProgram()); 241 Targets = Counter.count(); 242 243 assert(!verifyReducerWorkItem(Test.getProgram(), &errs()) && 244 "input module is broken after counting chunks"); 245 assert(isReduced(Test.getProgram(), Test, CurrentFilepath) && 246 "input module no longer interesting after counting chunks"); 247 248 #ifndef NDEBUG 249 // Make sure that the number of chunks does not change as we reduce. 250 std::vector<Chunk> NoChunks; 251 Oracle NoChunksCounter(NoChunks); 252 std::unique_ptr<ReducerWorkItem> Clone = 253 cloneReducerWorkItem(Test.getProgram()); 254 ExtractChunksFromModule(NoChunksCounter, *Clone); 255 assert(Targets == NoChunksCounter.count() && 256 "number of chunks changes when reducing"); 257 #endif 258 } 259 if (!Targets) { 260 errs() << "\nNothing to reduce\n"; 261 return; 262 } 263 264 std::vector<Chunk> ChunksStillConsideredInteresting = {{0, Targets - 1}}; 265 std::unique_ptr<ReducerWorkItem> ReducedProgram; 266 267 for (unsigned int Level = 0; Level < StartingGranularityLevel; Level++) { 268 increaseGranularity(ChunksStillConsideredInteresting); 269 } 270 271 std::atomic<bool> AnyReduced; 272 std::unique_ptr<ThreadPool> ChunkThreadPoolPtr; 273 if (NumJobs > 1) 274 ChunkThreadPoolPtr = 275 std::make_unique<ThreadPool>(hardware_concurrency(NumJobs)); 276 277 bool FoundAtLeastOneNewUninterestingChunkWithCurrentGranularity; 278 do { 279 FoundAtLeastOneNewUninterestingChunkWithCurrentGranularity = false; 280 281 std::set<Chunk> UninterestingChunks; 282 283 // When running with more than one thread, serialize the original bitcode 284 // to OriginalBC. 285 SmallString<0> OriginalBC; 286 if (NumJobs > 1) { 287 raw_svector_ostream BCOS(OriginalBC); 288 WriteBitcodeToFile(*Test.getProgram().M, BCOS); 289 } 290 291 std::deque<std::shared_future<SmallString<0>>> TaskQueue; 292 for (auto I = ChunksStillConsideredInteresting.rbegin(), 293 E = ChunksStillConsideredInteresting.rend(); 294 I != E; ++I) { 295 std::unique_ptr<ReducerWorkItem> Result = nullptr; 296 unsigned WorkLeft = std::distance(I, E); 297 298 // Run in parallel mode, if the user requested more than one thread and 299 // there are at least a few chunks to process. 300 if (NumJobs > 1 && WorkLeft > 1) { 301 unsigned NumInitialTasks = std::min(WorkLeft, unsigned(NumJobs)); 302 unsigned NumChunksProcessed = 0; 303 304 ThreadPool &ChunkThreadPool = *ChunkThreadPoolPtr; 305 TaskQueue.clear(); 306 307 AnyReduced = false; 308 // Queue jobs to process NumInitialTasks chunks in parallel using 309 // ChunkThreadPool. When the tasks are added to the pool, parse the 310 // original module from OriginalBC with a fresh LLVMContext object. This 311 // ensures that the cloned module of each task uses an independent 312 // LLVMContext object. If a task reduces the input, serialize the result 313 // back in the corresponding Result element. 314 for (unsigned J = 0; J < NumInitialTasks; ++J) { 315 TaskQueue.emplace_back(ChunkThreadPool.async( 316 [J, I, &Test, &ExtractChunksFromModule, &UninterestingChunks, 317 &ChunksStillConsideredInteresting, &OriginalBC, &AnyReduced]() { 318 return ProcessChunkFromSerializedBitcode( 319 *(I + J), Test, ExtractChunksFromModule, 320 UninterestingChunks, ChunksStillConsideredInteresting, 321 OriginalBC, AnyReduced); 322 })); 323 } 324 325 // Start processing results of the queued tasks. We wait for the first 326 // task in the queue to finish. If it reduced a chunk, we parse the 327 // result and exit the loop. 328 // Otherwise we will try to schedule a new task, if 329 // * no other pending job reduced a chunk and 330 // * we have not reached the end of the chunk. 331 while (!TaskQueue.empty()) { 332 auto &Future = TaskQueue.front(); 333 Future.wait(); 334 335 NumChunksProcessed++; 336 SmallString<0> Res = Future.get(); 337 TaskQueue.pop_front(); 338 if (Res.empty()) { 339 unsigned NumScheduledTasks = NumChunksProcessed + TaskQueue.size(); 340 if (!AnyReduced && I + NumScheduledTasks != E) { 341 Chunk &ChunkToCheck = *(I + NumScheduledTasks); 342 TaskQueue.emplace_back(ChunkThreadPool.async( 343 [&Test, &ExtractChunksFromModule, &UninterestingChunks, 344 &ChunksStillConsideredInteresting, &OriginalBC, 345 &ChunkToCheck, &AnyReduced]() { 346 return ProcessChunkFromSerializedBitcode( 347 ChunkToCheck, Test, ExtractChunksFromModule, 348 UninterestingChunks, ChunksStillConsideredInteresting, 349 OriginalBC, AnyReduced); 350 })); 351 } 352 continue; 353 } 354 355 Expected<std::unique_ptr<Module>> MOrErr = parseBitcodeFile( 356 MemoryBufferRef(StringRef(Res.data(), Res.size()), 357 "<llvm-reduce tmp module>"), 358 Test.getProgram().M->getContext()); 359 if (!MOrErr) 360 report_fatal_error("Failed to read bitcode"); 361 Result = std::make_unique<ReducerWorkItem>(); 362 Result->M = std::move(MOrErr.get()); 363 break; 364 } 365 // Forward I to the last chunk processed in parallel. 366 I += NumChunksProcessed - 1; 367 } else { 368 Result = CheckChunk(*I, cloneReducerWorkItem(Test.getProgram()), Test, 369 ExtractChunksFromModule, UninterestingChunks, 370 ChunksStillConsideredInteresting); 371 } 372 373 if (!Result) 374 continue; 375 376 Chunk &ChunkToCheckForUninterestingness = *I; 377 FoundAtLeastOneNewUninterestingChunkWithCurrentGranularity = true; 378 UninterestingChunks.insert(ChunkToCheckForUninterestingness); 379 ReducedProgram = std::move(Result); 380 errs() << " **** SUCCESS | lines: " << getLines(CurrentFilepath) << "\n"; 381 writeOutput(*ReducedProgram, "Saved new best reduction to "); 382 } 383 // Delete uninteresting chunks 384 erase_if(ChunksStillConsideredInteresting, 385 [&UninterestingChunks](const Chunk &C) { 386 return UninterestingChunks.count(C); 387 }); 388 } while (!ChunksStillConsideredInteresting.empty() && 389 (FoundAtLeastOneNewUninterestingChunkWithCurrentGranularity || 390 increaseGranularity(ChunksStillConsideredInteresting))); 391 392 // If we reduced the testcase replace it 393 if (ReducedProgram) 394 Test.setProgram(std::move(ReducedProgram)); 395 errs() << "Couldn't increase anymore.\n"; 396 } 397 398 void llvm::runDeltaPass( 399 TestRunner &Test, 400 function_ref<void(Oracle &, Module &)> ExtractChunksFromModule) { 401 runDeltaPassInt<Module>(Test, ExtractChunksFromModule); 402 } 403 404 void llvm::runDeltaPass( 405 TestRunner &Test, 406 function_ref<void(Oracle &, MachineFunction &)> ExtractChunksFromModule) { 407 runDeltaPassInt<MachineFunction>(Test, ExtractChunksFromModule); 408 } 409