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 logger = std.experimental.logger;
39 import std.algorithm : map, filter;
40 import std.array : array, empty;
41 import std.concurrency : Tid;
42 import std.typecons : Flag;
43 import std.range : enumerate, popFront, front;
44 
45 import dextool.compilation_db : limitOrAllRange, parse, prependFlags, addCompiler, replaceCompiler,
46     addSystemIncludes, fileRange, ParsedCompileCommand, CompileCommandDB, Compiler;
47 import dextool.type : ExitStatusType, Path, AbsolutePath;
48 
49 import dextool.plugin.analyze.visitor : TUVisitor;
50 import dextool.plugin.analyze.mccabe;
51 
52 /// The commands have a lifetime that persist throughout the whole analyze thus
53 /// just reuse them as-is.
54 immutable(ParsedCompileCommand) immReuse(ParsedCompileCommand v) @trusted {
55     return cast(immutable) v;
56 }
57 
58 ExitStatusType doAnalyze(AnalyzeBuilder analyze_builder, ref AnalyzeResults analyze_results, string[] in_cflags,
59         string[] in_files, CompileCommandDB compile_db, AbsolutePath restrictDir, int workerThreads) @safe {
60     import std.conv : to;
61     import dextool.compilation_db : defaultCompilerFilter;
62     import dextool.utility : prependDefaultFlags, PreferLang;
63 
64     {
65         import std.concurrency : setMaxMailboxSize, OnCrowding, thisTid;
66 
67         // safe in newer versions than 2.071.1
68         () @trusted { setMaxMailboxSize(thisTid, 1024, OnCrowding.block); }();
69     }
70 
71     auto compDbRange() {
72         if (compile_db.empty) {
73             return fileRange(in_files.map!(a => Path(a)).array, Compiler("/usr/bin/c++"));
74         }
75         return compile_db.fileRange;
76     }
77 
78     auto files = compDbRange.parse(defaultCompilerFilter).addSystemIncludes.prependFlags(
79             prependDefaultFlags(in_cflags, PreferLang.cpp)).enumerate.array;
80     const total_files = files.length;
81 
82     enum State {
83         none,
84         init,
85         putFile,
86         receive,
87         testFinish,
88         finish,
89         exit
90     }
91 
92     auto pool = new Pool(workerThreads);
93     State st;
94     debug State old;
95 
96     while (st != State.exit) {
97         debug if (st != old) {
98             logger.trace("doAnalyze: ", st.to!string());
99             old = st;
100         }
101 
102         final switch (st) {
103         case State.none:
104             st = State.init;
105             break;
106         case State.init:
107             st = State.testFinish;
108             break;
109         case State.putFile:
110             st = State.receive;
111             break;
112         case State.receive:
113             st = State.testFinish;
114             break;
115         case State.testFinish:
116             if (files.empty)
117                 st = State.finish;
118             else
119                 st = State.putFile;
120             break;
121         case State.finish:
122             assert(files.empty);
123             if (pool.empty)
124                 st = State.exit;
125             break;
126         case State.exit:
127             break;
128         }
129 
130         switch (st) {
131         case State.init:
132             for (; !files.empty; files.popFront) {
133                 if (!pool.run(&analyzeWorker, analyze_builder, files.front.index,
134                         total_files, files.front.value.immReuse, restrictDir)) {
135                     // reached CPU limit
136                     break;
137                 }
138             }
139             break;
140         case State.putFile:
141             if (pool.run(&analyzeWorker, analyze_builder,
142                     files.front.index, total_files, files.front.value.immReuse, restrictDir)) {
143                 // successfully spawned a worker
144                 files.popFront;
145             }
146             break;
147         case State.receive:
148             goto case;
149         case State.finish:
150             pool.receive((dextool.plugin.analyze.mccabe.Function a) {
151                 analyze_results.put(a);
152             });
153             break;
154         default:
155             break;
156         }
157     }
158 
159     return ExitStatusType.Ok;
160 }
161 
162 void analyzeWorker(Tid owner, AnalyzeBuilder analyze_builder, size_t file_idx,
163         size_t total_files, immutable ParsedCompileCommand pdata, AbsolutePath restrictDir) nothrow {
164     import std.concurrency : send;
165     import std.typecons : Yes;
166     import std.exception : collectException;
167     import dextool.utility : analyzeFile;
168     import libclang_ast.context : ClangContext;
169 
170     try {
171         logger.infof("File %d/%d ", file_idx + 1, total_files);
172     } catch (Exception e) {
173     }
174 
175     auto visitor = new TUVisitor(restrictDir);
176     AnalyzeCollection analyzers;
177     try {
178         analyzers = analyze_builder.finalize;
179         analyzers.register(visitor);
180         auto ctx = ClangContext(Yes.useInternalHeaders, Yes.prependParamSyntaxOnly);
181         if (analyzeFile(pdata.cmd.absoluteFile, pdata.flags.completeFlags,
182                 visitor, ctx) == ExitStatusType.Errors) {
183             logger.error("Unable to analyze: ", cast(string) pdata.cmd.absoluteFile);
184             return;
185         }
186     } catch (Exception e) {
187         collectException(logger.error(e.msg));
188     }
189 
190     foreach (f; analyzers.mcCabeResult.functions[]) {
191         try {
192             // assuming send is correctly implemented.
193             () @trusted { owner.send(f); }();
194         } catch (Exception e) {
195             collectException(logger.error("Unable to send to owner thread '%s': %s", owner, e.msg));
196         }
197     }
198 }
199 
200 class Pool {
201     import std.concurrency : Tid, thisTid;
202     import std.typecons : Nullable;
203 
204     Tid[] pool;
205     int workerThreads;
206 
207     this(int workerThreads) @safe {
208         import std.parallelism : totalCPUs;
209 
210         if (workerThreads <= 0) {
211             this.workerThreads = totalCPUs;
212         } else {
213             this.workerThreads = workerThreads;
214         }
215     }
216 
217     bool run(F, ARGS...)(F func, auto ref ARGS args) {
218         auto tid = makeWorker(func, args);
219         return !tid.isNull;
220     }
221 
222     /** Relay data in the mailbox back to the provided function.
223      *
224      * trusted: on the assumption that receiveTimeout is @safe _enough_.
225      * assuming `ops` is @safe.
226      *
227      * Returns: if data where received
228      */
229     bool receive(T)(T ops) @trusted {
230         import core.time;
231         import std.concurrency : LinkTerminated, receiveTimeout;
232 
233         bool got_any_data;
234 
235         try {
236             // empty the mailbox of data
237             for (;;) {
238                 auto got_data = receiveTimeout(msecs(0), ops);
239                 got_any_data = got_any_data || got_data;
240 
241                 if (!got_data) {
242                     break;
243                 }
244             }
245         } catch (LinkTerminated e) {
246             removeWorker(e.tid);
247         }
248 
249         return got_any_data;
250     }
251 
252     bool empty() @safe {
253         return pool.length == 0;
254     }
255 
256     void removeWorker(Tid tid) {
257         import std.array : array;
258 
259         pool = pool.filter!(a => tid != a).array();
260     }
261 
262     //TODO add attribute check of func so only @safe func can be used.
263     Nullable!Tid makeWorker(F, ARGS...)(F func, auto ref ARGS args) {
264         import std.concurrency : spawnLinked;
265 
266         typeof(return) rval;
267 
268         if (pool.length < workerThreads) {
269             // assuming that spawnLinked is of high quality. Assuming func is @safe.
270             rval = () @trusted { return spawnLinked(func, thisTid, args); }();
271             pool ~= rval.get;
272         }
273 
274         return rval;
275     }
276 }
277 
278 /** Hold the configuration parameters used to construct analyze collections.
279  *
280  * It is intended to be used to construct analyze collections in the worker
281  * threads.
282  *
283  * It is important that the member variables can be passed to a thread.
284  * This is easiest if they are of primitive types.
285  */
286 struct AnalyzeBuilder {
287     private {
288         Flag!"doMcCabeAnalyze" analyzeMcCabe;
289     }
290 
291     static auto make() {
292         return AnalyzeBuilder();
293     }
294 
295     auto mcCabe(bool do_this_analyze) {
296         analyzeMcCabe = cast(Flag!"doMcCabeAnalyze") do_this_analyze;
297         return this;
298     }
299 
300     auto finalize() {
301         return AnalyzeCollection(analyzeMcCabe);
302     }
303 }
304 
305 /** Analyzers used in worker threads to collect results.
306  *
307  * TODO reduce null checks. It is a sign of weakness in the design.
308  */
309 struct AnalyzeCollection {
310     import libclang_ast.ast.declaration;
311 
312     McCabeResult mcCabeResult;
313     private McCabe mcCabe;
314     private bool doMcCabe;
315 
316     this(Flag!"doMcCabeAnalyze" mccabe) {
317         doMcCabe = mccabe;
318         this.mcCabeResult = new McCabeResult;
319         this.mcCabe = new McCabe(mcCabeResult);
320     }
321 
322     void register(TUVisitor v) @trusted {
323         if (doMcCabe) {
324             v.onFunctionDecl ~= &mcCabe.analyze!FunctionDecl;
325             v.onCxxMethod ~= &mcCabe.analyze!CxxMethod;
326             v.onConstructor ~= &mcCabe.analyze!Constructor;
327             v.onDestructor ~= &mcCabe.analyze!Destructor;
328             v.onConversionFunction ~= &mcCabe.analyze!ConversionFunction;
329             v.onFunctionTemplate ~= &mcCabe.analyze!FunctionTemplate;
330         }
331     }
332 }
333 
334 /** Results collected in the main thread.
335  */
336 struct AnalyzeResults {
337     private {
338         AbsolutePath outdir;
339 
340         McCabeResult mcCabe;
341         int mccabeThreshold;
342         Flag!"dumpMcCabe" dumpMcCabe;
343 
344         Flag!"outputJson" json_;
345         Flag!"outputStdout" stdout_;
346     }
347 
348     static auto make() {
349         return Builder();
350     }
351 
352     struct Builder {
353         private AbsolutePath outdir;
354         private bool dumpMcCabe;
355         private int mccabeThreshold;
356         private bool json_;
357         private bool stdout_;
358 
359         auto mcCabe(bool dump_this, int threshold) {
360             this.dumpMcCabe = dump_this;
361             this.mccabeThreshold = threshold;
362             return this;
363         }
364 
365         auto json(bool v) {
366             this.json_ = v;
367             return this;
368         }
369 
370         auto stdout(bool v) {
371             this.stdout_ = v;
372             return this;
373         }
374 
375         auto outputDirectory(string path) {
376             this.outdir = AbsolutePath(Path(path));
377             return this;
378         }
379 
380         auto finalize() {
381             // dfmt off
382             return AnalyzeResults(outdir,
383                                   new McCabeResult,
384                                   mccabeThreshold,
385                                   cast(Flag!"dumpMcCabe") dumpMcCabe,
386                                   cast(Flag!"outputJson") json_,
387                                   cast(Flag!"outputStdout") stdout_,
388                                   );
389             // dfmt on
390         }
391     }
392 
393     void put(dextool.plugin.analyze.mccabe.Function f) @safe {
394         mcCabe.put(f);
395     }
396 
397     void dumpResult() @safe {
398         import std.path : buildPath;
399 
400         const string base = buildPath(outdir, "result_");
401 
402         if (dumpMcCabe) {
403             if (json_)
404                 dextool.plugin.analyze.mccabe.resultToJson(Path(base ~ "mccabe.json")
405                         .AbsolutePath, mcCabe, mccabeThreshold);
406             if (stdout_)
407                 dextool.plugin.analyze.mccabe.resultToStdout(mcCabe, mccabeThreshold);
408         }
409     }
410 }