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 328 this.mcCabeResult = new McCabeResult; 329 // remove this in newer versions than 2.071.1 where nullableRef is implemented. 330 //import std.typecons : nullableRef; 331 //this.mcCabe = McCabe(nullableRef(&this.mcCabeResult)); 332 () @trusted { 333 import std.typecons : NullableRef; 334 335 this.mcCabe = McCabe(NullableRef!McCabeResult(&this.mcCabeResult)); 336 }(); 337 } 338 339 void register(TUVisitor v) { 340 if (doMcCabe) { 341 v.onFunctionDecl ~= &mcCabe.analyze!FunctionDecl; 342 v.onCxxMethod ~= &mcCabe.analyze!CxxMethod; 343 v.onConstructor ~= &mcCabe.analyze!Constructor; 344 v.onDestructor ~= &mcCabe.analyze!Destructor; 345 v.onConversionFunction ~= &mcCabe.analyze!ConversionFunction; 346 v.onFunctionTemplate ~= &mcCabe.analyze!FunctionTemplate; 347 } 348 } 349 } 350 351 /** Results collected in the main thread. 352 */ 353 struct AnalyzeResults { 354 private { 355 AbsolutePath outdir; 356 357 McCabeResult mcCabe; 358 int mccabeThreshold; 359 Flag!"dumpMcCabe" dumpMcCabe; 360 361 Flag!"outputJson" json_; 362 Flag!"outputStdout" stdout_; 363 } 364 365 static auto make() { 366 return Builder(); 367 } 368 369 struct Builder { 370 private AbsolutePath outdir; 371 private bool dumpMcCabe; 372 private int mccabeThreshold; 373 private bool json_; 374 private bool stdout_; 375 376 auto mcCabe(bool dump_this, int threshold) { 377 this.dumpMcCabe = dump_this; 378 this.mccabeThreshold = threshold; 379 return this; 380 } 381 382 auto json(bool v) { 383 this.json_ = v; 384 return this; 385 } 386 387 auto stdout(bool v) { 388 this.stdout_ = v; 389 return this; 390 } 391 392 auto outputDirectory(string path) { 393 this.outdir = AbsolutePath(FileName(path)); 394 return this; 395 } 396 397 auto finalize() { 398 // dfmt off 399 return AnalyzeResults(outdir, 400 new McCabeResult, 401 mccabeThreshold, 402 cast(Flag!"dumpMcCabe") dumpMcCabe, 403 cast(Flag!"outputJson") json_, 404 cast(Flag!"outputStdout") stdout_, 405 ); 406 // dfmt on 407 } 408 } 409 410 void put(dextool.plugin.analyze.mccabe.Function f) @safe { 411 mcCabe.put(f); 412 } 413 414 void dumpResult() @safe { 415 import std.path : buildPath; 416 417 const string base = buildPath(outdir, "result_"); 418 419 if (dumpMcCabe) { 420 if (json_) 421 dextool.plugin.analyze.mccabe.resultToJson(FileName(base ~ "mccabe.json") 422 .AbsolutePath, mcCabe, mccabeThreshold); 423 if (stdout_) 424 dextool.plugin.analyze.mccabe.resultToStdout(mcCabe, mccabeThreshold); 425 } 426 } 427 }