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