1 //===- Delta.cpp - Delta Debugging Algorithm Implementation ---------------===//
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
7 //===----------------------------------------------------------------------===//
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.
13 //===----------------------------------------------------------------------===//
16 #include "ReducerWorkItem.h"
17 #include "TestRunner.h"
19 #include "llvm/ADT/STLExtras.h"
20 #include "llvm/Analysis/ModuleSummaryAnalysis.h"
21 #include "llvm/Analysis/ProfileSummaryInfo.h"
22 #include "llvm/Bitcode/BitcodeReader.h"
23 #include "llvm/Bitcode/BitcodeWriter.h"
24 #include "llvm/CodeGen/MachineFunction.h"
25 #include "llvm/IR/Module.h"
26 #include "llvm/IR/Verifier.h"
27 #include "llvm/MC/TargetRegistry.h"
28 #include "llvm/Support/CommandLine.h"
29 #include "llvm/Support/MemoryBufferRef.h"
30 #include "llvm/Support/ThreadPool.h"
36 extern cl::OptionCategory LLVMReduceOptions
;
38 static cl::opt
<bool> AbortOnInvalidReduction(
39 "abort-on-invalid-reduction",
40 cl::desc("Abort if any reduction results in invalid IR"),
41 cl::cat(LLVMReduceOptions
));
43 static cl::opt
<unsigned int> StartingGranularityLevel(
44 "starting-granularity-level",
45 cl::desc("Number of times to divide chunks prior to first test"),
46 cl::cat(LLVMReduceOptions
));
48 #ifdef LLVM_ENABLE_THREADS
49 static cl::opt
<unsigned> NumJobs(
51 cl::desc("Maximum number of threads to use to process chunks. Set to 1 to "
52 "disable parallelism."),
53 cl::init(1), cl::cat(LLVMReduceOptions
));
58 /// Splits Chunks in half and prints them.
59 /// If unable to split (when chunk size is 1) returns false.
60 static bool increaseGranularity(std::vector
<Chunk
> &Chunks
) {
62 errs() << "Increasing granularity...";
63 std::vector
<Chunk
> NewChunks
;
64 bool SplitAny
= false;
66 for (Chunk C
: Chunks
) {
67 if (C
.End
- C
.Begin
== 0)
68 NewChunks
.push_back(C
);
70 int Half
= (C
.Begin
+ C
.End
) / 2;
71 NewChunks
.push_back({C
.Begin
, Half
});
72 NewChunks
.push_back({Half
+ 1, C
.End
});
79 errs() << "Success! " << NewChunks
.size() << " New Chunks:\n";
80 for (auto C
: Chunks
) {
90 // Check if \p ChunkToCheckForUninterestingness is interesting. Returns the
91 // modified module if the chunk resulted in a reduction.
92 static std::unique_ptr
<ReducerWorkItem
>
93 CheckChunk(const Chunk ChunkToCheckForUninterestingness
,
94 std::unique_ptr
<ReducerWorkItem
> Clone
, const TestRunner
&Test
,
95 ReductionFunc ExtractChunksFromModule
,
96 const DenseSet
<Chunk
> &UninterestingChunks
,
97 const std::vector
<Chunk
> &ChunksStillConsideredInteresting
) {
98 // Take all of ChunksStillConsideredInteresting chunks, except those we've
99 // already deemed uninteresting (UninterestingChunks) but didn't remove
100 // from ChunksStillConsideredInteresting yet, and additionally ignore
101 // ChunkToCheckForUninterestingness chunk.
102 std::vector
<Chunk
> CurrentChunks
;
103 CurrentChunks
.reserve(ChunksStillConsideredInteresting
.size() -
104 UninterestingChunks
.size() - 1);
105 copy_if(ChunksStillConsideredInteresting
, std::back_inserter(CurrentChunks
),
106 [&](const Chunk
&C
) {
107 return C
!= ChunkToCheckForUninterestingness
&&
108 !UninterestingChunks
.count(C
);
111 // Generate Module with only Targets inside Current Chunks
112 Oracle
O(CurrentChunks
);
113 ExtractChunksFromModule(O
, *Clone
);
115 // Some reductions may result in invalid IR. Skip such reductions.
116 if (Clone
->verify(&errs())) {
117 if (AbortOnInvalidReduction
) {
118 errs() << "Invalid reduction, aborting.\n";
119 Clone
->print(errs());
123 errs() << " **** WARNING | reduction resulted in invalid module, "
130 errs() << "Ignoring: ";
131 ChunkToCheckForUninterestingness
.print();
132 for (const Chunk
&C
: UninterestingChunks
)
137 if (!Clone
->isReduced(Test
)) {
138 // Program became non-reduced, so this chunk appears to be interesting.
146 static SmallString
<0> ProcessChunkFromSerializedBitcode(
147 const Chunk ChunkToCheckForUninterestingness
, const TestRunner
&Test
,
148 ReductionFunc ExtractChunksFromModule
,
149 const DenseSet
<Chunk
> &UninterestingChunks
,
150 ArrayRef
<Chunk
> ChunksStillConsideredInteresting
, StringRef OriginalBC
,
151 std::atomic
<bool> &AnyReduced
) {
153 auto CloneMMM
= std::make_unique
<ReducerWorkItem
>();
154 MemoryBufferRef
Data(OriginalBC
, "<bc file>");
155 CloneMMM
->readBitcode(Data
, Ctx
, Test
.getToolName());
157 SmallString
<0> Result
;
158 if (std::unique_ptr
<ReducerWorkItem
> ChunkResult
=
159 CheckChunk(ChunkToCheckForUninterestingness
, std::move(CloneMMM
),
160 Test
, ExtractChunksFromModule
, UninterestingChunks
,
161 ChunksStillConsideredInteresting
)) {
162 raw_svector_ostream
BCOS(Result
);
163 ChunkResult
->writeBitcode(BCOS
);
164 // Communicate that the task reduced a chunk.
170 using SharedTaskQueue
= std::deque
<std::shared_future
<SmallString
<0>>>;
172 /// Runs the Delta Debugging algorithm, splits the code into chunks and
173 /// reduces the amount of chunks that are considered interesting by the
174 /// given test. The number of chunks is determined by a preliminary run of the
175 /// reduction pass where no change must be made to the module.
176 void llvm::runDeltaPass(TestRunner
&Test
, ReductionFunc ExtractChunksFromModule
,
178 assert(!Test
.getProgram().verify(&errs()) &&
179 "input module is broken before making changes");
180 errs() << "*** " << Message
<< "...\n";
184 // Count the number of chunks by counting the number of calls to
185 // Oracle::shouldKeep() but always returning true so no changes are
187 std::vector
<Chunk
> AllChunks
= {{0, INT_MAX
}};
188 Oracle
Counter(AllChunks
);
189 ExtractChunksFromModule(Counter
, Test
.getProgram());
190 Targets
= Counter
.count();
192 assert(!Test
.getProgram().verify(&errs()) &&
193 "input module is broken after counting chunks");
194 assert(Test
.getProgram().isReduced(Test
) &&
195 "input module no longer interesting after counting chunks");
198 // Make sure that the number of chunks does not change as we reduce.
199 std::vector
<Chunk
> NoChunks
= {{0, INT_MAX
}};
200 Oracle
NoChunksCounter(NoChunks
);
201 std::unique_ptr
<ReducerWorkItem
> Clone
=
202 Test
.getProgram().clone(Test
.getTargetMachine());
203 ExtractChunksFromModule(NoChunksCounter
, *Clone
);
204 assert(Targets
== NoChunksCounter
.count() &&
205 "number of chunks changes when reducing");
210 errs() << "\nNothing to reduce\n";
211 errs() << "----------------------------\n";
215 std::vector
<Chunk
> ChunksStillConsideredInteresting
= {{0, Targets
- 1}};
216 std::unique_ptr
<ReducerWorkItem
> ReducedProgram
;
218 for (unsigned int Level
= 0; Level
< StartingGranularityLevel
; Level
++) {
219 increaseGranularity(ChunksStillConsideredInteresting
);
222 std::atomic
<bool> AnyReduced
;
223 std::unique_ptr
<ThreadPool
> ChunkThreadPoolPtr
;
226 std::make_unique
<ThreadPool
>(hardware_concurrency(NumJobs
));
228 bool FoundAtLeastOneNewUninterestingChunkWithCurrentGranularity
;
230 FoundAtLeastOneNewUninterestingChunkWithCurrentGranularity
= false;
232 DenseSet
<Chunk
> UninterestingChunks
;
234 // When running with more than one thread, serialize the original bitcode
236 SmallString
<0> OriginalBC
;
238 raw_svector_ostream
BCOS(OriginalBC
);
239 Test
.getProgram().writeBitcode(BCOS
);
242 SharedTaskQueue TaskQueue
;
243 for (auto I
= ChunksStillConsideredInteresting
.rbegin(),
244 E
= ChunksStillConsideredInteresting
.rend();
246 std::unique_ptr
<ReducerWorkItem
> Result
= nullptr;
247 unsigned WorkLeft
= std::distance(I
, E
);
249 // Run in parallel mode, if the user requested more than one thread and
250 // there are at least a few chunks to process.
251 if (NumJobs
> 1 && WorkLeft
> 1) {
252 unsigned NumInitialTasks
= std::min(WorkLeft
, unsigned(NumJobs
));
253 unsigned NumChunksProcessed
= 0;
255 ThreadPool
&ChunkThreadPool
= *ChunkThreadPoolPtr
;
256 assert(TaskQueue
.empty());
259 // Queue jobs to process NumInitialTasks chunks in parallel using
260 // ChunkThreadPool. When the tasks are added to the pool, parse the
261 // original module from OriginalBC with a fresh LLVMContext object. This
262 // ensures that the cloned module of each task uses an independent
263 // LLVMContext object. If a task reduces the input, serialize the result
264 // back in the corresponding Result element.
265 for (unsigned J
= 0; J
< NumInitialTasks
; ++J
) {
266 Chunk ChunkToCheck
= *(I
+ J
);
267 TaskQueue
.emplace_back(ChunkThreadPool
.async(
268 ProcessChunkFromSerializedBitcode
, ChunkToCheck
, std::ref(Test
),
269 ExtractChunksFromModule
, UninterestingChunks
,
270 ChunksStillConsideredInteresting
, OriginalBC
,
271 std::ref(AnyReduced
)));
274 // Start processing results of the queued tasks. We wait for the first
275 // task in the queue to finish. If it reduced a chunk, we parse the
276 // result and exit the loop.
277 // Otherwise we will try to schedule a new task, if
278 // * no other pending job reduced a chunk and
279 // * we have not reached the end of the chunk.
280 while (!TaskQueue
.empty()) {
281 auto &Future
= TaskQueue
.front();
284 NumChunksProcessed
++;
285 SmallString
<0> Res
= Future
.get();
286 TaskQueue
.pop_front();
288 unsigned NumScheduledTasks
= NumChunksProcessed
+ TaskQueue
.size();
289 if (!AnyReduced
&& I
+ NumScheduledTasks
!= E
) {
290 Chunk ChunkToCheck
= *(I
+ NumScheduledTasks
);
291 TaskQueue
.emplace_back(ChunkThreadPool
.async(
292 ProcessChunkFromSerializedBitcode
, ChunkToCheck
,
293 std::ref(Test
), ExtractChunksFromModule
, UninterestingChunks
,
294 ChunksStillConsideredInteresting
, OriginalBC
,
295 std::ref(AnyReduced
)));
300 Result
= std::make_unique
<ReducerWorkItem
>();
301 MemoryBufferRef
Data(StringRef(Res
), "<bc file>");
302 Result
->readBitcode(Data
, Test
.getProgram().M
->getContext(),
307 // If we broke out of the loop, we still need to wait for everything to
308 // avoid race access to the chunk set.
310 // TODO: Create a way to kill remaining items we're ignoring; they could
312 ChunkThreadPoolPtr
->wait();
315 // Forward I to the last chunk processed in parallel.
316 I
+= NumChunksProcessed
- 1;
319 CheckChunk(*I
, Test
.getProgram().clone(Test
.getTargetMachine()),
320 Test
, ExtractChunksFromModule
, UninterestingChunks
,
321 ChunksStillConsideredInteresting
);
327 const Chunk ChunkToCheckForUninterestingness
= *I
;
328 FoundAtLeastOneNewUninterestingChunkWithCurrentGranularity
= true;
329 UninterestingChunks
.insert(ChunkToCheckForUninterestingness
);
330 ReducedProgram
= std::move(Result
);
332 // Delete uninteresting chunks
333 erase_if(ChunksStillConsideredInteresting
,
334 [&UninterestingChunks
](const Chunk
&C
) {
335 return UninterestingChunks
.count(C
);
337 } while (!ChunksStillConsideredInteresting
.empty() &&
338 (FoundAtLeastOneNewUninterestingChunkWithCurrentGranularity
||
339 increaseGranularity(ChunksStillConsideredInteresting
)));
341 // If we reduced the testcase replace it
342 if (ReducedProgram
) {
343 Test
.setProgram(std::move(ReducedProgram
));
344 // FIXME: Report meaningful progress info
345 Test
.writeOutput(" **** SUCCESS | Saved new best reduction to ");
348 errs() << "Couldn't increase anymore.\n";
349 errs() << "----------------------------\n";