Skip to content

Commit 5fa9d6e

Browse files
committed
Massively performance of gathering coverage thanks to improved multi threading.
Previously both the GCC and LLVM generators simply partitioned the amount of files and functions into equally sized batches and then processed them. This lead to harsh thread under utilization however as not every file or function is equally sized. Instead, we now use a ConcurrentLinkedQueue which is a lockless FIFO datastructures which contains all functions that should be processed. Every thread simply pops off functions from the front of the queue for processing until the queue is empty. This leads to practically 100% thread utilization
1 parent 227754f commit 5fa9d6e

File tree

2 files changed

+127
-87
lines changed

2 files changed

+127
-87
lines changed

src/main/kotlin/net/zero9178/cov/data/GCCJSONCoverageGenerator.kt

Lines changed: 70 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import java.nio.file.LinkOption
3636
import java.nio.file.Paths
3737
import java.util.concurrent.CompletableFuture
3838
import java.util.concurrent.CompletionException
39-
import kotlin.math.ceil
39+
import java.util.concurrent.ConcurrentLinkedQueue
4040

4141
class GCCJSONCoverageGenerator(private val myGcov: String, private val myMajorVersion: Int) : CoverageGenerator {
4242

@@ -391,21 +391,56 @@ class GCCJSONCoverageGenerator(private val myGcov: String, private val myMajorVe
391391

392392
val files = try {
393393
if (!CoverageGeneratorSettings.getInstance().branchCoverageEnabled) {
394-
processFiles(filesToProcess, env, project)
394+
filesToProcess.mapNotNull {
395+
processFile(it, env, project)
396+
}
395397
} else {
396-
DumbService.getInstance(project).runReadActionInSmartMode<List<CoverageFileData>> {
397-
filesToProcess.chunked(ceil(filesToProcess.size / Thread.activeCount().toDouble()).toInt()).map {
398+
399+
val lineAssoc = System.nanoTime()
400+
val functionsToProcess = filesToProcess.flatMap { file ->
401+
val linesOfFunction = file.lines.groupBy { it.functionName }
402+
val list = file.functions.map {
403+
Triple(file, it, linesOfFunction[it.name] ?: emptyList())
404+
}
405+
list
406+
}.sortedByDescending {
407+
it.third.size
408+
}
409+
log.info("Line assoc took ${System.nanoTime() - lineAssoc} ns")
410+
411+
val parallelGen = System.nanoTime()
412+
val queue = ConcurrentLinkedQueue(functionsToProcess)
413+
val result = DumbService.getInstance(project).runReadActionInSmartMode<List<CoverageFileData>> {
414+
(0 until Thread.activeCount()).map {
398415
CompletableFuture.supplyAsync {
399416
runReadAction {
400-
var list = emptyList<CoverageFileData>()
417+
val map = mutableListOf<Pair<File, CoverageFunctionData>>()
401418
ProgressManager.getInstance().executeProcessUnderProgress({
402-
list = processFiles(it, env, project)
419+
generateSequence { queue.poll() }.forEach {
420+
map.add(
421+
it.first
422+
to
423+
processFunction(
424+
it.first,
425+
it.second,
426+
it.third,
427+
env,
428+
project
429+
)
430+
)
431+
}
403432
}, indicator)
404-
list
433+
map
405434
}
406435
}
407-
}.flatMap { it.join() }
436+
}.flatMap { it.get() }.groupBy { it.first }.map { entry ->
437+
CoverageFileData(
438+
env.toLocalPath(entry.key.file).replace('\\', '/'),
439+
entry.value.map { it.second }.associateBy { it.functionName })
440+
}
408441
}
442+
log.info("Parallel branch gen took ${System.nanoTime() - parallelGen} ns")
443+
result
409444
}.associateBy { it.filePath }
410445
} catch (e: CompletionException) {
411446
val cause = e.cause
@@ -424,29 +459,39 @@ class GCCJSONCoverageGenerator(private val myGcov: String, private val myMajorVe
424459
)
425460
}
426461

427-
private fun processFiles(
428-
files: List<File>,
462+
private fun processFunction(
463+
file: File,
464+
function: Function,
465+
lines: List<Line>,
429466
env: CPPEnvironment,
430467
project: Project
431-
) = files.filter { it.lines.isNotEmpty() || it.functions.isNotEmpty() }.map { file ->
468+
) = CoverageFunctionData(
469+
function.startLine.toInt() toCP 0,
470+
function.endLine.toInt() toCP 0,
471+
function.demangledName,
472+
FunctionLineData(lines.associate { it.lineNumber.toInt() to it.count.toLong() }),
473+
if (CoverageGeneratorSettings.getInstance().branchCoverageEnabled)
474+
findStatementsForBranches(
475+
lines,
476+
env.toLocalPath(file.file),
477+
project
478+
) else emptyList()
479+
)
480+
481+
private fun processFile(
482+
file: File,
483+
env: CPPEnvironment,
484+
project: Project
485+
): CoverageFileData? {
486+
if (file.lines.isEmpty() && file.functions.isEmpty()) {
487+
return null
488+
}
432489
val linesOfFunction = file.lines.groupBy { it.functionName }
433490
val functions = file.functions.map { function ->
434-
ProgressManager.checkCanceled()
435491
val lines = linesOfFunction[function.name] ?: emptyList()
436-
CoverageFunctionData(
437-
function.startLine.toInt() toCP 0,
438-
function.endLine.toInt() toCP 0,
439-
function.demangledName,
440-
FunctionLineData(lines.associate { it.lineNumber.toInt() to it.count.toLong() }),
441-
if (CoverageGeneratorSettings.getInstance().branchCoverageEnabled)
442-
findStatementsForBranches(
443-
lines,
444-
env.toLocalPath(file.file),
445-
project
446-
) else emptyList()
447-
)
492+
processFunction(file, function, lines, env, project)
448493
}.associateBy { it.functionName }
449-
CoverageFileData(env.toLocalPath(file.file).replace('\\', '/'), functions)
494+
return CoverageFileData(env.toLocalPath(file.file).replace('\\', '/'), functions)
450495
}
451496

452497
private data class Root(

src/main/kotlin/net/zero9178/cov/data/LLVMCoverageGenerator.kt

Lines changed: 57 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ import java.nio.file.Path
4747
import java.nio.file.Paths
4848
import java.util.concurrent.CompletableFuture
4949
import java.util.concurrent.CompletionException
50-
import kotlin.math.ceil
50+
import java.util.concurrent.ConcurrentLinkedQueue
5151

5252
private sealed class Component
5353

@@ -327,70 +327,65 @@ class LLVMCoverageGenerator(
327327

328328
val filesMap = datas.flatMap { data ->
329329
//Associates the filename with a list of all functions in that file
330-
val funcMap =
330+
val functions =
331331
data.functions.flatMap { func ->
332-
func.filenames.map { it to func }
333-
}.groupBy({ it.first }) {
334-
it.second
332+
func.filenames.filter {
333+
val filePath = environment.toLocalPath(it).replace('\\', '/')
334+
CoverageGeneratorSettings.getInstance().calculateExternalSources || sources.any {
335+
it == filePath
336+
}
337+
}.map { it to func }
335338
}
336339

337-
data.files.fold(emptyList<CoverageFileData>()) fileFold@{ result, file ->
338-
339-
val filePath = environment.toLocalPath(file).replace('\\', '/')
340-
if (!CoverageGeneratorSettings.getInstance().calculateExternalSources && !sources.any {
341-
it == filePath
342-
}) {
343-
return@fileFold result
340+
if (majorVersion >= 12 || !CoverageGeneratorSettings.getInstance()
341+
.branchCoverageEnabled
342+
) {
343+
functions.mapNotNull {
344+
processFunction(environment, project, demangledNames, it.first, it.second)?.let { result ->
345+
it.first to result
346+
}
344347
}
345-
346-
val llvmFunctions = funcMap.getOrDefault(
347-
file,
348-
emptyList()
349-
)
350-
351-
val functionsMap = if (majorVersion >= 12 || !CoverageGeneratorSettings.getInstance()
352-
.branchCoverageEnabled
353-
) {
354-
processFunctions(environment, project, demangledNames, file, llvmFunctions)
355-
} else {
356-
DumbService.getInstance(project).runReadActionInSmartMode<List<CoverageFunctionData>> {
357-
ProgressManager.checkCanceled()
358-
try {
359-
llvmFunctions.chunked(
360-
ceil(
361-
data.files.size / Thread.activeCount()
362-
.toDouble()
363-
).toInt()
364-
).map { functions ->
365-
CompletableFuture.supplyAsync {
366-
runReadAction {
367-
var list = emptyList<CoverageFunctionData>()
368-
ProgressManager.getInstance().executeProcessUnderProgress({
369-
list =
370-
processFunctions(environment, project, demangledNames, file, functions)
371-
}, indicator)
372-
list
373-
}
348+
} else {
349+
val queue = ConcurrentLinkedQueue(functions.sortedByDescending {
350+
it.second.regions.size
351+
})
352+
DumbService.getInstance(project).runReadActionInSmartMode<List<Pair<String, CoverageFunctionData>>> {
353+
ProgressManager.checkCanceled()
354+
try {
355+
(0 until Thread.activeCount()).map {
356+
CompletableFuture.supplyAsync {
357+
runReadAction {
358+
val list = mutableListOf<Pair<String, CoverageFunctionData>>()
359+
ProgressManager.getInstance().executeProcessUnderProgress({
360+
generateSequence { queue.poll() }.forEach {
361+
processFunction(
362+
environment,
363+
project,
364+
demangledNames,
365+
it.first,
366+
it.second
367+
)?.let { result ->
368+
list.add(it.first to result)
369+
}
370+
}
371+
}, indicator)
372+
list
374373
}
375-
}.flatMap { it.join() }
376-
} catch (e: CompletionException) {
377-
val cause = e.cause
378-
if (cause != null) {
379-
throw cause
380-
} else {
381-
throw e
382374
}
375+
}.flatMap { it.join() }
376+
} catch (e: CompletionException) {
377+
val cause = e.cause
378+
if (cause != null) {
379+
throw cause
380+
} else {
381+
throw e
383382
}
384383
}
385-
}.associateBy { it.functionName }
386-
387-
ProgressManager.checkCanceled()
388-
389-
result + CoverageFileData(
390-
filePath,
391-
functionsMap
392-
)
393-
384+
}
385+
}.groupBy { it.first }.map { entry ->
386+
CoverageFileData(
387+
environment.toLocalPath(entry.key).replace('\\', '/'),
388+
entry.value.map { it.second }.associateBy { it.functionName })
394389
}
395390
}.associateBy { it.filePath }
396391
log.info("Processing coverage data took ${System.nanoTime() - processDataStart}ns")
@@ -401,20 +396,20 @@ class LLVMCoverageGenerator(
401396
)
402397
}
403398

404-
private fun processFunctions(
399+
private fun processFunction(
405400
environment: CPPEnvironment,
406401
project: Project,
407402
demangledNames: Map<String, String>,
408403
file: String,
409-
functions: List<Function>
410-
): List<CoverageFunctionData> = functions.fold(emptyList()) { result, function ->
404+
function: Function
405+
): CoverageFunctionData? {
411406

412407
var regions = function.regions.filter {
413408
function.filenames[it.fileId] == file
414409
}
415410

416411
if (regions.isEmpty()) {
417-
return@fold result
412+
return null
418413
}
419414

420415
val functionRegions = regions.map { region ->
@@ -431,7 +426,7 @@ class LLVMCoverageGenerator(
431426
)
432427
}
433428

434-
result + CoverageFunctionData(
429+
return CoverageFunctionData(
435430
regions.first().start,
436431
regions.first().end,
437432
demangledNames[function.name] ?: function.name,

0 commit comments

Comments
 (0)