Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ and `:ddprof-lib:benchmarks`), `ddprof-test` (Java integration tests), `ddprof-t
## Build & Test Quick Reference

Configurations: `release`, `debug`, plus `asan`/`tsan`/`fuzzer` when the sanitizer runtime
is found for the selected compiler (inactive on macOS and musl).
is found for the selected compiler (inactive on macOS and musl), plus opt-in `coverage`
with `-PenableCoverage` (`:ddprof-lib:coverageReport`; the C++ report covers only the
`-O0 -DDEBUG` build — see [doc/building/CodeCoverage.md](doc/building/CodeCoverage.md)).

```bash
./.claude/commands/build-and-summarize buildRelease # release library + JAR
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/*
* Copyright 2026, Datadog, Inc.
* SPDX-License-Identifier: Apache-2.0
*/

Comment thread
zhengyu123 marked this conversation as resolved.
Resolved
package com.datadoghq.native.config

Expand Down Expand Up @@ -54,6 +58,16 @@ object ConfigurationPresets {
// reason: those configs instrument every memory access, so the measured
// per-sample times say more about the sanitizer than about the sampler.
val samplerPerf = project.hasProperty("enableSamplerPerf")

// Opt-in gcov/llvm-cov instrumented build. Inactive by default -- only
// registered as an active configuration (and thus only gets its
// compile/link/assemble/gtest tasks created at all, see
// NativeBuildPlugin/GtestPlugin filtering on getActiveConfigurations)
// when -PenableCoverage is passed. This keeps coverage instrumentation
// completely out of the default build and out of CI runs that don't
// ask for it, mirroring how asan/tsan are gated off when unsupported.
val enableCoverage = project.hasProperty("enableCoverage")

extension.buildConfigurations.apply {
register("release") {
configureRelease(this, currentPlatform, currentArch, version)
Expand All @@ -74,6 +88,9 @@ object ConfigurationPresets {
register("fuzzer") {
configureFuzzer(this, currentPlatform, currentArch, version, rootDir, compiler)
}
register("coverage") {
configureCoverage(this, currentPlatform, currentArch, version, enableCoverage)
}
}

val activeConfigs = extension.getActiveConfigurations(currentPlatform, currentArch)
Expand Down Expand Up @@ -406,4 +423,31 @@ object ConfigurationPresets {
}
}
}

/**
* gcov/llvm-cov-instrumented debug build (`--coverage` is understood by both
* gcc and clang and emits gcov-compatible .gcno/.gcda files, so one preset
* covers both toolchains/platforms without a separate clang source-based
* coverage format). Inactive unless the caller opts in via -PenableCoverage.
*
* `-fprofile-update=atomic`: the default counter updates are plain
* read-modify-writes, so the profiler's concurrent threads (samplers, signal
* handlers, JVM threads) lose and corrupt counts -- a Linux run produced
* negative branch counts (GCC bug 68080) that gcovr refuses to parse.
*/
fun configureCoverage(
config: BuildConfiguration,
platform: Platform,
architecture: Architecture,
version: String,
active: Boolean
) {
// Built on configureDebug so coverage measures the same binary testDebug runs
// (including -z,nodelete on Linux), only with the instrumentation added.
configureDebug(config, platform, architecture, version)
config.active.set(active)
config.publishable.set(false)
config.compilerArgs.addAll("--coverage", "-fprofile-update=atomic")
config.linkerArgs.add("--coverage")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class GtestPlugin : Plugin<Project> {
}

// Check if gtest is available
val hasGtest = checkGtestAvailable()
val hasGtest = PlatformUtils.isGtestAvailable()
if (!hasGtest) {
project.logger.warn("WARNING: Google Test not found - skipping native tests")
}
Expand All @@ -96,21 +96,6 @@ class GtestPlugin : Plugin<Project> {
}
}

private fun checkGtestAvailable(): Boolean {
// Check common gtest locations
val locations = when (PlatformUtils.currentPlatform) {
Platform.MACOS -> listOf(
"/opt/homebrew/opt/googletest",
"/usr/local/opt/googletest"
)
Platform.LINUX -> listOf(
"/usr/include/gtest",
"/usr/local/include/gtest"
)
}
return locations.any { File(it).exists() }
}

private fun createBuildNativeLibsTask(project: Project, extension: GtestExtension, hasGtest: Boolean) {
project.tasks.register("buildNativeLibs") {
group = "build"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/*
* Copyright 2026, Datadog, Inc.
* SPDX-License-Identifier: Apache-2.0
*/

package com.datadoghq.native.model

Expand All @@ -18,11 +22,19 @@ abstract class BuildConfiguration @Inject constructor(
abstract val testJvmArgs: ListProperty<String>
abstract val active: Property<Boolean>

/**
* Whether this configuration's jar is part of the published artifact set. An
* unpublishable configuration (e.g. the opt-in coverage build) is only packaged
* while it is active, for in-repo consumers such as the test projects.
*/
abstract val publishable: Property<Boolean>

override fun getName(): String = configName

init {
// Default to active unless overridden
active.convention(true)
publishable.convention(true)
testEnvironment.convention(emptyMap())
testJvmArgs.convention(emptyList())
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/*
* Copyright 2026, Datadog, Inc.
* SPDX-License-Identifier: Apache-2.0
*/

package com.datadoghq.native.util

Expand Down Expand Up @@ -62,6 +66,24 @@ object PlatformUtils {
)
}

/**
* Google Test install locations probed by the gtest plugin (and by the coverage
* preflight check, since a missing gtest silently drops the gtest half of coverage).
*/
Comment thread
zhengyu123 marked this conversation as resolved.
Resolved
val gtestLocations: List<String>
get() = when (currentPlatform) {
Platform.MACOS -> listOf(
"/opt/homebrew/opt/googletest",
"/usr/local/opt/googletest"
)
Platform.LINUX -> listOf(
"/usr/include/gtest",
"/usr/local/include/gtest"
)
}

fun isGtestAvailable(): Boolean = gtestLocations.any { File(it).exists() }

/**
* Check if a compiler is available and functional
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/*
* Copyright 2026, Datadog, Inc.
* SPDX-License-Identifier: Apache-2.0
*/

package com.datadoghq.profiler

Expand Down Expand Up @@ -78,6 +82,23 @@ import javax.inject.Inject
*/
class ProfilerTestPlugin : Plugin<Project> {

companion object {
Comment thread
zhengyu123 marked this conversation as resolved.
Resolved
private const val ADDITIONAL_JVM_ARGS_EXTENSION = "additionalJvmArgs"

/**
* Extra JVM args a build script can contribute to a musl Exec test task (e.g.
* ddprof-test's JaCoCo agent for the coverage tasks, which Gradle only attaches to
* Test tasks). Get-or-create, since a build script's configureEach on the task can
* run before the plugin's own configuration of it.
*/
@Suppress("UNCHECKED_CAST")
fun additionalJvmArgs(task: Exec): ListProperty<String> =
task.extensions.findByName(ADDITIONAL_JVM_ARGS_EXTENSION) as ListProperty<String>?
?: task.project.objects.listProperty(String::class.java).also {
task.extensions.add(ADDITIONAL_JVM_ARGS_EXTENSION, it)
}
}

override fun apply(project: Project) {
val extension = project.extensions.create(
"profilerTest",
Expand Down Expand Up @@ -362,6 +383,9 @@ class ProfilerTestPlugin : Plugin<Project> {
}
allArgs.addAll(testConfig.extraJvmArgs)
allArgs.addAll(testConfig.configJvmArgs)
// Read here, not contributed via a doFirst of the build script's own: that
Comment thread
zhengyu123 marked this conversation as resolved.
Resolved
// would run before this one and its args would be overwritten.
allArgs.addAll(additionalJvmArgs(execTask).get())

// System properties
testConfig.systemProperties.forEach { (key, value) ->
Expand Down
Loading
Loading