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 }