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 }