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