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 }