1 /**
2 Copyright: Copyright (c) 2017, Joakim Brännström. All rights reserved.
3 License: MPL-2
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This Source Code Form is subject to the terms of the Mozilla Public License,
7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain
8 one at http://mozilla.org/MPL/2.0/.
9 
10 # Threading information flow
11 Main thread:
12     Get all the files to analyze.
13     Spawn worker threads.
14     Send to the worker thread the:
15         - file to analyze
16         - enough data to construct an analyzer collection
17     Collect the received analyze data from worker threads.
18     Wait until all files are analyzed and the last worker thread has sent back the data.
19     Dump the result according to the users config via CLI.
20 
21 Worker thread:
22     Connect a clang AST visitor with an analyzer constructed from the builder the main thread sent over.
23     Run the analyze pass.
24     Send back the analyze result to the main thread.
25 
26 # Design Assumptions
27  - No actual speed is gained if the working threads are higher than the core count.
28     Thus the number of workers are <= CPU count.
29  - The main thread that receive the data completely empty its mailbox.
30     This is not interleaved with spawning new workers.
31     This behavior will make it so that the worker threads sending data to the
32     main thread reach an equilibrium.
33     The number of worker threads are limited by the amount of data that the
34     main thread can receive.
35 */
36 module dextool.plugin.analyze.analyze;
37 
38 import std.concurrency : Tid;
39 import std.typecons : Flag;
40 import logger = std.experimental.logger;
41 
42 import dextool.compilation_db : SearchResult, CompileCommandDB;
43 import dextool.type : ExitStatusType, FileName, AbsolutePath;
44 
45 import dextool.plugin.analyze.visitor : TUVisitor;
46 import dextool.plugin.analyze.mccabe;
47 
48 immutable(SearchResult) idup(SearchResult v) @safe {
49     import std.algorithm : map;
50     import std.array : array;
51 
52     immutable(string)[] flags = v.cflags.map!(a => a.dup).array();
53 
54     return immutable SearchResult(flags, immutable AbsolutePath(v.absoluteFile));
55 }
56 
57 ExitStatusType doAnalyze(AnalyzeBuilder analyze_builder, ref AnalyzeResults analyze_results, string[] in_cflags,
58         string[] in_files, CompileCommandDB compile_db, AbsolutePath restrictDir, int workerThreads) @safe {
59     import std.conv : to;
60     import std.range : enumerate;
61     import dextool.compilation_db : defaultCompilerFilter, SearchResult;
62     import dextool.utility : prependDefaultFlags, PreferLang;
63     import dextool.plugin.analyze.filerange : AnalyzeFileRange;
64 
65     {
66         import std.concurrency : setMaxMailboxSize, OnCrowding, thisTid;
67 
68         // safe in newer versions than 2.071.1
69         () @trusted{ setMaxMailboxSize(thisTid, 1024, OnCrowding.block); }();
70     }
71 
72     const auto user_cflags = prependDefaultFlags(in_cflags, PreferLang.cpp);
73 
74     auto files = AnalyzeFileRange(compile_db, in_files, in_cflags, defaultCompilerFilter).enumerate;
75     const total_files = files.length;
76 
77     enum State {
78         none,
79         init,
80         putFile,
81         receive,
82         testFinish,
83         finish,
84         exit
85     }
86 
87     auto pool = new Pool(workerThreads);
88     State st;
89     debug State old;
90 
91     while (st != State.exit) {
92         debug if (st != old) {
93             logger.trace("doAnalyze: ", st.to!string());
94             old = st;
95         }
96 
97         final switch (st) {
98         case State.none:
99             st = State.init;
100             break;
101         case State.init:
102             st = State.testFinish;
103             break;
104         case State.putFile:
105             st = State.receive;
106             break;
107         case State.receive:
108             st = State.testFinish;
109             break;
110         case State.testFinish:
111             if (files.empty)
112                 st = State.finish;
113             else
114                 st = State.putFile;
115             break;
116         case State.finish:
117             assert(files.empty);
118             if (pool.empty)
119                 st = State.exit;
120             break;
121         case State.exit:
122             break;
123         }
124 
125         switch (st) {
126         case State.init:
127             import std.algorithm : filter;
128 
129             for (; !files.empty; files.popFront) {
130                 if (files.front.value.isNull) {
131                     logger.warning(
132                             "Skipping file because it is not possible to determine the compiler flags");
133                 } else if (!pool.run(&analyzeWorker, analyze_builder, files.front.index,
134                         total_files, files.front.value.get.idup, restrictDir)) {
135                     // reached CPU limit
136                     break;
137                 }
138             }
139             break;
140         case State.putFile:
141             if (files.front.value.isNull) {
142                 logger.warning(
143                         "Skipping file because it is not possible to determine the compiler flags");
144                 files.popFront;
145             } else {
146                 if (pool.run(&analyzeWorker, analyze_builder, files.front.index,
147                         total_files, files.front.value.get.idup, restrictDir)) {
148                     // successfully spawned a worker
149                     files.popFront;
150                 }
151             }
152             break;
153         case State.receive:
154             goto case;
155         case State.finish:
156             pool.receive((dextool.plugin.analyze.mccabe.Function a) {
157                 analyze_results.put(a);
158             });
159             break;
160         default:
161             break;
162         }
163     }
164 
165     return ExitStatusType.Ok;
166 }
167 
168 void analyzeWorker(Tid owner, AnalyzeBuilder analyze_builder, size_t file_idx,
169         size_t total_files, immutable SearchResult pdata, AbsolutePath restrictDir) nothrow {
170     import std.concurrency : send;
171     import std.typecons : Yes;
172     import std.exception : collectException;
173     import dextool.utility : analyzeFile;
174     import cpptooling.analyzer.clang.context : ClangContext;
175 
176     try {
177         logger.infof("File %d/%d ", file_idx + 1, total_files);
178     }
179     catch (Exception e) {
180     }
181 
182     auto visitor = new TUVisitor(restrictDir);
183     AnalyzeCollection analyzers;
184     try {
185         analyzers = analyze_builder.finalize;
186         analyzers.register(visitor);
187         auto ctx = ClangContext(Yes.useInternalHeaders, Yes.prependParamSyntaxOnly);
188         if (analyzeFile(pdata.absoluteFile, pdata.cflags, visitor, ctx) == ExitStatusType.Errors) {
189             logger.error("Unable to analyze: ", cast(string) pdata.absoluteFile);
190             return;
191         }
192     }
193     catch (Exception e) {
194         collectException(logger.error(e.msg));
195     }
196 
197     foreach (f; analyzers.mcCabeResult.functions[]) {
198         try {
199             // assuming send is correctly implemented.
200             () @trusted{ owner.send(f); }();
201         }
202         catch (Exception e) {
203             collectException(logger.error("Unable to send to owner thread '%s': %s", owner, e.msg));
204         }
205     }
206 }
207 
208 class Pool {
209     import std.concurrency : Tid, thisTid;
210     import std.typecons : Nullable;
211 
212     Tid[] pool;
213     int workerThreads;
214 
215     this(int workerThreads) @safe {
216         import std.parallelism : totalCPUs;
217 
218         if (workerThreads <= 0) {
219             this.workerThreads = totalCPUs;
220         } else {
221             this.workerThreads = workerThreads;
222         }
223     }
224 
225     bool run(F, ARGS...)(F func, auto ref ARGS args) {
226         auto tid = makeWorker(func, args);
227         return !tid.isNull;
228     }
229 
230     /** Relay data in the mailbox back to the provided function.
231      *
232      * trusted: on the assumption that receiveTimeout is @safe _enough_.
233      * assuming `ops` is @safe.
234      *
235      * Returns: if data where received
236      */
237     bool receive(T)(T ops) @trusted {
238         import core.time;
239         import std.concurrency : LinkTerminated, receiveTimeout;
240 
241         bool got_any_data;
242 
243         try {
244             // empty the mailbox of data
245             for (;;) {
246                 auto got_data = receiveTimeout(msecs(0), ops);
247                 got_any_data = got_any_data || got_data;
248 
249                 if (!got_data) {
250                     break;
251                 }
252             }
253         }
254         catch (LinkTerminated e) {
255             removeWorker(e.tid);
256         }
257 
258         return got_any_data;
259     }
260 
261     bool empty() @safe {
262         return pool.length == 0;
263     }
264 
265     void removeWorker(Tid tid) {
266         import std.array : array;
267         import std.algorithm : filter;
268 
269         pool = pool.filter!(a => tid != a).array();
270     }
271 
272     //TODO add attribute check of func so only @safe func can be used.
273     Nullable!Tid makeWorker(F, ARGS...)(F func, auto ref ARGS args) {
274         import std.concurrency : spawnLinked;
275 
276         typeof(return) rval;
277 
278         if (pool.length < workerThreads) {
279             // assuming that spawnLinked is of high quality. Assuming func is @safe.
280             rval = () @trusted{ return spawnLinked(func, thisTid, args); }();
281             pool ~= rval;
282         }
283 
284         return rval;
285     }
286 }
287 
288 /** Hold the configuration parameters used to construct analyze collections.
289  *
290  * It is intended to be used to construct analyze collections in the worker
291  * threads.
292  *
293  * It is important that the member variables can be passed to a thread.
294  * This is easiest if they are of primitive types.
295  */
296 struct AnalyzeBuilder {
297     private {
298         Flag!"doMcCabeAnalyze" analyzeMcCabe;
299     }
300 
301     static auto make() {
302         return AnalyzeBuilder();
303     }
304 
305     auto mcCabe(bool do_this_analyze) {
306         analyzeMcCabe = cast(Flag!"doMcCabeAnalyze") do_this_analyze;
307         return this;
308     }
309 
310     auto finalize() {
311         return AnalyzeCollection(analyzeMcCabe);
312     }
313 }
314 
315 /** Analyzers used in worker threads to collect results.
316  *
317  * TODO reduce null checks. It is a sign of weakness in the design.
318  */
319 struct AnalyzeCollection {
320     import cpptooling.analyzer.clang.ast.declaration;
321 
322     McCabeResult mcCabeResult;
323     private McCabe mcCabe;
324     private bool doMcCabe;
325 
326     this(Flag!"doMcCabeAnalyze" mccabe) {
327         doMcCabe = mccabe;
328 
329         this.mcCabeResult = new McCabeResult;
330         // remove this in newer versions than 2.071.1 where nullableRef is implemented.
331         //import std.typecons : nullableRef;
332         //this.mcCabe = McCabe(nullableRef(&this.mcCabeResult));
333         () @trusted{
334             import std.typecons : NullableRef;
335 
336             this.mcCabe = McCabe(NullableRef!McCabeResult(&this.mcCabeResult));
337         }();
338     }
339 
340     void register(TUVisitor v) {
341         if (doMcCabe) {
342             v.onFunctionDecl ~= &mcCabe.analyze!FunctionDecl;
343             v.onCxxMethod ~= &mcCabe.analyze!CxxMethod;
344             v.onConstructor ~= &mcCabe.analyze!Constructor;
345             v.onDestructor ~= &mcCabe.analyze!Destructor;
346             v.onConversionFunction ~= &mcCabe.analyze!ConversionFunction;
347             v.onFunctionTemplate ~= &mcCabe.analyze!FunctionTemplate;
348         }
349     }
350 }
351 
352 /** Results collected in the main thread.
353  */
354 struct AnalyzeResults {
355     private {
356         AbsolutePath outdir;
357 
358         McCabeResult mcCabe;
359         int mccabeThreshold;
360         Flag!"dumpMcCabe" dumpMcCabe;
361 
362         Flag!"outputJson" json_;
363         Flag!"outputStdout" stdout_;
364     }
365 
366     static auto make() {
367         return Builder();
368     }
369 
370     struct Builder {
371         private AbsolutePath outdir;
372         private bool dumpMcCabe;
373         private int mccabeThreshold;
374         private bool json_;
375         private bool stdout_;
376 
377         auto mcCabe(bool dump_this, int threshold) {
378             this.dumpMcCabe = dump_this;
379             this.mccabeThreshold = threshold;
380             return this;
381         }
382 
383         auto json(bool v) {
384             this.json_ = v;
385             return this;
386         }
387 
388         auto stdout(bool v) {
389             this.stdout_ = v;
390             return this;
391         }
392 
393         auto outputDirectory(string path) {
394             this.outdir = AbsolutePath(FileName(path));
395             return this;
396         }
397 
398         auto finalize() {
399             // dfmt off
400             return AnalyzeResults(outdir,
401                                   new McCabeResult,
402                                   mccabeThreshold,
403                                   cast(Flag!"dumpMcCabe") dumpMcCabe,
404                                   cast(Flag!"outputJson") json_,
405                                   cast(Flag!"outputStdout") stdout_,
406                                   );
407             // dfmt on
408         }
409     }
410 
411     void put(dextool.plugin.analyze.mccabe.Function f) @safe {
412         mcCabe.put(f);
413     }
414 
415     void dumpResult() @safe {
416         import std.path : buildPath;
417 
418         const string base = buildPath(outdir, "result_");
419 
420         if (dumpMcCabe) {
421             if (json_)
422                 dextool.plugin.analyze.mccabe.resultToJson(FileName(base ~ "mccabe.json")
423                         .AbsolutePath, mcCabe, mccabeThreshold);
424             if (stdout_)
425                 dextool.plugin.analyze.mccabe.resultToStdout(mcCabe, mccabeThreshold);
426         }
427     }
428 }