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