Skip to content
Draft
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
9 changes: 9 additions & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Copyright 2026, Datadog, Inc.

image: alpine:3.22.1@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1

variables:
Expand Down Expand Up @@ -183,6 +185,13 @@ generate-reliability-child-pipeline:
- if: '$JDK_VERSION != null || $DEBUG_LEVEL != null || $HASH != null || $DOWNSTREAM != null'
when: never
- when: on_success
# Replaces the default before_script: its CANCELLED guard would exit before
# generated-reliability.yml is written, and run-reliability-tests (a bridge
# job, so not skipped by that guard) then fails to create its child pipeline
# from the missing artifact. Merge-queue branches (mq-working-branch-*) have
# no open PR, so prepare.sh always sets CANCELLED=true there. RUN_RELIABILITY
# is never set when CANCELLED is, so the noop child pipeline is generated.
before_script: []
script:
- |
if [ "${RUN_RELIABILITY:-}" = "true" ]; then
Expand Down
7 changes: 7 additions & 0 deletions .gitlab/benchmarks/.gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Copyright 2026, Datadog, Inc.

variables:
DD_OCTO_STS_IMAGE: registry.ddbuild.io/images/dd-octo-sts-ci-base:2025.06-1

Expand Down Expand Up @@ -28,6 +30,11 @@ variables:
when: never
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: never
# Merge-queue branches have no open PR, so prepare.sh cancels the pipeline
# and deploy-artifact publishes nothing to benchmark; the BP run would
# only delay the queue (run-benchmarks uses strategy: depend).
- if: '$CI_COMMIT_BRANCH =~ /^mq-working-branch-/'
when: never

# Decide whether benchmarks run for this branch and generate the child
# pipeline YAML accordingly.
Expand Down
17 changes: 16 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<!-- Copyright 2026, Datadog, Inc. -->

<!-- Copyright 2026, Datadog, Inc. -->

<!-- CLAUDE BOOTSTRAP INSTRUCTIONS
If you are Claude and reading this file, check if CLAUDE.md exists in the same directory.
If CLAUDE.md does not exist or only contains the redirect + Compact Instructions below, that
Expand Down Expand Up @@ -86,7 +88,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 Expand Up @@ -125,10 +129,16 @@ debug symbols via `buildDebug`, memory errors via ASan, suppressions in
Custom Kotlin-based native build plugin (`build-logic/`) instead of Gradle's
`cpp-library`/`cpp-application` plugins; it invokes `clang++`/`g++` directly. Build
configurations are discovered dynamically from `ConfigurationPresets.kt`, never hardcoded.
Google Test entry-point archives precede their dependencies so architecture-gated
test files also link on hosts where they register no cases.
Two-JDK pattern: `JAVA_HOME` (build, JDK 17+, Gradle runs on 21 LTS) runs Gradle;
`JAVA_TEST_HOME` runs tests against the target JDK; target bytecode is Java 8 via
`--release 8`.

The `ddprof-stresstest:chaosElements` outgoing configuration shares the chaos JAR
with `ddprof-test` stack-correctness tests. See
[TestingGuide.md](doc/building/TestingGuide.md#virtual-thread-remount-correctness).

Details and troubleshooting:
[build-logic/README.md](build-logic/README.md),
[doc/building/NativeBuildPlugin.md](doc/building/NativeBuildPlugin.md) (task DSL, debug-symbol
Expand All @@ -148,6 +158,7 @@ before working on a subsystem.
| String dictionary (arena chunks, rotation/clear protocols) | [doc/architecture/StringDictionary.md](doc/architecture/StringDictionary.md) |
| RefCountGuard (lock-free RAII reader-drain before reclamation) | [doc/architecture/RefCountGuard.md](doc/architecture/RefCountGuard.md) |
| TLS context (thread-local tracing context, distributed tracing) | [doc/architecture/TLSContext.md](doc/architecture/TLSContext.md) |
| Native stack unwinding (DWARF CFA rules, arm64 temporary argument areas, JNI wrapper phases and LR/PLT, validated Linux signal-frame recovery) | [doc/architecture/JvmSupportLayer.md](doc/architecture/JvmSupportLayer.md) |
| Profiling engines (CPU, wall-clock, allocation, native memory, liveness) | [doc/architecture/ProfilingEngines.md](doc/architecture/ProfilingEngines.md) |
| JFR integration (FlightRecorder, metadata, constant pools, buffers) | [doc/architecture/JfrIntegration.md](doc/architecture/JfrIntegration.md) |
| Live-heap reference chains (implementation, signal scheduling, design) | [doc/architecture/LiveHeapReferenceChainsImplementation.md](doc/architecture/LiveHeapReferenceChainsImplementation.md), [doc/architecture/ReferenceChainsSignalsExplained.md](doc/architecture/ReferenceChainsSignalsExplained.md), [doc/ReferenceChainsDesign.md](doc/ReferenceChainsDesign.md), [doc/ReferenceChainsCollectionSummary.md](doc/ReferenceChainsCollectionSummary.md) |
Expand Down Expand Up @@ -303,6 +314,10 @@ pointer-not-yet-stored. Never permanently advance an iteration cursor past NULL
at the first NULL or track the last contiguous non-NULL entry so missing entries are
retried.

**Stack completeness.** A successful carrier crossing does not establish a complete
virtual-thread stack. Preserve walker-reported truncation through JFR serialization;
see [JvmSupportLayer.md](doc/architecture/JvmSupportLayer.md#continuation-completeness).

**JVM abstraction.** Shared code must not refer to JVM-implementation-specific code directly,
only through the abstraction files: `jvmSupport.{h,inline.h,cpp}`, `jvmThread.{h,cpp}`,
`vmEntry.{h,cpp}`, `javaApi.cpp`. Implementation-specific code lives under `hotspot/`,
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.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,4 @@
// Copyright 2026, Datadog, Inc.

package com.datadoghq.native.gtest

Expand Down Expand Up @@ -161,8 +162,8 @@ class GtestTaskBuilder(
Platform.LINUX -> { /* System paths */ }
}

// Add gtest libraries
lib("gtest", "gtest_main", "gmock", "gmock_main", "dl", "pthread", "m")
// Static archives must precede their dependencies, including when no tests are registered.
lib("gtest_main", "gmock_main", "gmock", "gtest", "dl", "pthread", "m")
if (PlatformUtils.currentPlatform == Platform.LINUX) {
lib("rt")
}
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).
*/
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 {
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
// 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